Jump to content

[RST] [Python] Text In Image (GUI) [cmiN]

Recommended Posts


Dupa cum spune si titlul, acest soft ascunde text in imagini, pentru mai multe detalii vezi comentariul sursei.

L-am pus aici ca sa ramana mai mult timp "vizibil", daca e vreo problema il mut la stuff.

In 3 zile, 300 linii de cod, m-am gandit sa ma rezolv cat de cat cu atestatul si cum iubesc sursa libera si impartasirea ei pun la dispozitie asta:


#! /usr/bin/env python
# Text In Image
# 02.01.2012 cmiN
# This is a simple GUI script which can hide text in pictures
# using least significant bit method.
# Also the input text can be encrypted and the output can be decrypted too
# with a symmetric key using AES.
# Writing is done directly on input image so be careful with certain extensions
# because the output will always have the BMP format.
# Contact: cmin764@yahoo/gmail.com

from Tkinter import * # widgets's classes
from tkFileDialog import askopenfilename # get file name
from tkMessageBox import showerror, showinfo # user dialog
from PIL import Image # image converting
from Crypto.Cipher import AES # text cipher

class Engine:
Code for processing the image.
Separated from GUI.

def __init__(self):
""" Initialize parameters. """
self.ext = "bmp" # save format
self.name = None # save name
self.path = None # save path
self.im = None # image object, read and write
self.generator = None # get locations to write/read bits
self.useAES = None # use it or not
self.aes = None # AES object
self.data = None # data to be written to image
self.width = None # image width
self.height = None # image height
self.tmp = None # last string, used when key changes

def binary(self, nr, size):
""" Get 1&0 representation. """
return bin(nr).replace("0b", "").zfill(size * 8)

def path_name(self, path):
""" Split a file path in path and name. """
ind = path.rfind("/") + 1
return (path[:ind], path[ind:])

def set_generator(self):
""" Useful for resetting. """
self.generator = ((wp, hp, ch) for wp in xrange(self.width) # WxHxC
for hp in xrange(self.height)
for ch in xrange(3))

def load(self, path):
""" Load image. """
self.im = Image.open(path)
(self.width, self.height) = self.im.size
(self.path, self.name) = self.path_name(path)
return self.width * self.height * 3 # total useful bytes

def parse_key(self, key):
""" If key exists make an AES object from it. """
if not key:
self.aes = None # empty key == no encryption
return self.parse_string(self.tmp) # must return size (see the next return)
key.decode() # test availability
size = len(key)
for padding in (16, 24, 32): # fixed key size
if size <= padding:
key += chr(0) * (padding - size)
self.aes = AES.new(key)
return self.parse_string(self.tmp) # if key changes you must update string

def parse_string(self, string):
""" Convert to bitstring. """
if not string: # without string can't start the process
self.tmp = None
self.data = None
return 0
string.decode() # test availability
self.tmp = string
if self.useAES and self.aes: # encrypt it
string += chr(0) * ((16 - len(string) % 16) % 16) # multiple of 16 string
string = self.aes.encrypt(string)
string = str().join([self.binary(ord(x), 1) for x in string]) # convert every char in a set of 8 bits
size = self.binary(len(string), 4) # get binary representation of string's length in 4 bytes
self.data = size + string
return len(self.data)

def write(self):
""" Write using LSB. """
self.set_generator() # rearm
for bit in self.data:
(wp, hp, ch) = self.generator.next() # get next position
values = list(self.im.getpixel((wp, hp))) # retrieve its values
tmp = self.binary(values[ch], 1) # convert one of them
values[ch] = int(tmp[:7] + bit, 2) # alter that channel
self.im.putpixel((wp, hp), tuple(values)) # put it back
self.im.save(self.path + self.name, format=self.ext) # save the new output

def read(self):
""" Read from every LSB. """
self.set_generator() # rearm
total = self.width * self.height * 3
if total < 32:
raise Exception("Text not found.")
size = chunk = string = str()
i = 0 # for(i=0; true; ++i)
while True:
(wp, hp, ch) = self.generator.next() # i byte
values = self.im.getpixel((wp, hp))
tmp = self.binary(values[ch], 1)
if i < 32: # it's lame but I prefer string/bitset
size += tmp[7]
if i == 31:
size = int(size, 2)
if size < 1 or (size + 32) > total:
raise Exception("Text not found.")
elif i < size + 32:
chunk += tmp[7]
if len(chunk) == 8:
string += chr(int(chunk, 2))
chunk = str()
i += 1
if self.useAES and self.aes:
if len(string) % 16 != 0:
raise Exception("Text not encrypted.")
string = self.aes.decrypt(string).rstrip(chr(0))
string.decode() # raise an exception if invalid
return string

class GUI(Frame):
Main window, inherited from Frame.
Here we put our widgets and set their behavior.

def __init__(self, master=None, margin=30):
""" Same as Frame's constructor. """
Frame.__init__(self, master, padx=margin, pady=margin)

def widgets(self):
""" Build and grid widgets. """
# ---- create variables ----
self.totalBytes = IntVar() # depends on image size
self.usedBytes = IntVar() # how many of them are used
self.textStatus = StringVar() # used per total bytes
self.useEncryption = IntVar() # 0-plain 1-AES
self.mode = IntVar() # 0-read 1-write
self.textOpt = dict() # text last config
self.keyOpt = dict() # key last config
self.loaded = False # image loaded or not
# ---- create widgets ----
self.label = Label(self, textvariable=self.textStatus)
self.about = Label(self, text="About", fg="blue")
self.text = Text(self, width=30, height=5, fg="grey")
self.scrollbar = Scrollbar(self, orient="vertical", command=self.text.yview)
self.loadButton = Button(self, text="Load", width=5, command=lambda: self.action("load"))
self.readRadio = Radiobutton(self, text="Read", variable=self.mode, value=0, command=self.set_state)
self.checkButton = Checkbutton(self, text="Use AES", variable=self.useEncryption, onvalue=1, offvalue=0, command=self.set_state)
self.startButton = Button(self, text="Start", width=5, state="disabled", command=lambda: self.action("start"))
self.writeRadio = Radiobutton(self, text="Write", variable=self.mode, value=1, command=self.set_state)
self.keyEntry = Entry(self, width=10, fg="grey", show="*")
# ---- show widgets ----
self.label.grid(row=0, column=0, columnspan=2, sticky="w")
self.about.grid(row=0, column=2, sticky="e")
self.text.grid(row=1, column=0, rowspan=3, columnspan=3)
self.scrollbar.grid(row=1, column=3, rowspan=3, sticky="ns")
self.loadButton.grid(row=4, column=0, sticky="w", pady=5)
self.readRadio.grid(row=4, column=1)
self.checkButton.grid(row=4, column=2, sticky="e")
self.startButton.grid(row=5, column=0, sticky="w")
self.writeRadio.grid(row=5, column=1)
self.keyEntry.grid(row=5, column=2, sticky="e")

def behavior(self):
""" Customize widgets. """
self.text.insert(0.0, "Text here")
self.keyEntry.insert(0, "Key here")
self.text.bind("<Button>", self.handle_event)
self.text.bind("<KeyRelease>", self.handle_event)
self.keyEntry.bind("<Button>", self.handle_event)
self.keyEntry.bind("<KeyRelease>", self.handle_event)
self.textOpt = self.get_opt(self.text)
self.keyOpt = self.get_opt(self.keyEntry)
self.about.bind("<Button>", self.handle_event)

def action(self, arg):
""" What every button triggers. """
if arg == "load":
fileTypes = [("BMP", "*.bmp"), ("JPEG", ("*.jpeg", "*.jpg")), ("PNG", "*.png"), ("All Files", "*.*")]
path = askopenfilename(parent=self, title="Open image", filetypes=fileTypes)
if path != "":
except IOError as msg:
showerror("Error", str(msg).capitalize().strip(".") + ".") # some formatting
self.loaded = True
self.master.title("Text In Image - %s" % app.name) # update name in title
elif arg == "start":
if self.mode.get():
except Exception as msg:
showerror("Error", str(msg).capitalize().strip(".") + ".")
showinfo("Info", "Done.")
string = app.read()
except UnicodeError:
showerror("Error", "Text not found or wrong key.")
except Exception as msg:
showerror("Error", str(msg).capitalize().strip(".") + ".")
self.textOpt["fg"] = "black" # touched
self.text.delete(0.0, END)
self.text.insert(0.0, string)
showinfo("Info", "Done.")

def set_status(self):
""" Get used per total bytes. """
string = "%9.3f%s/%9.3f%s"
unit1 = unit2 = "b"
used = self.usedBytes.get()
total = self.totalBytes.get()
if used > total:
if used > 999999:
unit1 = "Mb"
used /= 1000000.0
elif used > 999:
unit1 = "Kb"
used /= 1000.0
if total > 999999:
unit2 = "Mb"
total /= 1000000.0
elif total > 999:
unit2 = "Kb"
total /= 1000.0
self.textStatus.set(string % (used, unit1, total, unit2))

def get_opt(self, widget):
""" Get some options from a widget then pack them. """
opt = dict()
opt["state"] = widget["state"]
opt["fg"] = widget["fg"]
opt["bg"] = widget["bg"]
return opt

def set_state(self):
""" Enable or disable a widget according to option selected. """
if self.mode.get(): # write
self.text.config(state="disabled", bg="lightgrey", fg="darkgrey")
if self.useEncryption.get(): # use AES
app.useAES = True
app.useAES = False
length = app.parse_string(app.tmp)
if self.loaded: # a file is loaded
if self.mode.get() == 0: # read mode
ok = True
elif app.data != None and self.usedBytes.get() <= self.totalBytes.get():
ok = True
ok = False
ok = False # no file loaded
if ok:

def handle_event(self, event):
""" Handle events for specific widgets. """
if event.widget is self.text and self.text["state"] == "normal":
if self.text["fg"] == "grey":
self.text.delete(0.0, END)
self.textOpt["fg"] = self.text["fg"] = "black"
string = self.text.get(0.0, END).strip()
length = app.parse_string(string)
except UnicodeError:
showerror("Error", "Invalid text.")
elif event.widget is self.keyEntry and self.keyEntry["state"] == "normal":
if self.keyEntry["fg"] == "grey":
self.keyEntry.delete(0, END)
self.keyOpt["fg"] = self.keyEntry["fg"] = "black"
key = self.keyEntry.get()[:32] # first 32 (max size is 32)
length = app.parse_key(key)
except UnicodeError:
showerror("Error", "Invalid key.")
elif event.widget is self.about:
showinfo("About", "Hide text, which can be encrypted with AES, in pictures, preferably bitmaps. Coded by cmiN. Visit rstcenter.com")

if __name__ == "__main__":
app = Engine() # core
root = Tk() # toplevel
root.title("Text In Image")
root.maxsize(350, 250)
root.iconbitmap("tii.ico") # comment if you don't have one

Testat pe windows, fedora si slackware, merge perfect, dar sa cititi comentariul principal.

Aveti nevoie de python 2.7, PIL si pycrypto. Pe linux de obicei sunt preinstalate.

Versiune portabila:



Updated: 14.01.2012

Edited by cmiN
  • Upvote 3

Share this post

Link to post
Share on other sites

Pune-i extensia .py in caz de ai .pyw (ca sa-ti afiseze consola) si vezi daca da in output vreo eroare, eventual incearca sa faci mai intai setarile si apoi sa dai load ca sunt curios daca se comporta la fel. Pe win i-am testat comportamentul in toate felurile, logica e buna, dar pe alte platforme daca da vreo eroare din cauza vreounui pachet nu mai face anumite chestii.

Share this post

Link to post
Share on other sites

Confirmare Fedora 14:





Insa dupa scriere, avem o problema:


Byte-ul respectiv ce specifica "BMP" in hexedit: hexd.png

Pentru a repara (asa cum a specificat si cmiN) se redenumeste fisierul cu extensia ".bmp", et voila.

Dupa redenumire inca se poate citi (evident):


Share this post

Link to post
Share on other sites

Tot .bmp salveaza, dar daca modificam extensia imi dadea bataie de cap fisierul, pentru ca daca avea extensie diferita trebuia sa sterg sursa si sa-l las pe asta nou, fiindca as fi avut 2 fisiere pe urma din cauza extensiei ce diferea. Oricum pe windows nu face galagie, iar pe slackware o sa testez dupa ce mi-l pun si eu, dar dupa comportament e clar ca da o eroare undeva. Aveti grija la versiunea de PIL sau cum e instalata ca pe unele distributii nu au decodere. M-am chinuit cu PIL pentru ceva mai multa compatiblitate si pentru un alt mod de scriere a bitilor fata de celelalte softuri de genul.

Cand o sa lansez versiune finala o sa-l "inghet" pentru win intr-o arhiva. Multumesc tuturor :D.

Share this post

Link to post
Share on other sites

Testat si pe slackware, doar am luat si instalat din tar.gz pycrypto, in rest am folosit py2.6 si PIL-ul standard si a mers si la conversie (cu alte extensii).

Share this post

Link to post
Share on other sites


Am rezolvat toate bugurile, ceva modificari in interfata, versiuni portabile pentru windows (fara dependente) si acum afiseaza un mesaj dupa ce s-a executat cu succes comanda lui Start.

Share this post

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Create New...