mirror of
https://github.com/cedricbonhomme/Stegano.git
synced 2025-05-12 17:18:30 +02:00
Added new steganalysis method: exif header for jpeg file. Added new demon image (Elisha Cuthbert).
This commit is contained in:
parent
0310d634bd
commit
fc03000cb1
4 changed files with 727 additions and 0 deletions
73
exif-header.py
Normal file
73
exif-header.py
Normal 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
197
exif/minimal_exif_reader.py
Normal 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
457
exif/minimal_exif_writer.py
Normal 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
|
BIN
pictures/Elisha-Cuthbert.jpg
Normal file
BIN
pictures/Elisha-Cuthbert.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 373 KiB |
Loading…
Add table
Reference in a new issue