eggification
[Photo.git] / Products / Photo / xmp_jpeg.py
diff --git a/Products/Photo/xmp_jpeg.py b/Products/Photo/xmp_jpeg.py
new file mode 100755 (executable)
index 0000000..e193114
--- /dev/null
@@ -0,0 +1,256 @@
+# -*- 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)