eggification
[Photo.git] / Products / Photo / xmp_jpeg.py
1 # -*- coding: utf-8 -*-
2 #######################################################################################
3 # Photo is a part of Plinn - http://plinn.org #
4 # Copyright (C) 2008 BenoƮt PIN <benoit.pin@ensmp.fr> #
5 # #
6 # This program is free software; you can redistribute it and/or #
7 # modify it under the terms of the GNU General Public License #
8 # as published by the Free Software Foundation; either version 2 #
9 # of the License, or (at your option) any later version. #
10 # #
11 # This program is distributed in the hope that it will be useful, #
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of #
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
14 # GNU General Public License for more details. #
15 # #
16 # You should have received a copy of the GNU General Public License #
17 # along with this program; if not, write to the Free Software #
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. #
19 #######################################################################################
20 """ Jpeg plugin for xmp read/write support.
21
22
23 """
24
25 from xmp import XMP
26 from types import StringType
27
28 class JpegXmpIO(object):
29
30 JPEG_XMP_LEADIN = 'http://ns.adobe.com/xap/1.0/\x00'
31 JPEG_XMP_LEADIN_LENGTH = len(JPEG_XMP_LEADIN)
32
33 MARKERS = {
34 0xFFC0: ("SOF0", "Baseline DCT", True),
35 0xFFC1: ("SOF1", "Extended Sequential DCT", True),
36 0xFFC2: ("SOF2", "Progressive DCT", True),
37 0xFFC3: ("SOF3", "Spatial lossless", True),
38 0xFFC4: ("DHT", "Define Huffman table", True),
39 0xFFC5: ("SOF5", "Differential sequential DCT", True),
40 0xFFC6: ("SOF6", "Differential progressive DCT", True),
41 0xFFC7: ("SOF7", "Differential spatial", True),
42 0xFFC8: ("JPG", "Extension", False),
43 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", True),
44 0xFFCA: ("SOF10", "Progressive DCT (AC)", True),
45 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", True),
46 0xFFCC: ("DAC", "Define arithmetic coding conditioning", True),
47 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", True),
48 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", True),
49 0xFFCF: ("SOF15", "Differential spatial (AC)", True),
50 0xFFD0: ("RST0", "Restart 0", False),
51 0xFFD1: ("RST1", "Restart 1", False),
52 0xFFD2: ("RST2", "Restart 2", False),
53 0xFFD3: ("RST3", "Restart 3", False),
54 0xFFD4: ("RST4", "Restart 4", False),
55 0xFFD5: ("RST5", "Restart 5", False),
56 0xFFD6: ("RST6", "Restart 6", False),
57 0xFFD7: ("RST7", "Restart 7", False),
58 0xFFD8: ("SOI", "Start of image", False),
59 0xFFD9: ("EOI", "End of image", False),
60 0xFFDA: ("SOS", "Start of scan", True),
61 0xFFDB: ("DQT", "Define quantization table", True),
62 0xFFDC: ("DNL", "Define number of lines", True),
63 0xFFDD: ("DRI", "Define restart interval", True),
64 0xFFDE: ("DHP", "Define hierarchical progression", True),
65 0xFFDF: ("EXP", "Expand reference component", True),
66 0xFFE0: ("APP0", "Application segment 0", True),
67 0xFFE1: ("APP1", "Application segment 1", True),
68 0xFFE2: ("APP2", "Application segment 2", True),
69 0xFFE3: ("APP3", "Application segment 3", True),
70 0xFFE4: ("APP4", "Application segment 4", True),
71 0xFFE5: ("APP5", "Application segment 5", True),
72 0xFFE6: ("APP6", "Application segment 6", True),
73 0xFFE7: ("APP7", "Application segment 7", True),
74 0xFFE8: ("APP8", "Application segment 8", True),
75 0xFFE9: ("APP9", "Application segment 9", True),
76 0xFFEA: ("APP10", "Application segment 10", True),
77 0xFFEB: ("APP11", "Application segment 11", True),
78 0xFFEC: ("APP12", "Application segment 12", True),
79 0xFFED: ("APP13", "Application segment 13", True),
80 0xFFEE: ("APP14", "Application segment 14", True),
81 0xFFEF: ("APP15", "Application segment 15", True),
82 0xFFF0: ("JPG0", "Extension 0", False),
83 0xFFF1: ("JPG1", "Extension 1", False),
84 0xFFF2: ("JPG2", "Extension 2", False),
85 0xFFF3: ("JPG3", "Extension 3", False),
86 0xFFF4: ("JPG4", "Extension 4", False),
87 0xFFF5: ("JPG5", "Extension 5", False),
88 0xFFF6: ("JPG6", "Extension 6", False),
89 0xFFF7: ("JPG7", "Extension 7", False),
90 0xFFF8: ("JPG8", "Extension 8", False),
91 0xFFF9: ("JPG9", "Extension 9", False),
92 0xFFFA: ("JPG10", "Extension 10", False),
93 0xFFFB: ("JPG11", "Extension 11", False),
94 0xFFFC: ("JPG12", "Extension 12", False),
95 0xFFFD: ("JPG13", "Extension 13", False),
96 0xFFFE: ("COM", "Comment", True)
97 }
98
99
100 @staticmethod
101 def i16(c,o=0):
102 return ord(c[o+1]) + (ord(c[o])<<8)
103
104 @staticmethod
105 def getBlockInfo(marker, f):
106 start = f.tell()
107 length = JpegXmpIO.i16(f.read(2))
108
109 markerInfo = JpegXmpIO.MARKERS[marker]
110 blockInfo = { 'name' : markerInfo[0]
111 , 'description' : markerInfo[1]
112 , 'start' : start
113 , 'length' : length}
114
115 jump = start + length
116 f.seek(jump)
117
118 return blockInfo
119
120 @staticmethod
121 def getBlockInfos(f) :
122 f.seek(0)
123 s = f.read(1)
124
125 blockInfos = []
126
127 while 1:
128 s = s + f.read(1)
129 i = JpegXmpIO.i16(s)
130
131 if JpegXmpIO.MARKERS.has_key(i):
132 name, desciption, handle = JpegXmpIO.MARKERS[i]
133
134 if handle:
135 blockInfo = JpegXmpIO.getBlockInfo(i, f)
136 blockInfos.append(blockInfo)
137 if i == 0xFFDA: # start of scan
138 break
139 s = f.read(1)
140 elif i == 0 or i == 65535:
141 # padded marker or junk; move on
142 s = "\xff"
143
144 return blockInfos
145
146
147 @staticmethod
148 def genJpegXmpBlock(uXmpData, paddingSize=2) :
149 block = u''
150
151 block += JpegXmpIO.JPEG_XMP_LEADIN
152 block += XMP.genXMPPacket(uXmpData, paddingSize)
153 # utf-8 mandatory in jpeg files (xmp specification)
154 block = block.encode('utf-8')
155
156 length = len(block) + 2
157
158 # TODO : reduce padding size if this assertion occurs
159 assert length <= 0xfffd, "Jpeg block too long: %d (max: 0xfffd)" % hex(length)
160
161 chrlength = chr(length >> 8 & 0xff) + chr(length & 0xff)
162
163 block = chrlength + block
164
165 return block
166
167
168
169 @staticmethod
170 def read(f) :
171
172 blockInfos = JpegXmpIO.getBlockInfos(f)
173 app1BlockInfos = [b for b in blockInfos if b['name'] == 'APP1']
174
175 xmpBlocks = []
176
177 for info in app1BlockInfos :
178 f.seek(info['start'])
179 data = f.read(info['length'])[2:]
180 if data.startswith(JpegXmpIO.JPEG_XMP_LEADIN) :
181 xmpBlocks.append(data)
182
183 assert len(xmpBlocks) <= 1, "Multiple xmp block data is not yet supported."
184
185 if len(xmpBlocks) == 1 :
186 data = xmpBlocks[0]
187 packet = data[len(JpegXmpIO.JPEG_XMP_LEADIN):]
188 return packet
189 else :
190 return None
191
192 @staticmethod
193 def write(original, new, uxmp) :
194
195 blockInfos = JpegXmpIO.getBlockInfos(original)
196 app1BlockInfos = [b for b in blockInfos if b['name'] == 'APP1']
197
198 xmpBlockInfos = []
199
200 for info in app1BlockInfos :
201 original.seek(info['start'])
202 lead = original.read(JpegXmpIO.JPEG_XMP_LEADIN_LENGTH+2)[2:]
203 if lead == JpegXmpIO.JPEG_XMP_LEADIN :
204 xmpBlockInfos.append(info)
205
206
207 assert len(xmpBlockInfos) <= 1, "Multiple xmp block data is not yet supported."
208
209 if isinstance(uxmp, StringType) :
210 uxmp = unicode(uxmp, 'utf-8')
211
212 if len(xmpBlockInfos) == 0 :
213 blockInfo = [b for b in blockInfos if b['name'] == 'APP13']
214
215 if not blockInfo :
216 blockInfo = [b for b in blockInfos if b['name'] == 'APP1']
217
218 if not blockInfo :
219 blockInfo = [b for b in blockInfos if b['name'] == 'APP0']
220
221 if not blockInfo : raise ValueError, "No suitable place to write xmp segment"
222
223 info = blockInfo[0]
224 print 'create xmp after: %s' % info['name']
225
226 original.seek(0)
227 before = original.read(info['start'] + info['length'])
228 after = original.read()
229
230 jpegBlock = '\xFF\xE1' + JpegXmpIO.genJpegXmpBlock(uxmp)
231
232 else :
233 info = xmpBlockInfos[0]
234
235 original.seek(0)
236 before = original.read(info['start'])
237
238 original.seek(info['start'] + info['length'])
239 after = original.read()
240
241 jpegBlock = JpegXmpIO.genJpegXmpBlock(uxmp)
242
243 new.seek(0)
244 new.write(before)
245 new.write(jpegBlock)
246 new.write(after)
247
248 # if original == new :
249 # new.seek(0)
250 # else :
251 # new.close()
252 # original.close()
253
254
255 XMP.registerReader('image/jpeg', JpegXmpIO.read)
256 XMP.registerWriter('image/jpeg', JpegXmpIO.write)