Hide Private Message in an Image (Python)

Updated vegaseat 5 Tallied Votes 10K Views Share

Let's say you want to send a short private message to your friend, but don't want grandma to snoop around and find it. One way is to use the Python Image Library (PIL) and hide the message in a picture file's pixels. Just looking at the picture you will barely detect any difference between the before and after picture. All your friend needs is the decode_image portion of this simple code to get the message back.

Slavi commented: Pretty damn cool +6
ddanbe commented: Great! +15
''' PIL_HideText1.py
hide a short message (255 char max) in an image
the image has to be .bmp or .png format
and the image mode has to be 'RGB'
'''

from PIL import Image

def encode_image(img, msg):
    """
    use the red portion of an image (r, g, b) tuple to
    hide the msg string characters as ASCII values
    red value of the first pixel is used for length of string
    """
    length = len(msg)
    # limit length of message to 255
    if length > 255:
        print("text too long! (don't exeed 255 characters)")
        return False
    if img.mode != 'RGB':
        print("image mode needs to be RGB")
        return False
    # use a copy of image to hide the text in
    encoded = img.copy()
    width, height = img.size
    index = 0
    for row in range(height):
        for col in range(width):
            r, g, b = img.getpixel((col, row))
            # first value is length of msg
            if row == 0 and col == 0 and index < length:
                asc = length
            elif index <= length:
                c = msg[index -1]
                asc = ord(c)
            else:
                asc = r
            encoded.putpixel((col, row), (asc, g , b))
            index += 1
    return encoded

def decode_image(img):
    """
    check the red portion of an image (r, g, b) tuple for
    hidden message characters (ASCII values)
    """
    width, height = img.size
    msg = ""
    index = 0
    for row in range(height):
        for col in range(width):
            try:
                r, g, b = img.getpixel((col, row))
            except ValueError:
                # need to add transparency a for some .png files
                r, g, b, a = img.getpixel((col, row))		
            # first pixel r value is length of message
            if row == 0 and col == 0:
                length = r
            elif index <= length:
                msg += chr(r)
            index += 1
    return msg


# pick a .png or .bmp file you have in the working directory
# or give full path name
original_image_file = "Beach7.png"
#original_image_file = "Beach7.bmp"

img = Image.open(original_image_file)
# image mode needs to be 'RGB'
print(img, img.mode)  # test

# create a new filename for the modified/encoded image
encoded_image_file = "enc_" + original_image_file

# don't exceed 255 characters in the message
secret_msg = "this is a secret message added to the image"
print(len(secret_msg))  # test

img_encoded = encode_image(img, secret_msg)

if img_encoded:
    # save the image with the hidden text
    img_encoded.save(encoded_image_file)
    print("{} saved!".format(encoded_image_file))

    # view the saved file, works with Windows only
    # behaves like double-clicking on the saved file
    import os
    os.startfile(encoded_image_file)
    '''
    # or activate the default viewer associated with the image
    # works on more platforms like Windows and Linux
    import webbrowser
    webbrowser.open(encoded_image_file)
    '''
    # get the hidden text back ...
    img2 = Image.open(encoded_image_file)
    hidden_text = decode_image(img2)
    print("Hidden text:\n{}".format(hidden_text))
vegaseat 1,735 DaniWeb's Hypocrite Team Colleague

Just for kicks, here is the original and encoded image file used ...
1edb0bfc24e1afbcfa7d358659e147ecf690d29feeafb497cc3020c47bfead8d

Lardmeister 461 Posting Virtuoso

I downloaded the encode picture from above and applied this little code:

''' PIL_HideText_decode.py
get the hidden text back from an encoded image
'''

from PIL import Image

def decode_image(img):
    """
    check the red portion of an image (r, g, b) tuple for
    hidden message characters (ASCII values)
    """
    width, height = img.size
    msg = ""
    index = 0
    for row in range(height):
        for col in range(width):
            try:
                r, g, b = img.getpixel((col, row))
            except ValueError:
                # need to add transparency a for some .png files
                r, g, b, a = img.getpixel((col, row))
            # first pixel r value is length of message
            if row == 0 and col == 0:
                length = r
            elif index <= length:
                msg += chr(r)
            index += 1
    return msg

# downloaded from internet web page
encoded_image_file = "Beach7_enc.png"

# get the hidden text back ...
img2 = Image.open(encoded_image_file)
print(img2, img2.mode)  # test
hidden_text = decode_image(img2)
print("Hidden text:\n{}".format(hidden_text))

''' my result -->
Hidden text:
this is a secret message added to the image
'''

It works!

maria anna 0 Newbie Poster

Inline Code Example Here

maria anna 0 Newbie Poster

Hai
I want to convert this code into gimp plugin in python language.Their is no security plugin exits in gimp. Please help me to convert the above code into a plugin . After applying plugin i want to view the hidden secret msg in the image. Please help me

Schol-R-LEA 1,446 Commie Mutant Traitor Featured Poster

Actually, there already exists a steganographic plugin for GIMP, which you can probbly use off the shelf for your purposes. The source should be viewable if you want to go over it or make changes to it.

jismy jose 0 Newbie Poster

image steganography plugin in python

from gimpfu import *
#import math, random, os, sys

def do_message_box(msg):
    handler = pdb.gimp_message_get_handler()
    pdb.gimp_message_set_handler(MESSAGE_BOX)   #{ MESSAGE-BOX (0), CONSOLE (1), ERROR-CONSOLE (2) }
    pdb.gimp_message(msg)
    pdb.gimp_message_set_handler(handler)

def hide_image(image, layer, carrier, radix):

    if layer.width > carrier.width or layer.height > carrier.height:
    do_message_box("Carrier image must be at least as large as the image to be hidden!!")
    return

    s = int(radix)

    pdb.gimp_image_undo_group_start(image)
    newlayer = pdb.gimp_layer_new_from_drawable(carrier, image)
    image.add_layer(newlayer, -1)

    gimp.progress_init("Hiding image...")

    ntx = int( (layer.width+63)/64 )            # number of tiles horizontally
    nty = int( (layer.height+63)/64 )           # number of tiles vertically
    for i in range(ntx):
    for j in range(nty):                    # for each tile...
        srcTile = layer.get_tile(False, j, i)
        carTile = carrier.get_tile(False, j, i)
        dstTile = newlayer.get_tile(False, j, i)
        for x in range(srcTile.ewidth):         # for each pixel in tile..
        for y in range(srcTile.eheight):
            spixel = srcTile[x,y]
            cpixel = carTile[x,y]
            r = min(255, ord(cpixel[0]) - (ord(cpixel[0]) % s) + int(s * ord(spixel[0]) / 256))
            g = min(255, ord(cpixel[1]) - (ord(cpixel[1]) % s) + int(s * ord(spixel[1]) / 256))
            b = min(255, ord(cpixel[2]) - (ord(cpixel[2]) % s) + int(s * ord(spixel[2]) / 256))
            dstTile[x,y] = chr(r) + chr(g) + chr(b)
    gimp.progress_update(float(i)/ntx)

    newlayer.flush()
    newlayer.update(0, 0, newlayer.width, newlayer.height)

    pdb.gimp_image_resize_to_layers(image)
    pdb.gimp_image_undo_group_end(image)
    gimp.displays_flush() 

def recover_image(image, layer, radix):

    s = int(radix)

    pdb.gimp_image_undo_group_start(image)
    newlayer = layer.copy()
    image.add_layer(newlayer, -1)

    gimp.progress_init("Recovering image...")

    ntx = int( (layer.width+63)/64 )            # number of tiles horizontally
    nty = int( (layer.height+63)/64 )           # number of tiles vertically
    for i in range(ntx):
    for j in range(nty):                    # for each tile...
        srcTile = layer.get_tile(False, j, i)
        dstTile = newlayer.get_tile(False, j, i)
        for x in range(srcTile.ewidth):         # for each pixel in tile..
        for y in range(srcTile.eheight):
            pixel = srcTile[x,y]
            r = (ord(pixel[0]) % s) * 256 / s
            g = (ord(pixel[1]) % s) * 256 / s
            b = (ord(pixel[2]) % s) * 256 / s
            dstTile[x,y] = chr(r) + chr(g) + chr(b)
    gimp.progress_update(float(i)/ntx)

    newlayer.flush()
    newlayer.update(0, 0, newlayer.width, newlayer.height)

    pdb.gimp_image_undo_group_end(image)
    gimp.displays_flush() 

register(
    "python_fu_simple_steg_hide_image",
    "Hides an RGB image in another RGB image",
    "Hides an RGB image in another RGB image",
    "",
    "",
    "Version 1.0 (2-4-2015)",
    "Hide image",
    "RGB",
    [
      (PF_IMAGE,    "image",    "Input image", None),
      (PF_DRAWABLE, "drawable", "Input layer", None),
      (PF_DRAWABLE, "carrier",  "Carrier image", None),
      (PF_SPINNER,  "radix",    "Radix", 4, (2, 64, 1))
    ],
    [],
    hide_image,
    menu="<Image>/MyScripts/Steg/Image Steganography")

register(
    "python_fu_simple_steg_recover_image",
    "Recovers a 'hidden image' from an RGB image",
    "Recovers a 'hidden image' from an RGB image",
    "",
    "",
    "Version 1.0 (2-4-2015)",
    "Recover image",
    "RGB",
    [
      (PF_IMAGE,    "image",    "Input image", None),
      (PF_DRAWABLE, "drawable", "Input layer", None),
      (PF_SPINNER,  "radix",    "Radix", 4, (2, 64, 1))
    ],
    [],
    recover_image,
    menu="<Image>/MyScripts/Steg/Image Steganography")

main()

*

jismy jose 0 Newbie Poster

text steganography plugin

from gimpfu import *

def do_message_box(msg):
    handler = pdb.gimp_message_get_handler()
    pdb.gimp_message_set_handler(MESSAGE_BOX)   #{ MESSAGE-BOX (0), CONSOLE (1), ERROR-CONSOLE (2) }
    pdb.gimp_message(msg)
    pdb.gimp_message_set_handler(handler)

def add_steg_text(image, layer, text):

    length = len(text)
    # limit length of message to 255
    if length > 255:
        do_message_box("text too long! (don't exeed 255 characters)")
        return

    pdb.gimp_image_undo_group_start(image)
    newlayer = layer.copy()
    image.add_layer(newlayer, -1)
    pr = newlayer.get_pixel_rgn(0,0,newlayer.width,newlayer.height)

    for index in range(length+1):
        col = (index) / layer.height
        row = (index) % layer.height
        if index == 0:              #store length in first pixel
            newpixel = chr(length)
        else:                   #store string in subsequent pixels
            newpixel = text[index-1]
        newpixel += pr[col,row][1] + pr[col,row][2]
        if layer.has_alpha:
            newpixel += pr[col,row][3]
        pr[col,row] = newpixel 

    newlayer.flush()
    layer.merge_shadow(True)
    newlayer.update(0, 0, newlayer.width, newlayer.height)

    pdb.gimp_image_undo_group_end(image)
    gimp.displays_flush() 

def read_steg_text(image, layer, outputmode):

    pr = layer.get_pixel_rgn(0,0,layer.width,layer.height)
    length = ord(pr[0,0][0])
    msg = ""
    for index in range(length):
        col = (index+1) / layer.height
        row = (index+1) % layer.height
        msg += pr[col,row][0]

    if outputmode == 0:
        do_message_box("%s" %msg)

register(
    "python_fu_simple_steg_text_add",
    "Adds a simple 'hidden message' to an RGB image",
    "Adds a simple 'hidden message' to an RGB image",
    "",
    "",
    "Version 1.0 (25-3-2015)",
    "Add message",
    "RGB*",
    [
      (PF_IMAGE,    "image",    "Input image", None),
      (PF_DRAWABLE, "drawable", "Input layer", None),
      (PF_STRING,   "text",     "Text:",       "")
    ],
    [],
    add_steg_text,
    menu="<Image>/MyScripts/Steg/Text Steganography")

register(
    "python_fu_simple_steg_text_read",
    "Reads a simple 'hidden message' added to an RGB image",
    "Reads a simple 'hidden message' added to an RGB image",
    "",
    "",
    "Version 1.0 (25-3-2015)",
    "Display message",
    "RGB*",
    [
      (PF_IMAGE,    "image",    "Input image", None),
      (PF_DRAWABLE, "drawable", "Input layer", None),
      (PF_OPTION,   "outputmode", "Output mode", 0, ("Display in a message box", "None"))
    ],
    [],
    read_steg_text,
    menu="<Image>/MyScripts/Steg/Text Steganography")

main()
Chirag_4 0 Newbie Poster

What if i have a text file then what to do?

JamesCherrill 4,733 Most Valuable Poster Team Colleague Featured Poster

The problem with a text file is that any modification to the text is highly visible to the user, so this technique won't work.

sadiq_4 0 Newbie Poster

please can you summarize the algorithm for me.

sadiq_4 0 Newbie Poster

for images with large size the algorithm takes a long time for hiding the information

Amjed_1 0 Newbie Poster

is there any way to make this work with GIF instead of png ?

rproffitt 2,565 "Nothing to see here." Moderator

@Amjed_1, such things are out there. https://www.google.com/search?client=firefox-b-1&q=Steganography+GIF Change that to include "source code" to find them.

vegaseat 1,735 DaniWeb's Hypocrite Team Colleague

The algorithm used here works best wirth PNG or BMP image files. Image file formats that use compression that leads to a loss in picture quality are not suitable.

JamesCherrill 4,733 Most Valuable Poster Team Colleague Featured Poster

GIF uses lossless compression that does not degrade the quality of the image. As such there no a priori reason why steganography woudn't work.

nourimane commented: Changing some pixels only can made a small error in compressed format but if the image is large these errors remain invisible for the human eye. +0
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.