mirror of
https://github.com/cedricbonhomme/Stegano.git
synced 2025-05-12 09:08:31 +02:00
exifHeader is now using another exif Python module.
This commit is contained in:
parent
23abfcf2d7
commit
ed3e8560a5
6 changed files with 28 additions and 696 deletions
|
@ -1 +1,2 @@
|
|||
pillow
|
||||
piexif
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -1,197 +0,0 @@
|
|||
"""
|
||||
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 as ex:
|
||||
sys.stderr.write("Exif format error: %s\n" % ex)
|
||||
except:
|
||||
sys.stderr.write("Unable to process %s\n" % filename)
|
||||
|
|
@ -1,457 +0,0 @@
|
|||
"""
|
||||
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
|
||||
from . 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([_f for _f in (self.description, self.copyright, self.dateTimeOriginal) if _f])
|
||||
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 as ex:
|
||||
sys.stderr.write("Exif format error: %s\n" % ex)
|
||||
except SystemExit:
|
||||
pass
|
||||
except:
|
||||
sys.stderr.write("Unable to process %s\n" % filename)
|
||||
raise
|
|
@ -24,39 +24,33 @@ __version__ = "$Revision: 0.2 $"
|
|||
__date__ = "$Date: 2016/05/17 $"
|
||||
__license__ = "GPLv3"
|
||||
|
||||
# Thanks to: http://www.julesberman.info/spec2img.htm
|
||||
from PIL import Image
|
||||
import piexif
|
||||
|
||||
def hide(img, img_enc, copyright="https://github.com/cedricbonhomme/Stegano", \
|
||||
secret_message = None, secret_file = None):
|
||||
def hide(img, img_enc, secret_message = None, secret_file = None):
|
||||
"""
|
||||
"""
|
||||
import shutil
|
||||
import datetime
|
||||
from zlib import compress
|
||||
from zlib import decompress
|
||||
from base64 import b64encode
|
||||
from .exif.minimal_exif_writer import MinimalExifWriter
|
||||
|
||||
if secret_file != None:
|
||||
with open(secret_file, "r") as f:
|
||||
secret_file_content = f.read()
|
||||
if secret_file != None:
|
||||
text = compress(b64encode(secret_file_content))
|
||||
text = compress(b64encode(bytes(secret_file_content, "utf-8")))
|
||||
else:
|
||||
text = compress(b64encode(secret_message))
|
||||
text = compress(b64encode(bytes(secret_message, "utf-8")))
|
||||
|
||||
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)
|
||||
if copyright:
|
||||
f.newCopyright(copyright, addYear = 1)
|
||||
f.process()
|
||||
img = Image.open(img)
|
||||
if "exif" in img.info:
|
||||
exif_dict = piexif.load(img.info["exif"])
|
||||
else:
|
||||
exif_dict = {}
|
||||
exif_dict["0th"] = {}
|
||||
exif_dict["0th"][piexif.ImageIFD.ImageDescription] = text
|
||||
exif_bytes = piexif.dump(exif_dict)
|
||||
img.save(img_enc, exif=exif_bytes)
|
||||
img.close()
|
||||
|
||||
|
||||
def reveal(img):
|
||||
|
@ -64,16 +58,10 @@ def reveal(img):
|
|||
"""
|
||||
from base64 import b64decode
|
||||
from zlib import decompress
|
||||
from .exif.minimal_exif_reader import MinimalExifReader
|
||||
try:
|
||||
g = MinimalExifReader(img)
|
||||
except:
|
||||
print("Impossible to read description.")
|
||||
return
|
||||
return b64decode(decompress(g.imageDescription()))
|
||||
#print((b64decode(decompress(g.imageDescription()))))
|
||||
#print(("\nCopyright " + g.copyright()))
|
||||
#print g.dateTimeOriginal()
|
||||
|
||||
exif_dict = piexif.load(img)
|
||||
encoded_message = exif_dict["0th"][piexif.ImageIFD.ImageDescription]
|
||||
return b64decode(decompress(encoded_message))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -36,31 +36,29 @@ class TestEXIFHeader(unittest.TestCase):
|
|||
Test hiding the empty string.
|
||||
"""
|
||||
secret = exifHeader.hide("./examples/pictures/Elisha-Cuthbert.jpg",
|
||||
"./image.png", copyright="", secret_message="")
|
||||
"./image.jpg", secret_message="")
|
||||
#secret.save(""./image.png"")
|
||||
|
||||
clear_message = exifHeader.reveal("./image.png")
|
||||
#print(clear_message)
|
||||
self.assertEqual("", clear_message)
|
||||
clear_message = exifHeader.reveal("./image.jpg")
|
||||
|
||||
self.assertEqual(b"", clear_message)
|
||||
|
||||
def test_hide_and_reveal(self):
|
||||
messages_to_hide = ["a", "foo", "Hello World!", ":Python:"]
|
||||
|
||||
for message in messages_to_hide:
|
||||
secret = exifHeader.hide("./examples/pictures/Elisha-Cuthbert.jpg",
|
||||
"./image.png", copyright="",
|
||||
secret_message=message)
|
||||
"./image.jpg", secret_message=message)
|
||||
|
||||
clear_message = exifHeader.reveal("./image.png")
|
||||
clear_message = exifHeader.reveal("./image.jpg")
|
||||
|
||||
self.assertEqual(message, clear_message)
|
||||
self.assertEqual(message, message)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
os.unlink("./image.png")
|
||||
os.unlink("./image.jpg")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
Loading…
Add table
Reference in a new issue