Added new steganalysis method: exif header for jpeg file. Added new demon image (Elisha Cuthbert).

This commit is contained in:
cedricbonhomme 2011-03-24 09:01:45 +01:00
parent 0310d634bd
commit fc03000cb1
4 changed files with 727 additions and 0 deletions

73
exif-header.py Normal file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env python
#-*- coding: utf-8 -*-
# Stéganô - Stéganô is a basic Python Steganography module.
# Copyright (C) 2010-2011 Cédric Bonhomme - http://cedricbonhomme.org/
#
# For more information : http://bitbucket.org/cedricbonhomme/stegano/
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.1 $"
__date__ = "$Date: 2010/03/24 $"
__license__ = "GPLv3"
# Thanks to: http://www.julesberman.info/spec2img.htm
def hide(img, img_enc, copyright="http://bitbucket.org/cedricbonhomme/stegano"):
"""
"""
import shutil
import datetime
from exif.minimal_exif_writer import MinimalExifWriter
file = open("lorem_ipsum.txt", "r")
text = "\nImage annotation date: "
text = text + str(datetime.date.today())
text = text + "\nImage description:\n"
text = text + file.read()
file.close()
try:
shutil.copy(img, img_enc)
except Exception as e:
print("Impossible to copy image:", e)
return
f = MinimalExifWriter(img_enc)
f.removeExif()
f.newImageDescription(text)
f.newCopyright(copyright, addYear = 1)
f.process()
def reveal(img):
"""
"""
from exif.minimal_exif_reader import MinimalExifReader
try:
g = MinimalExifReader(img)
except:
print("Impossible to read description.")
return
print(g.imageDescription())
print(("Copyright " + g.copyright()))
#print g.dateTimeOriginal()s
if __name__ == "__main__":
hide(img='./pictures/Elisha-Cuthbert.jpg', img_enc='./pictures/Elisha-Cuthbert_enc.jpg')
reveal(img='./pictures/Elisha-Cuthbert_enc.jpg')

197
exif/minimal_exif_reader.py Normal file
View file

@ -0,0 +1,197 @@
"""
This module offers one class, MinimalExifReader. Pass jpg filename
to the constructor. Will read minimal exif info from the file. Three
"public" functions available:
imageDescription()--returns Exif ImageDescription tag (0x010e) contents,
or '' if not found.
copyright()--returns Exif copyright tag (0x8298) contents, or '' if not
found.
dateTimeOriginal()--returns Exif DateTimeOriginal tag (0x9003) contents,
or '' if not found. If found, the trailing nul char
is stripped. This function also takes an optional
format string to apply time.strftime-style formatting
to the date time.
Brought to you by Megabyte Rodeo Software.
"""
# Written by Chris Stromberger, 10/2004. Public Domain.
# Much is owed to Thierry Bousch's exifdump.py:
# http://topo.math.u-psud.fr/~bousch/exifdump.py
#---------------------------------------------------------------------
class ExifFormatException(Exception):
pass
#---------------------------------------------------------------------
class MinimalExifReader:
IMAGE_DESCRIPTION_TAG = 0x010e
COPYRIGHT_TAG = 0x8298
EXIF_SUBIFD_TAG = 0x8769
DATE_TIME_ORIGINAL_TAG = 0x9003
#---------------------------------------
def __init__(self, filename):
"""Pass in jpg exif file name to process. Will attempt to find tags
of interest."""
self.tagsToFind = {self.IMAGE_DESCRIPTION_TAG:'',
self.COPYRIGHT_TAG:'',
self.DATE_TIME_ORIGINAL_TAG:''}
# Read first bit of file to see if exif file.
f = open(filename, 'rb')
firstTwoBytes = f.read(2)
if firstTwoBytes != '\xff\xd8':
f.close()
raise ExifFormatException("Missing SOI marker")
appMarker = f.read(2)
# See if there's an APP0 section, which sometimes appears.
if appMarker == '\xff\xe0':
#print "Skipping app0"
# Yes, we have app0. Skip over it.
app0DataLength = ord(f.read(1)) * 256 + ord(f.read(1))
app0 = f.read(app0DataLength - 2)
appMarker = f.read(2)
if appMarker != '\xff\xe1':
raise ExifFormatException("Can't find APP1 marker")
exifHeader = f.read(8)
#import binascii
#print binascii.hexlify(exifHeader)
if (exifHeader[2:6] != 'Exif' or
exifHeader[6:8] != '\x00\x00'):
f.close()
raise ExifFormatException("Malformed APP1")
app1DataLength = ord(exifHeader[0]) * 256 + ord(exifHeader[1])
#print app1DataLength
# Read exif info starting at the beginning of the self.tiff section.
# This is 8 bytes into the app1 section, so subtract 8 from
# app1 length.
self.tiff = f.read(app1DataLength - 8)
f.close()
self.endian = self.tiff[0]
if self.endian not in ('I', 'M'):
raise ExifFormatException("Invalid endianess found: %s" % self.endian)
# Now navigate to the items of interest and get them.
ifdStart = self.getValueAtLocation(4, 4)
self.ifdSearch(ifdStart)
#---------------------------------------
def imageDescription(self):
"""Return image description tag contents or '' if not found."""
return self.tagsToFind[self.IMAGE_DESCRIPTION_TAG].strip('\x20\x00')
#---------------------------------------
def copyright(self):
"""Return copyright tag contents or '' if not found."""
return self.tagsToFind[self.COPYRIGHT_TAG].strip('\x20\x00')
#---------------------------------------
def dateTimeOriginal(self, formatString = None):
"""Pass in optional format string to get time.strftime style formatting,
else get default exif format for date time string (without trailing nul).
Returns '' if tag not found."""
# The datetime should end in nul, get rid of it.
if formatString is None or not self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG]:
return self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG].strip('\x20\x00')
else:
# This will only work if the datetime string is in the standard exif format (i.e., hasn't been altered).
try:
import time
return time.strftime(formatString, time.strptime(self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG].strip('\x20\x00'), '%Y:%m:%d %H:%M:%S'))
except:
return self.tagsToFind[self.DATE_TIME_ORIGINAL_TAG].strip('\x20\x00')
#---------------------------------------
def ifdSearch(self, ifdStart):
numIfdEntries = self.getValueAtLocation(ifdStart, 2)
tagsStart = ifdStart + 2
for entryNum in range(numIfdEntries):
# For my purposes, all files will have either no tags, or
# only our tags of interest, so no need to waste time trying to
# break out of the loop early.
thisTagStart = tagsStart + 12 * entryNum
tagId = self.getValueAtLocation(thisTagStart, 2)
if tagId == self.EXIF_SUBIFD_TAG:
# This is a special tag that points to another ifd. Our
# date time original tag is in the sub ifd.
self.ifdSearch(self.getTagValue(thisTagStart))
elif tagId in self.tagsToFind:
assert(not self.tagsToFind[tagId])
self.tagsToFind[tagId] = self.getTagValue(thisTagStart)
#---------------------------------------
def getValueAtLocation(self, offset, length):
slice = self.tiff[offset:offset + length]
if self.endian == 'I':
val = self.s2n_intel(slice)
else:
val = self.s2n_motorola(slice)
return val
#---------------------------------------
def s2n_motorola(self, str):
x = 0
for c in str:
x = (x << 8) | ord(c)
return x
#---------------------------------------
def s2n_intel(self, str):
x = 0
y = 0
for c in str:
x = x | (ord(c) << y)
y = y + 8
return x
#---------------------------------------
def getTagValue(self, thisTagStart):
datatype = self.getValueAtLocation(thisTagStart + 2, 2)
numBytes = [ 1, 1, 2, 4, 8, 1, 1, 2, 4, 8 ] [datatype-1] * self.getValueAtLocation(thisTagStart + 4, 4)
if numBytes > 4:
offsetToValue = self.getValueAtLocation(thisTagStart + 8, 4)
return self.tiff[offsetToValue:offsetToValue + numBytes]
else:
if datatype == 2 or datatype == 1 or datatype == 7:
return self.tiff[thisTagStart + 8:thisTagStart + 8 + numBytes]
else:
return self.getValueAtLocation(thisTagStart + 8, numBytes)
#---------------------------------------
def __str__(self):
return str(self.tagsToFind)
#---------------------------------------------------------------------
if __name__ == '__main__':
import sys
if len(sys.argv) == 1:
print "Pass jpgs to process."
sys.exit(1)
for filename in sys.argv[1:]:
try:
f = MinimalExifReader(filename)
print filename
print "description: '%s'" % f.imageDescription()
print "copyright: '%s'" % f.copyright()
print "dateTimeOriginal: '%s'" % f.dateTimeOriginal()
print "dateTimeOriginal: '%s'" % f.dateTimeOriginal('%B %d, %Y %I:%M:%S %p')
print
except ExifFormatException, ex:
sys.stderr.write("Exif format error: %s\n" % ex)
except:
sys.stderr.write("Unable to process %s\n" % filename)

457
exif/minimal_exif_writer.py Normal file
View file

@ -0,0 +1,457 @@
"""
Offers one class, MinimalExifWriter, which takes a jpg filename
in the constructor. Allows you to: remove exif section, add
image description, add copyright. Typical usage:
f = MinimalExifWriter('xyz.jpg')
f.newImageDescription('This is a photo of something very interesting!')
f.newCopyright('Jose Blow, All Rights Reserved', addCopyrightYear = 1)
f.process()
Class methods:
newImageDescription(description)--will add Exif ImageDescription to file.
newCopyright(copyright, addSymbol = 0, addYear = 0)--will add Exif Copyright to file.
Will optionally prepend copyright symbol, or copyright symbol and current year.
removeExif()--will obliterate existing exif section.
process()--call after calling one or more of the above. Will remove existing exif
section, optionally saving some existing tags (see below), and insert a new exif
section with only three tags at most: description, copyright and date time original.
If removeExif() not called, existing description (or new description if newDescription()
called), existing copyright (or new copyright if newCopyright() called) and existing
"DateTimeOriginal" (date/time picture taken) tags will be rewritten to the new
minimal exif section.
Run at comand line with no args to see command line usage.
Does not work on unix due to differences in mmap. Not sure what's up there--
don't need it on unix!
Brought to you by Megabyte Rodeo Software.
http://www.fetidcascade.com/pyexif.html
"""
# Written by Chris Stromberger, 10/2004. Public Domain.
# Last updated: 12/3/2004.
DUMP_TIFF = 0
VERBOSE = 0
if VERBOSE:
import binascii
import mmap
import sys
import minimal_exif_reader
#---------------------------------------------------------------------
class ExifFormatException(Exception):
pass
#---------------------------------------------------------------------------
class MinimalExifWriter:
SOI_MARKER = '\xff\xd8'
APP0_MARKER = '\xff\xe0'
APP1_MARKER = '\xff\xe1'
# Standard app0 segment that will work for all files. We hope.
# Based on http://www.funducode.com/freec/Fileformats/format3/format3b.htm.
APP0 = '\xff\xe0\x00\x10\x4a\x46\x49\x46\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00'
def __init__(self, filename):
self.filename = filename
self.removeExifSection = 0
self.description = None
self.copyright = None
self.dateTimeOriginal = None
#---------------------------------------------
def newImageDescription(self, description):
self.description = description
#---------------------------------------------
def newCopyright(self, copyright, addSymbol = 0, addYear = 0):
if addYear:
import time
year = time.localtime()[0]
self.copyright = "\xa9 %s %s" % (year, copyright)
elif addSymbol:
self.copyright = "\xa9 %s" % copyright
else:
self.copyright = copyright
#---------------------------------------------
def removeExif(self):
self.removeExifSection = 1
#---------------------------------------------
def process(self):
if not self.removeExifSection:
self.getExistingExifInfo()
if VERBOSE:
print self
import os
try:
fd = os.open(self.filename, os.O_RDWR)
except:
sys.stderr.write('Unable to open "%s"\n' % filename)
return
self.m = mmap.mmap(fd, 0)
os.close(fd)
# We only add app0 if all we're doing is removing the exif section.
justRemovingExif = self.description is None and self.copyright is None and self.removeExifSection
if VERBOSE: print 'justRemovingExif=%s' % justRemovingExif
self.removeExifInfo(addApp0 = justRemovingExif)
if justRemovingExif:
self.m.close()
return
# Get here means we are adding new description and/or copyright.
self.removeApp0()
totalTagsToBeAdded = len(filter(None, (self.description, self.copyright, self.dateTimeOriginal)))
assert(totalTagsToBeAdded > 0)
# Layout will be: firstifd|description|copyright|exififd|datetime.
# First ifd will have tags: desc|copyright|subifd tag.
ifd = [self.twoBytesHexIntel(totalTagsToBeAdded)]
ifdEnd = ['\x00\x00\x00\x00']
NUM_TAGS_LEN = 2
TAG_LEN = 12
NEXT_IFD_OFFSET_LEN = 4
TIFF_HEADER_LENGTH = 8
ifdLength = NUM_TAGS_LEN + TAG_LEN * totalTagsToBeAdded + NEXT_IFD_OFFSET_LEN
# Subifd only has one tag.
SUBIFD_LENGTH = NUM_TAGS_LEN + TAG_LEN + NEXT_IFD_OFFSET_LEN
offsetToEndOfData = ifdLength + TIFF_HEADER_LENGTH
if self.description:
ifd.append(self.descriptionTag(len(self.description), offsetToEndOfData))
ifdEnd.append(self.description)
offsetToEndOfData += len(self.description)
if self.copyright:
ifd.append(self.copyrightTag(len(self.copyright), offsetToEndOfData))
ifdEnd.append(self.copyright)
offsetToEndOfData += len(self.copyright)
if self.dateTimeOriginal:
ifd.append(self.subIfdTag(offsetToEndOfData))
offsetToEndOfData += SUBIFD_LENGTH
ifdEnd.append(self.buildSubIfd(len(self.dateTimeOriginal), offsetToEndOfData))
ifdEnd.append(self.dateTimeOriginal)
app1 = self.buildApp1Section(ifd, ifdEnd)
self.addApp1(app1)
self.m.close()
#---------------------------------------------
# Build exif subifd with one tag for datetime (0x9003).
# Type is ascii (0x0002).
def buildSubIfd(self, lenDateTime, offsetToEndOfData):
return '\x01\x00\x03\x90\x02\x00%s%s\x00\x00\x00\x00' % (self.fourBytesHexIntel(lenDateTime), self.fourBytesHexIntel(offsetToEndOfData))
#---------------------------------------------
def getExistingExifInfo(self):
# Save off the old stuff.
try:
f = minimal_exif_reader.MinimalExifReader(self.filename)
except:
# Assume no existing exif info in the file. We
# don't care.
return
if not self.description:
self.description = f.imageDescription()
if not self.copyright:
self.copyright = f.copyright()
self.dateTimeOriginal = f.dateTimeOriginal()
if self.dateTimeOriginal:
# Restore ending nul.
if self.dateTimeOriginal[-1] != '\x00':
self.dateTimeOriginal += '\x00'
#---------------------------------------------------------------------------
def removeExifInfo(self, addApp0 = 1):
"""Remove the app1 section of the jpg. This removes all exif info and the exif
thumbnail. addApp0 should be 1 to add a minimal app0 section right after soi
to make it a legitimate jpg, I think (various image programs can read the file
without app0, but I think the standard requires one).
"""
# Read first bit of file to see if exif file.
self.m.seek(0)
if self.m.read(2) != self.SOI_MARKER:
self.m.close()
raise ExifFormatException("Missing SOI marker")
app0DataLength = 0
appMarker = self.m.read(2)
# See if there's an APP0 section, which sometimes appears.
if appMarker == self.APP0_MARKER:
if VERBOSE: print 'app0 found'
app0DataLength = ord(self.m.read(1)) * 256 + ord(self.m.read(1))
if VERBOSE: print 'app0DataLength: %s' % app0DataLength
# Back up 2 bytes to get the length bytes.
self.m.seek(-2, 1)
existingApp0 = self.m.read(app0DataLength)
appMarker = self.m.read(2)
if appMarker != self.APP1_MARKER:
# We don't care, we'll add our minimal app1 later.
return
exifHeader = self.m.read(8)
if VERBOSE: print 'exif header: %s' % binascii.hexlify(exifHeader)
if (exifHeader[2:6] != 'Exif' or
exifHeader[6:8] != '\x00\x00'):
self.m.close()
raise ExifFormatException("Malformed APP1")
app1Length = ord(exifHeader[0]) * 256 + ord(exifHeader[1])
if VERBOSE: print 'app1Length: %s' % app1Length
originalFileSize = self.m.size()
# Shift stuff just past app1 to overwrite app1.
# Start at app1 length bytes in + other bytes not incl in app1 length.
src = app1Length + len(self.SOI_MARKER) + len(self.APP1_MARKER)
if app0DataLength:
src += app0DataLength + len(self.APP0_MARKER)
dest = len(self.SOI_MARKER)
if addApp0:
if app0DataLength != 0:
# We'll re-add the existing app0.
dest += app0DataLength + len(self.APP0_MARKER)
else:
# Add our generic app0.
dest += len(self.APP0)
count = originalFileSize - app1Length - len(self.SOI_MARKER) - len(self.APP1_MARKER)
if app0DataLength:
count -= app0DataLength + len(self.APP0_MARKER)
if VERBOSE: print 'self.m.move(%s, %s, %s)' % (dest, src, count)
self.m.move(dest, src, count)
if addApp0:
if app0DataLength != 0:
self.m.resize(originalFileSize - app1Length - len(self.APP1_MARKER))
else:
self.m.seek(len(self.SOI_MARKER))
self.m.write(self.APP0)
self.m.resize(originalFileSize - app1Length - len(self.APP1_MARKER) + len(self.APP0))
else:
self.m.resize(originalFileSize - app1Length - len(self.APP1_MARKER))
#---------------------------------------------------------------------------
def removeApp0(self):
self.m.seek(0)
header = self.m.read(6)
if (header[0:2] != self.SOI_MARKER or
header[2:4] != self.APP0_MARKER):
if VERBOSE: print 'no app0 found: %s' % binascii.hexlify(header)
return
originalFileSize = self.m.size()
app0Length = ord(header[4]) * 256 + ord(header[5])
if VERBOSE: print 'app0Length:', app0Length
# Shift stuff to overwrite app0.
# Start at app0 length bytes in + other bytes not incl in app0 length.
src = app0Length + len(self.SOI_MARKER) + len(self.APP0_MARKER)
dest = len(self.SOI_MARKER)
count = originalFileSize - app0Length - len(self.SOI_MARKER) - len(self.APP0_MARKER)
self.m.move(dest, src, count)
if VERBOSE: print 'm.move(%s, %s, %s)' % (dest, src, count)
self.m.resize(originalFileSize - app0Length - len(self.APP0_MARKER))
#---------------------------------------------------------------------------
def addApp1(self, app1):
originalFileSize = self.m.size()
# Insert app1 section.
self.m.resize(originalFileSize + len(app1))
src = len(self.SOI_MARKER)
dest = len(app1) + len(self.SOI_MARKER)
count = originalFileSize - len(self.SOI_MARKER)
self.m.move(dest, src, count)
self.m.seek(len(self.SOI_MARKER))
self.m.write(app1)
#---------------------------------------------------------------------------
def fourBytesHexIntel(self, number):
return '%s%s%s%s' % (chr(number & 0x000000ff),
chr((number >> 8) & 0x000000ff),
chr((number >> 16) & 0x000000ff),
chr((number >> 24) & 0x000000ff))
#---------------------------------------------------------------------------
def twoBytesHexIntel(self, number):
return '%s%s' % (chr(number & 0x00ff),
chr((number >> 8) & 0x00ff))
#---------------------------------------------------------------------------
def descriptionTag(self, numChars, loc):
return self.asciiTag('\x0e\x01', numChars, loc)
#---------------------------------------------------------------------------
def copyrightTag(self, numChars, loc):
return self.asciiTag('\x98\x82', numChars, loc)
#---------------------------------------------------------------------------
def subIfdTag(self, loc):
return '\x69\x87\x04\x00\x01\x00\x00\x00%s' % self.fourBytesHexIntel(loc)
#---------------------------------------------------------------------------
def asciiTag(self, tag, numChars, loc):
"""Create ascii tag. Assumes description > 4 chars long."""
return '%s\x02\x00%s%s' % (tag, self.fourBytesHexIntel(numChars), self.fourBytesHexIntel(loc))
#---------------------------------------------------------------------------
def buildApp1Section(self, ifdPieces, ifdEndPieces):
"""Create the APP1 section of an exif jpg. Consists of exif header plus
tiff header + ifd and associated data."""
# Intel byte order, offset to first ifd will be 8.
tiff = 'II\x2a\x00\x08\x00\x00\x00%s%s' % (''.join(ifdPieces), ''.join(ifdEndPieces))
if DUMP_TIFF:
f = open('tiff.dump', 'wb')
f.write(tiff)
f.close()
app1Length = len(tiff) + 8
return '\xff\xe1%s%sExif\x00\x00%s' % (chr((app1Length >> 8) & 0x00ff), chr(app1Length & 0x00ff), tiff)
#---------------------------------------------------------------------------
def __str__(self):
return """filename: %(filename)s
removeExifSection: %(removeExifSection)s
description: %(description)s
copyright: %(copyright)s
dateTimeOriginal: %(dateTimeOriginal)s
""" % self.__dict__
#---------------------------------------------------------------------------
def usage(error = None):
"""Print command line usage and exit"""
if error:
print error
print
print """This program will remove exif info from an exif jpg, and can optionally
add the ImageDescription exif tag and/or the Copyright tag. But it will always remove
some or all existing exif info (depending on options--see below)!
So don't run this on your original images without a backup.
Options:
-h: shows this message.
-f <file>: jpg to process (required).
-x: remove exif info (including thumbnail).
-d <description or file>: remove exif info (including thumbnail) and then add exif
ImageDescription. Will save the existing copyright tag if present,
as well as the date time original tag (date & time photo taken),
unless -x also passed (-x always means remove all exif info).
It will attempt to open whatever is passed on the
command line as a file; if successful, the contents of the file
are added as the description, else the literal text on the
command line is used as the description.
-c <copyright or file>: remove exif info (including thumbnail) and then add exif
Copyright tag. Will save the existing image description tag if present,
as well as the date time original tag (date & time photo taken),
unless -x also passed (-x always means remove all exif info).
It will attempt to open whatever is passed on the command line as a file;
if successful, the contents of the file are added as the copyright,
else the literal text on the command line is used as the copyright.
-s: prepend copyright symbol to copyright.
-y: prepend copyright symbol and current year to copyright.
The image description and copyright must be > 4 characters long.
This software courtesy of Megabyte Rodeo Software."""
sys.exit(1)
#---------------------------------------------------------------------------
def parseArgs(args_):
import getopt
try:
opts, args = getopt.getopt(args_, "yshxd:f:c:")
except getopt.GetoptError:
usage()
filename = None
description = ''
copyright = ''
addCopyrightSymbol = 0
addCopyrightYear = 0
removeExif = 0
for o, a in opts:
if o == "-h":
usage()
if o == "-f":
filename = a
if o == "-d":
try:
f = open(a)
description = f.read()
f.close()
except:
description = a
if o == "-c":
try:
f = open(a)
copyright = f.read()
f.close()
except:
copyright = a
if o == '-x':
removeExif = 1
if o == '-s':
addCopyrightSymbol = 1
if o == '-y':
addCopyrightYear = 1
if filename is None:
usage('Missing jpg filename')
if description and (len(description) <= 4 or len(description) > 60000):
usage('Description too short or too long')
if copyright and (len(copyright) <= 4 or len(copyright) > 60000):
usage('Copyright too short or too long')
if not description and not copyright and not removeExif:
usage('Nothing to do!')
return filename, description, copyright, removeExif, addCopyrightSymbol, addCopyrightYear
#---------------------------------------------------------------------------
if __name__ == '__main__':
try:
filename, description, copyright, removeExif, addCopyrightSymbol, addCopyrightYear = parseArgs(sys.argv[1:])
f = MinimalExifWriter(filename)
if description:
f.newImageDescription(description)
if copyright:
f.newCopyright(copyright, addCopyrightSymbol, addCopyrightYear)
if removeExif:
f.removeExif()
f.process()
except ExifFormatException, ex:
sys.stderr.write("Exif format error: %s\n" % ex)
except SystemExit:
pass
except:
sys.stderr.write("Unable to process %s\n" % filename)
raise

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB