diff --git a/exif-header.py b/exif-header.py new file mode 100644 index 0000000..5e0a2fa --- /dev/null +++ b/exif-header.py @@ -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 + +__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') \ No newline at end of file diff --git a/exif/minimal_exif_reader.py b/exif/minimal_exif_reader.py new file mode 100644 index 0000000..5f5794a --- /dev/null +++ b/exif/minimal_exif_reader.py @@ -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) + diff --git a/exif/minimal_exif_writer.py b/exif/minimal_exif_writer.py new file mode 100644 index 0000000..11c8be3 --- /dev/null +++ b/exif/minimal_exif_writer.py @@ -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 : jpg to process (required). + -x: remove exif info (including thumbnail). + -d : 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 : 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 diff --git a/pictures/Elisha-Cuthbert.jpg b/pictures/Elisha-Cuthbert.jpg new file mode 100644 index 0000000..ffaeb90 Binary files /dev/null and b/pictures/Elisha-Cuthbert.jpg differ