Steganographic message hiding This code hides a message in an image by altering the blue values in the top left part. This is just for fun as I think it would be easy for a cryptographer to find and decrypt this.

Original



With message We will meet at London bridge at dawn



As you can (cannot) see, as long as the message is short enough, the naked eye has difficulty noticing the difference. Zooming in the top left corner will reveal the trick though.

from PIL import Image
from itertools import zip_longest
import doctest

def read_and_write_after_tranformation(infile, outfile, transformation):
    # Based on a StackOverflow answer.
    im = Image.open(infile)
    list_of_pixels = list(im.getdata())
    im2 = Image.new(im.mode, im.size)
    im2.putdata(transformation(list_of_pixels))
    im2.save(outfile)

def read_rgb(infile):
    return list(Image.open(infile).getdata())

def steganographic_encrypt(message, rgb_tuples):
    """
    Encodes a message in the blue values of the pixels on the top left corner of the image.

    &gt;&gt;&gt; steganographic_encrypt("Hello, World!", [(a, b, c, luminosity) for a, b, c, luminosity in zip(range(12, 30), range(20, 48), range(100, 128), range(100, 128))])
    [(12, 20, 72, 100), (13, 21, 101, 101), (14, 22, 108, 102), (15, 23, 108, 103), (16, 24, 111, 104), (17, 25, 44, 105), (18, 26, 32, 106), (19, 27, 87, 107), (20, 28, 111, 108), (21, 29, 114, 109), (22, 30, 108, 110), (23, 31, 100, 111), (24, 32, 33, 112), (25, 33, 113, 113), (26, 34, 114, 114), (27, 35, 115, 115), (28, 36, 116, 116), (29, 37, 117, 117)]
    """
    return [(red, green, blue if ascii_val is None else ascii_val, luminosity)
              for (red, green, blue, luminosity), ascii_val in zip_longest(rgb_tuples, map(ord, message))]

def steganographic_decrypt(rgb_tuples):
    """
    Decodes a message from the blue values of the pixels on the top left corner of the image.

    &gt;&gt;&gt; steganographic_decrypt([(12, 20, 72, 100), (13, 21, 101, 101), (14, 22, 108, 102), (15, 23, 108, 103), (16, 24, 111, 104), (17, 25, 44, 105), (18, 26, 32, 106), (19, 27, 87, 107), (20, 28, 111, 108), (21, 29, 114, 109), (22, 30, 108, 110), (23, 31, 100, 111), (24, 32, 33, 112), (25, 33, 113, 113), (26, 34, 114, 114), (27, 35, 115, 115), (28, 36, 116, 116), (29, 37, 117, 117)])
    'Hello, World!qrstu'
    """
    return ''.join([chr(blue) for (red, green, blue, luminosity) in rgb_tuples])

if __name__ == "__main__":
    doctest.testmod()
    read_and_write_after_tranformation(
        "cc.png",
        "cc2.png",
        lambda image: steganographic_encrypt("We will meet at London bridge at dawn", image)
    )

    print(steganographic_decrypt(read_rgb("cc2.png"))[0:100])
Working with resources

In read_and_write_after_tranformation you didn't bother to close any of the image resources:


# Based on a StackOverflow answer.
im = Image.open(infile)
list_of_pixels = list(im.getdata())
im2 = Image.new(im.mode, im.size)
im2.putdata(transformation(list_of_pixels))
im2.save(outfile)



Don't believe everything you read on Stack Overflow =)

You can wrap those in a with ... as. And while at it,
im and im2 are not great names.

with Image.open(infile) as in_img:
    list_of_pixels = list(in_img.getdata())
    with Image.new(in_img.mode, in_img.size) as out_img:
        out_img.putdata(transformation(list_of_pixels))
        out_img.save(outfile)


Usability

This is a nice script.
It would be great to make it more usable, 
with command line parameters:


hide sub-command: message to hide, input file, output file
show sub-command: file


You probably already know this, but argparse would be a good tool for this job.