+# -*- coding: utf-8 -*-
+#######################################################################################
+# Photo is a part of Plinn - http://plinn.org #
+# Copyright (C) 2008 BenoƮt PIN <benoit.pin@ensmp.fr> #
+# #
+# 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 2 #
+# 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, write to the Free Software #
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
+#######################################################################################
+""" Jpeg plugin for xmp read/write support.
+
+
+"""
+
+from xmp import XMP
+from types import StringType
+
+class JpegXmpIO(object):
+
+ JPEG_XMP_LEADIN = 'http://ns.adobe.com/xap/1.0/\x00'
+ JPEG_XMP_LEADIN_LENGTH = len(JPEG_XMP_LEADIN)
+
+ MARKERS = {
+ 0xFFC0: ("SOF0", "Baseline DCT", True),
+ 0xFFC1: ("SOF1", "Extended Sequential DCT", True),
+ 0xFFC2: ("SOF2", "Progressive DCT", True),
+ 0xFFC3: ("SOF3", "Spatial lossless", True),
+ 0xFFC4: ("DHT", "Define Huffman table", True),
+ 0xFFC5: ("SOF5", "Differential sequential DCT", True),
+ 0xFFC6: ("SOF6", "Differential progressive DCT", True),
+ 0xFFC7: ("SOF7", "Differential spatial", True),
+ 0xFFC8: ("JPG", "Extension", False),
+ 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", True),
+ 0xFFCA: ("SOF10", "Progressive DCT (AC)", True),
+ 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", True),
+ 0xFFCC: ("DAC", "Define arithmetic coding conditioning", True),
+ 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", True),
+ 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", True),
+ 0xFFCF: ("SOF15", "Differential spatial (AC)", True),
+ 0xFFD0: ("RST0", "Restart 0", False),
+ 0xFFD1: ("RST1", "Restart 1", False),
+ 0xFFD2: ("RST2", "Restart 2", False),
+ 0xFFD3: ("RST3", "Restart 3", False),
+ 0xFFD4: ("RST4", "Restart 4", False),
+ 0xFFD5: ("RST5", "Restart 5", False),
+ 0xFFD6: ("RST6", "Restart 6", False),
+ 0xFFD7: ("RST7", "Restart 7", False),
+ 0xFFD8: ("SOI", "Start of image", False),
+ 0xFFD9: ("EOI", "End of image", False),
+ 0xFFDA: ("SOS", "Start of scan", True),
+ 0xFFDB: ("DQT", "Define quantization table", True),
+ 0xFFDC: ("DNL", "Define number of lines", True),
+ 0xFFDD: ("DRI", "Define restart interval", True),
+ 0xFFDE: ("DHP", "Define hierarchical progression", True),
+ 0xFFDF: ("EXP", "Expand reference component", True),
+ 0xFFE0: ("APP0", "Application segment 0", True),
+ 0xFFE1: ("APP1", "Application segment 1", True),
+ 0xFFE2: ("APP2", "Application segment 2", True),
+ 0xFFE3: ("APP3", "Application segment 3", True),
+ 0xFFE4: ("APP4", "Application segment 4", True),
+ 0xFFE5: ("APP5", "Application segment 5", True),
+ 0xFFE6: ("APP6", "Application segment 6", True),
+ 0xFFE7: ("APP7", "Application segment 7", True),
+ 0xFFE8: ("APP8", "Application segment 8", True),
+ 0xFFE9: ("APP9", "Application segment 9", True),
+ 0xFFEA: ("APP10", "Application segment 10", True),
+ 0xFFEB: ("APP11", "Application segment 11", True),
+ 0xFFEC: ("APP12", "Application segment 12", True),
+ 0xFFED: ("APP13", "Application segment 13", True),
+ 0xFFEE: ("APP14", "Application segment 14", True),
+ 0xFFEF: ("APP15", "Application segment 15", True),
+ 0xFFF0: ("JPG0", "Extension 0", False),
+ 0xFFF1: ("JPG1", "Extension 1", False),
+ 0xFFF2: ("JPG2", "Extension 2", False),
+ 0xFFF3: ("JPG3", "Extension 3", False),
+ 0xFFF4: ("JPG4", "Extension 4", False),
+ 0xFFF5: ("JPG5", "Extension 5", False),
+ 0xFFF6: ("JPG6", "Extension 6", False),
+ 0xFFF7: ("JPG7", "Extension 7", False),
+ 0xFFF8: ("JPG8", "Extension 8", False),
+ 0xFFF9: ("JPG9", "Extension 9", False),
+ 0xFFFA: ("JPG10", "Extension 10", False),
+ 0xFFFB: ("JPG11", "Extension 11", False),
+ 0xFFFC: ("JPG12", "Extension 12", False),
+ 0xFFFD: ("JPG13", "Extension 13", False),
+ 0xFFFE: ("COM", "Comment", True)
+ }
+
+
+ @staticmethod
+ def i16(c,o=0):
+ return ord(c[o+1]) + (ord(c[o])<<8)
+
+ @staticmethod
+ def getBlockInfo(marker, f):
+ start = f.tell()
+ length = JpegXmpIO.i16(f.read(2))
+
+ markerInfo = JpegXmpIO.MARKERS[marker]
+ blockInfo = { 'name' : markerInfo[0]
+ , 'description' : markerInfo[1]
+ , 'start' : start
+ , 'length' : length}
+
+ jump = start + length
+ f.seek(jump)
+
+ return blockInfo
+
+ @staticmethod
+ def getBlockInfos(f) :
+ f.seek(0)
+ s = f.read(1)
+
+ blockInfos = []
+
+ while 1:
+ s = s + f.read(1)
+ i = JpegXmpIO.i16(s)
+
+ if JpegXmpIO.MARKERS.has_key(i):
+ name, desciption, handle = JpegXmpIO.MARKERS[i]
+
+ if handle:
+ blockInfo = JpegXmpIO.getBlockInfo(i, f)
+ blockInfos.append(blockInfo)
+ if i == 0xFFDA: # start of scan
+ break
+ s = f.read(1)
+ elif i == 0 or i == 65535:
+ # padded marker or junk; move on
+ s = "\xff"
+
+ return blockInfos
+
+
+ @staticmethod
+ def genJpegXmpBlock(uXmpData, paddingSize=2) :
+ block = u''
+
+ block += JpegXmpIO.JPEG_XMP_LEADIN
+ block += XMP.genXMPPacket(uXmpData, paddingSize)
+ # utf-8 mandatory in jpeg files (xmp specification)
+ block = block.encode('utf-8')
+
+ length = len(block) + 2
+
+ # TODO : reduce padding size if this assertion occurs
+ assert length <= 0xfffd, "Jpeg block too long: %d (max: 0xfffd)" % hex(length)
+
+ chrlength = chr(length >> 8 & 0xff) + chr(length & 0xff)
+
+ block = chrlength + block
+
+ return block
+
+
+
+ @staticmethod
+ def read(f) :
+
+ blockInfos = JpegXmpIO.getBlockInfos(f)
+ app1BlockInfos = [b for b in blockInfos if b['name'] == 'APP1']
+
+ xmpBlocks = []
+
+ for info in app1BlockInfos :
+ f.seek(info['start'])
+ data = f.read(info['length'])[2:]
+ if data.startswith(JpegXmpIO.JPEG_XMP_LEADIN) :
+ xmpBlocks.append(data)
+
+ assert len(xmpBlocks) <= 1, "Multiple xmp block data is not yet supported."
+
+ if len(xmpBlocks) == 1 :
+ data = xmpBlocks[0]
+ packet = data[len(JpegXmpIO.JPEG_XMP_LEADIN):]
+ return packet
+ else :
+ return None
+
+ @staticmethod
+ def write(original, new, uxmp) :
+
+ blockInfos = JpegXmpIO.getBlockInfos(original)
+ app1BlockInfos = [b for b in blockInfos if b['name'] == 'APP1']
+
+ xmpBlockInfos = []
+
+ for info in app1BlockInfos :
+ original.seek(info['start'])
+ lead = original.read(JpegXmpIO.JPEG_XMP_LEADIN_LENGTH+2)[2:]
+ if lead == JpegXmpIO.JPEG_XMP_LEADIN :
+ xmpBlockInfos.append(info)
+
+
+ assert len(xmpBlockInfos) <= 1, "Multiple xmp block data is not yet supported."
+
+ if isinstance(uxmp, StringType) :
+ uxmp = unicode(uxmp, 'utf-8')
+
+ if len(xmpBlockInfos) == 0 :
+ blockInfo = [b for b in blockInfos if b['name'] == 'APP13']
+
+ if not blockInfo :
+ blockInfo = [b for b in blockInfos if b['name'] == 'APP1']
+
+ if not blockInfo :
+ blockInfo = [b for b in blockInfos if b['name'] == 'APP0']
+
+ if not blockInfo : raise ValueError, "No suitable place to write xmp segment"
+
+ info = blockInfo[0]
+ print 'create xmp after: %s' % info['name']
+
+ original.seek(0)
+ before = original.read(info['start'] + info['length'])
+ after = original.read()
+
+ jpegBlock = '\xFF\xE1' + JpegXmpIO.genJpegXmpBlock(uxmp)
+
+ else :
+ info = xmpBlockInfos[0]
+
+ original.seek(0)
+ before = original.read(info['start'])
+
+ original.seek(info['start'] + info['length'])
+ after = original.read()
+
+ jpegBlock = JpegXmpIO.genJpegXmpBlock(uxmp)
+
+ new.seek(0)
+ new.write(before)
+ new.write(jpegBlock)
+ new.write(after)
+
+ # if original == new :
+ # new.seek(0)
+ # else :
+ # new.close()
+ # original.close()
+
+
+XMP.registerReader('image/jpeg', JpegXmpIO.read)
+XMP.registerWriter('image/jpeg', JpegXmpIO.write)