Skip to content

Instantly share code, notes, and snippets.

@laanwj
Last active January 31, 2024 11:06
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save laanwj/51f276c44ba9882bb4b27cc6f3a499a4 to your computer and use it in GitHub Desktop.
Save laanwj/51f276c44ba9882bb4b27cc6f3a499a4 to your computer and use it in GitHub Desktop.
tools to write (block) data to png files and vice versa
#!/usr/bin/env python3
# Distributed under the MIT software license
import binascii, struct, sys, io, argparse
from PIL import Image
IMG_WIDTH = 512 # could be made adaptive...
MIN_HEIGHT = 4 # minimum height of image; twitter won't let us upload anything smaller
BYTES_PER_PIXEL = 4 # RGBA, 8 bit
def div_roundup(x,y):
return (x+y-1)//y
def block_to_png(blockdata):
'''
Embed data in PNG image.
Pass in raw block data, returns raw PNG image.
'''
metaheader = struct.pack('>II', len(blockdata), binascii.crc32(blockdata))
data = metaheader + blockdata
# determine size;
# add an extra pixel to make sure that there's always padding, twitter will
# convert to JPG in the unlikely case that the entire alpha channel is 255
pixels = div_roundup(len(data), BYTES_PER_PIXEL) + 1
width = IMG_WIDTH
height = max(div_roundup(pixels, width), MIN_HEIGHT)
# add zero-padding
padding_len = width*height*BYTES_PER_PIXEL - len(data)
data += b'\x00' * padding_len
# compress as PNG
img = Image.frombytes("RGBA", (width, height), data)
outf = io.BytesIO()
img.save(outf, "PNG", optimize=True) # optimize sets compress_level to 9(max) as well
return outf.getvalue()
def parse_arguments():
parser = argparse.ArgumentParser(description='Convert bitcoin block data to PNG image')
parser.add_argument('infilename', metavar='INFILE.hex', type=str, nargs='?',
help='Block data (hex output from RPC "getblock <hash> false"). If not specified, read from standard input.')
parser.add_argument('outfilename', metavar='OUTFILE.png', type=str, nargs='?',
help='PNG image name. If not specified, write to standard output.')
return parser.parse_args()
def main():
args = parse_arguments()
if args.infilename is not None:
with open(args.infilename, 'r') as f:
blockdata = binascii.a2b_hex(f.read().strip())
else:
blockdata = binascii.a2b_hex(sys.stdin.read().strip())
imgdata = block_to_png(blockdata)
if args.outfilename is not None:
with open(args.outfilename, 'wb') as f:
f.write(imgdata)
else:
sys.stdout.buffer.write(imgdata)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
# Distributed under the MIT software license
import binascii, struct, sys, io, argparse
from PIL import Image
def png_to_block(blockdata):
'''
Extract embedded data in PNG image.
Pass in raw PNG image, return raw block data.
'''
inf = io.BytesIO(blockdata)
img = Image.open(inf)
if img.format != 'PNG' or img.mode != 'RGBA':
raise ValueError('Image has invalid format-mode {}-{}'.format(img.format, img.mode))
imgdata = img.tobytes()
metaheader = imgdata[0:8]
(blocksize,crc) = struct.unpack('>II', metaheader)
print('size {:d}×{:d}'.format(img.width, img.height))
print('metaheader:')
print(' size : {:d}'.format(blocksize))
print(' CRC32 : {:x}'.format(crc))
if 8+blocksize > len(imgdata):
raise ValueError('Block size does not fit in image, image was cropped or header corrupted')
blockdata = imgdata[8:8+blocksize]
if binascii.crc32(blockdata) != crc:
raise ValueError('Block CRC mismatch')
return blockdata
def parse_arguments():
parser = argparse.ArgumentParser(description='Convert PNG image back to bitcoin block data')
parser.add_argument('infilename', metavar='INFILE.png', type=str, nargs='?',
help='PNG image name. If not specified, read from standard input.')
parser.add_argument('outfilename', metavar='OUTFILE.txt', type=str, nargs='?',
help='Block data (hex output like RPC "getblock <hash> false"). If not specified, write to standard output.')
return parser.parse_args()
def main():
args = parse_arguments()
if args.infilename is not None:
with open(args.infilename, 'rb') as f:
imgdata = f.read()
else:
imgdata = sys.stdin.buffer.read()
blockdata = png_to_block(imgdata)
if args.outfilename is not None:
with open(args.outfilename, 'w') as f:
f.write(binascii.b2a_hex(blockdata).decode() + '\n')
else:
sys.stdout.write(binascii.b2a_hex(blockdata).decode() + '\n')
if __name__ == '__main__':
main()
@christianboyle
Copy link

Here's some additional help for anyone trying to use these scripts:

  1. Get the file: https://pbs.twimg.com/media/DjNNVO1XcAMzSAx.png:orig
  2. $ pip3 install Pillow
  3. Run the example in the tweet:
    $ python3 imgtoblock.py DjNNVO1XcAMzSAx.png output.txt
  4. This process will create a file called "output.txt" in addition to printing the size (8 byte length) and CRC32 (header) in the CLI:
    size 512×571
    metaheader:
    size : 1168239
    CRC32 : 5e8dabe1
  5. Open output.txt and notice that the HEX matches what's shown here: https://blockchain.info/block/0000000000000000000972454a8f0553eaae550dccfc0abd7c9be41e5b0f3078?format=hex

So far, so good. Now let's go the other direction and encode this block back to an image:

  1. Using the file we just created, output.txt (the HEX representation of block 534106) we run blocktoimg.py:
    $ python3 blocktoimg.py output.txt output.png
  2. This process will create a file called "output.png" in addition to printing the dimensions of the exported image:
    exporting 512×571 to output.png
  3. This image is identical to our original reference image in the tweet.
    You can verify this by running the images through a diff viewer.
    The comparison results in this output, showing no difference between the two images (no red coloring): https://i.imgur.com/4cHAldg.jpg

@laanwj
Copy link
Author

laanwj commented Jul 29, 2018

@christianboyle Thank you for the write-up. My snippet is really under-documented, hadn't expected it to catch on so much!

Edit: improved the code a bit, so that it can be used as module and in shell pipe constructs, and arguments are documented.

@radWorx
Copy link

radWorx commented Jul 29, 2018

hello, Trying to runt the script but I get "SyntaxError: Non-ASCII character '\xc3' in file(imgtoblock.py) on line 19, but no encoding declared;"
What encoding should I declare in the file?

@laanwj
Copy link
Author

laanwj commented Aug 1, 2018

@radWorx are you trying to run it with python 2? This is python 3, and for python 3 all source files are UTF-8 and there is no need to specify it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment