#! /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:
break
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()
else:
break
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() # rise 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)
self.grid()
self.widgets()
self.behavior()
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.config(yscrollcommand=self.scrollbar.set)
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)
self.set_state()
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 != "":
try:
self.totalBytes.set(app.load(path))
except IOError as msg:
showerror("Error", str(msg).capitalize().strip(".") + ".") # some formatting
else:
self.loaded = True
self.set_state()
self.master.title("Text In Image - %s" % app.name) # update name in title
elif arg == "start":
if self.mode.get():
try:
app.write()
except Exception as msg:
showerror("Error", str(msg).capitalize().strip(".") + ".")
else:
showinfo("Info", "Done.")
else:
try:
string = app.read()
except UnicodeError:
showerror("Error", "Text not found or wrong key.")
except Exception as msg:
showerror("Error", str(msg).capitalize().strip(".") + ".")
else:
self.text.config(state="normal")
self.textOpt["fg"] = "black" # touched
self.text.delete(0.0, END)
self.text.insert(0.0, string)
self.text.config(state="disabled")
self.usedBytes.set(app.parse_string(string))
self.set_status()
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:
self.label.config(fg="red")
else:
self.label.config(fg="black")
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(**self.textOpt)
else:
self.text.config(state="disabled", bg="lightgrey", fg="darkgrey")
if self.useEncryption.get(): # use AES
self.keyEntry.config(**self.keyOpt)
app.useAES = True
else:
self.keyEntry.config(state="disabled")
app.useAES = False
length = app.parse_string(app.tmp)
self.usedBytes.set(length)
self.set_status()
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
else:
ok = False
else:
ok = False # no file loaded
if ok:
self.startButton.config(state="normal")
else:
self.startButton.config(state="disabled")
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()
try:
length = app.parse_string(string)
except UnicodeError:
showerror("Error", "Invalid text.")
else:
self.usedBytes.set(length)
self.set_state()
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)
try:
length = app.parse_key(key)
except UnicodeError:
showerror("Error", "Invalid key.")
else:
self.usedBytes.set(length)
self.set_state()
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
GUI(root)
root.mainloop()