2003年1月9日追記
その後デジタルカメラを新調したら、これまたうまく動かなくなっちゃったんですよ。
実は今度買ったカメラ、Exif2.2対応だったんです。上のjpg_sz2.pyはExif2.1対応でした。
ではでは、ということでExif2.2の仕様書が手に入ったので、修正しちゃいました。
jpg_sz3.pyをどうぞ
2003年1月28日追記
サイズを読みとれないJPEGファイルを発見したので、調べてみるとJFIFフォーマットの場合にマーカFFDBのところの量子化テーブルが1つと決めうちしてしまっていたので、これを修正しました。
また、setFileメソッドで新しいファイルを指定した場合に、前回の結果が存在したら削除するようにしました。
わかりずらくなってきたので、バージョン番号付けました。今回のを 1.11 とさせていただきます。
jpg_sz_1_11.py
2003年2月10日追記
やはりまだサイズを読めないJPEGファイルがあったので、さらにいろいろ調べました。
そうしたらJFIFフォーマットにおいてSOFマーカが0以外のものもあるということを知り、それに対応しました。
恐らくこれでかなり対応できたのではないかと思います。
バージョン1.12とさせていただきます。
jpg_sz_1_12.py
1: VERSION = '1.12'
2: #
3: # JPEGファイルのヘッダ(?)部分を読み込み、画像のサイズを調べる
4: #
5: # JPEGファイルのフォーマットは以下のURLを参照のうえ、実際のフ
6: # ァイル数点をサンプルとした
7: # http://siisise.net/jpeg.html
8: # http://www.ba.wakwak.com/~tsuruzoh/Computer/Digicams/exif.html
9: # http://www5d.biglobe.ne.jp/~rugeye/html/appendix_jpeg.html
10: # (http://www5d.biglobe.ne.jp/~rugeye/html/dat/ja0.03e-5.pl.txt)
11: #
12: # Exif2.2の仕様書は以下より入手
13: # http://tsc.jeita.or.jp/WTO-01.htm
14: #
15: # 2003.2.10 JFIFフォーマットのSOF0以外のSOFを読むようにした
16: # 2003.1.28 setFile呼び出しで前回の結果が存在した場合に初期化するようにした
17: # JFIFフォーマットでFFDBのところに複数のテーブルがあった場合に正しく読めないのを修正した
18: # 2003.1.9 Exif2.2ファイルに対応
19: # pow演算に長整数を使うようにした
20: # 2002.9.12 Exif2.1ファイルに対応
21:
22: import os
23: from string import lower
24:
25: SOI = 0xFFD8
26: APP1 = 0xFFE1
27: EXIF_HDR = 'Exif\0\0'
28: TIFF_TAG_MARK = 0x002A
29:
30: type2bytes = { # TIFFタグのデータ形式
31: 1: 1, # unsigned byte
32: 2: 1, # ascii string
33: 3: 2, # unsigned short
34: 4: 4, # unsigned long
35: 5: 8, # unsigned rational (分数:unsigned longが2つ、分子・分母の順)
36: 6: 1, # signed byte
37: 7: 1, # undefined
38: 8: 2, # signed short
39: 9: 4, # signed long
40: 10: 8, # signed ratuonal (分数:signed longが2つ、分子・分母の順)
41: 11: 4, # single float
42: 12: 8 # double float
43: }
44:
45: IFD_TAG = {
46: # IFD0タグ
47: 0x010e: 'ImageDescription',
48: 0x010f: 'Make',
49: 0x0110: 'Model',
50: 0x0112: 'Orientation',
51: 0x011a: 'XResolution',
52: 0x011b: 'YResolution',
53: 0x0128: 'ResolutionUnit',
54: 0x0131: 'Software',
55: 0x0132: 'DateTime',
56: 0x013e: 'WhitePoint',
57: 0x013f: 'PrimaryChromaticities',
58: 0x0211: 'YCbCrCoefficients',
59: 0x0213: 'YCbCrPositioning',
60: 0x0214: 'ReferenceBlackWhite',
61: 0x8298: 'Copyright',
62: 0x8769: 'ExifIFDPointer',
63:
64: # SubIFDタグ
65: 0x829a: 'ExposureTime',
66: 0x829d: 'FNumber',
67: 0x8822: 'ExposureProgram',
68: 0x8827: 'ISOSpeedRatings',
69: 0x9000: 'ExifVersion',
70: 0x9003: 'DateTimeOriginal',
71: 0x9004: 'DateTimeDigitized',
72: 0x9101: 'ComponentsConfiguration',
73: 0x9102: 'CompressedBitsPerPixel',
74: 0x9201: 'ShutterSpeedValue',
75: 0x9202: 'ApertureValue',
76: 0x9203: 'BrightnessValue',
77: 0x9204: 'ExposureBiasValue',
78: 0x9205: 'MaxApertureValue',
79: 0x9206: 'SubjectDistance',
80: 0x9207: 'MeteringMode',
81: 0x9208: 'LightSource',
82: 0x9209: 'Flash',
83: 0x920a: 'FocalLength',
84: 0x927c: 'MakerNote',
85: 0x9286: 'UserComment',
86: 0x9290: 'SubSecTime',
87: 0x9291: 'SubSecTimeOriginal',
88: 0x9292: 'SubSecTimeDigitized',
89: 0xa000: 'FlashPixVersion',
90: 0xa001: 'ColorSpace',
91: 0xa002: 'PixelXDimension',
92: 0xa003: 'PixelYDimension',
93: 0xa004: 'RelatedSoundFile',
94: 0xa005: 'InteroperabilityIFDPointer',
95: 0xa20e: 'FocalPlaneXResolution',
96: 0xa20f: 'FocalPlaneYResolution',
97: 0xa210: 'FocalPlaneResolutionUnit',
98: 0xa215: 'ExposureIndex',
99: 0xa217: 'SensingMethod',
100: 0xa300: 'FileSource',
101: 0xa301: 'SceneType',
102: 0xa302: 'CFAPattern',
103:
104: # InteroperabilityIFDタグ
105: 0x0001: 'InteroperabilityIndex',
106: 0x0002: 'InteroperabilityVersion',
107: 0x1000: 'RelatedImageFileFormat',
108: 0x1001: 'RelatedImageWidth',
109: 0x1002: 'RelatedImageLength',
110:
111: # IFD1タグ
112: 0x0100: 'ImageWidth',
113: 0x0101: 'ImageLength',
114: 0x0102: 'BitsPerSample',
115: 0x0103: 'Compression',
116: 0x0106: 'PhotometricInterpretation',
117: 0x0111: 'StripOffsets',
118: 0x0112: 'Orientation',
119: 0x0115: 'SamplesPerPixel',
120: 0x0116: 'RowsPerStrip',
121: 0x0117: 'StripByteConunts',
122: 0x011a: 'XResolution',
123: 0x011b: 'YResolution',
124: 0x011c: 'PlanarConfiguration',
125: 0x0128: 'ResolutionUnit',
126: 0x0201: 'JpegInterchangeFormat',
127: 0x0202: 'JpegInterchangeFormatLength',
128: 0x0211: 'YCbCrCoefficients',
129: 0x0212: 'YCbCrSubSampling',
130: 0x0213: 'YCbCrPositioning',
131: 0x0214: 'ReferenceBlackWhite',
132: 0x00fe: 'NewSubfileType',
133:
134: # その他のタグ
135: 0x00ff: 'SubfileType',
136: 0x012d: 'TransferFunction',
137: 0x013b: 'Artist',
138: 0x013d: 'Predictor',
139: 0x013e: 'WhitePoint',
140: 0x013f: 'PrimaryChromaticities',
141: 0x0142: 'TileWidth',
142: 0x0143: 'TileLength',
143: 0x0144: 'TileOffsets',
144: 0x0145: 'TileByteCounts',
145: 0x014a: 'SubIFDs',
146: 0x015b: 'JPEGTables',
147: 0x828d: 'CFARepeatPatternDim',
148: 0x828e: 'CFAPattern',
149: 0x828f: 'BatteryLevel',
150: 0x83bb: 'IPTC/NAA',
151: 0x8773: 'InterColorProfile',
152: 0x8824: 'SpectralSensitivity',
153: 0x8825: 'GPSInfo',
154: 0x8828: 'OECF',
155: 0x8829: 'Interlace',
156: 0x882a: 'TimeZoneOffset',
157: 0x882b: 'SelfTimerMode',
158: 0x920b: 'FlashEnergy',
159: 0x920c: 'SpatialFrequencyResponse',
160: 0x920d: 'Noise',
161: 0x9211: 'ImageNumber',
162: 0x9212: 'SecurityClassification',
163: 0x9213: 'ImageHistory',
164: 0x9214: 'SubjectArea',
165: 0x9215: 'ExposureIndex',
166: 0x9216: 'TIFF/EPStandardID',
167: 0x9290: 'SubSecTime',
168: 0x9291: 'SubSecTimeOriginal',
169: 0x9292: 'SubSecTimeDigitized',
170: 0xa20b: 'FlashEnergy',
171: 0xa20c: 'SpatialFrequencyResponse',
172: 0xa214: 'SubjectLocation',
173:
174: # Exif2.2対応で追加
175: 0xa420: 'ImageUniqueID',
176: 0xa401: 'CustomRendered',
177: 0xa402: 'ExposureMode',
178: 0xa403: 'WhiteBalance',
179: 0xa404: 'DigitalZoomRatio',
180: 0xa405: 'FocalLengthIn35mmFilm',
181: 0xa406: 'SceneCaptureType',
182: 0xa407: 'GainControl',
183: 0xa408: 'Contrast',
184: 0xa409: 'Saturation',
185: 0xa40a: 'Sharpness',
186: 0xa40b: 'DeviceSettingDescription',
187: 0xa40c: 'SubjectDistanceRange'
188:
189: }
190:
191:
192: class JPG_SZ:
193: def __init__(self, filename=None):
194: if filename != None:
195: self.setFile(filename)
196:
197: def setFile(self, filename):
198: self.jpgfile = filename
199: self.ErrMsg = ''
200: if not os.access(self.jpgfile, os.R_OK | os.F_OK):
201: self.ErrMsg = 'file not found!'
202: return 1
203: self.endian = 0 # 0: Motorola, 1:Intel
204: self.img_info = {}
205: if hasattr(self, 'X'):
206: del self.X
207: if hasattr(self, 'Y'):
208: del self.Y
209:
210: def Read(self):
211: data = open(self.jpgfile, 'rb').read(20000) # 20kB読めば充分?
212: start = 0
213: end = 2
214: marker = self.bin2int(data[start:end])
215: if marker != SOI:
216: self.ErrMsg = 'Not a JPEG file!'
217: return 1
218:
219: marker = self.bin2int(data[start+2:end+2])
220: if marker == APP1:
221: start = end + 2
222: ret = self.ReadExifFile(start, data)
223: else:
224: start = end
225: ret = self.ReadJfifFile(start, data)
226:
227: return ret
228:
229: def ReadJfifFile(self, start, data):
230: ret = 0
231: end = start + 2
232: while 1:
233: marker = lower(self.bin2str(data[start:end]))
234: if marker == lower('FFE0'):
235: sz = self.bin2int(data[end:end+2])
236: if data[end+2:end+2+4] != 'JFIF':
237: self.ErrMsg = 'Not a JPEG file!'
238: ret = 1
239: break
240: if data[end+2:end+2+4] != 'JFXX':
241: self.ErrMsg = 'JFIF Extension File!'
242: ret = 1
243: # break
244: start = end + sz
245: end = start + 2
246: elif marker == lower('FFDB'):
247: sz = self.bin2int(data[end:end+2])
248: start = end + sz
249: end = start + 2
250: elif marker == lower('FFC4'):
251: sz = self.bin2int(data[end:end+2])
252: start = end + sz
253: end = start + 2
254: elif marker == lower('FFC0') or \
255: marker == lower('FFC1') or \
256: marker == lower('FFC2') or \
257: marker == lower('FFC3') or \
258: marker == lower('FFC5') or \
259: marker == lower('FFC6') or \
260: marker == lower('FFC7') or \
261: marker == lower('FFC9') or \
262: marker == lower('FFCA') or \
263: marker == lower('FFCB'):
264: sz_y = self.bin2int(data[end+3:end+5])
265: sz_x = self.bin2int(data[end+5:end+7])
266: self.X, self.Y = (sz_x, sz_y)
267: ret = 0
268: # start = end + 17
269: # end = start + 2
270: break
271: else:
272: if marker[:2] == lower('FF'):
273: sz = self.bin2int(data[end:end+2])
274: start = end + sz
275: end = start + 2
276: else:
277: break
278:
279: return ret
280:
281: def ReadExifFile(self, start, data):
282: d = self.img_info
283: end = start + 2
284: sz_app1 = self.bin2int(data[start:end])
285: start = end
286: end = start + 6
287: if data[start:end] != EXIF_HDR:
288: self.ErrMsg = 'Not a EXIF file!'
289: return 1
290: start = end
291: end = start + 2
292: s = data[start:end]
293: if s == 'II':
294: self.endian = 1
295: elif s == 'MM':
296: self.endian = 0
297: self.offset_base = start
298: start = end
299: end = start + 2
300: sz = self.bin2int(data[start:end])
301: if sz != TIFF_TAG_MARK:
302: self.ErrMsg = 'Not a EXIF file!'
303: return 1
304: start = end
305: end = start + 4
306: sz = self.bin2int(data[start:end])
307: # start = end
308: start = self.offset_base + sz
309: end = start + 2
310:
311: #
312: # IFD0の読みとり
313: #
314: next_ifd_pos = self.readIFD(start, data)
315: #
316: # SubIFDの読みとり
317: #
318: start = d['ExifIFDPointer'] + self.offset_base
319: p = self.readIFD(start, data)
320: while(p != self.offset_base):
321: start = p
322: p = self.readIFD(start, data)
323: #
324: # IDF1の読みとり
325: #
326: start = next_ifd_pos
327: p = self.readIFD(start, data)
328: #
329: # InteroperabilityIFDの読みとり
330: if p != self.offset_base:
331: start = p
332: else:
333: if d.has_key('InteroperabilityIFDPointer'):
334: start = d['InteroperabilityIFDPointer'] + self.offset_base
335: if start != self.offset_base:
336: p = self.readIFD(start, data)
337:
338: # for x in d.keys():
339: # print x, ' : ', d[x]
340:
341: if d.has_key('PixelXDimension') and d.has_key('PixelYDimension'):
342: self.X, self.Y = d['PixelXDimension'], d['PixelYDimension']
343: else:
344: self.X, self.Y = None, None
345: return 0
346:
347: def readIFD(self, start, data):
348: d = self.img_info
349: num_ifd = self.bin2int(data[start:start+2])
350: tag_pos = start + 2
351: for n in range(num_ifd):
352: tag_nam = IFD_TAG[self.bin2int(data[tag_pos:][:2])]
353: tagd_typ = self.bin2int(data[tag_pos+2:][:2])
354: tagd_siz = self.bin2int(data[tag_pos+4:][:4])
355: tagd = data[tag_pos+8:][:4]
356: sz = type2bytes[tagd_typ] * tagd_siz
357: if sz > 4:
358: offset = self.bin2int(tagd)
359: tagd = data[self.offset_base+offset:][:sz]
360:
361: if tagd_typ in (2, 7):
362: d[tag_nam] = tagd
363: elif tagd_typ == 5:
364: if self.endian == 0:
365: d1 = self.bin2int(tagd[:4])
366: d2 = self.bin2int(tagd[4:])
367: elif self.endian == 1:
368: d2 = self.bin2int(tagd[:4])
369: d1 = self.bin2int(tagd[4:])
370: d[tag_nam] = str(d1) + '/' + str(d2)
371: elif tagd_typ == 10:
372: if self.endian == 0:
373: d1 = self.bin2sint(tagd[:4])
374: d2 = self.bin2sint(tagd[4:])
375: elif self.endian == 1:
376: d2 = self.bin2sint(tagd[:4])
377: d1 = self.bin2sint(tagd[4:])
378: d[tag_nam] = str(d1) + '/' + str(d2)
379: elif tagd_typ in (1, 3, 4):
380: d[tag_nam] = self.bin2int(tagd)
381: elif tagd_typ in (6, 8, 9):
382: d[tag_nam] = self.bin2sint(tagd)
383:
384: tag_pos = tag_pos + 12
385:
386: next_ifd_pos = self.offset_base + self.bin2int(data[tag_pos:][:4])
387:
388: return next_ifd_pos
389:
390: def bin2str(self, d):
391: s = ''
392: for n in d:
393: ss = hex(ord(n))[2:]
394: if len(ss) == 1:
395: ss = '0' + ss
396: s = s + ss
397: return s
398:
399: def bin2int(self, d):
400: m = len(d) - 1
401: r = 0L
402: for n in range(len(d)):
403: if self.endian == 0:
404: r = r + ord(d[n]) * pow(256L, m - n)
405: elif self.endian == 1:
406: r = r + ord(d[n]) * pow(256L, n)
407: return r
408:
409: def bin2sint(self, d):
410: m = pow(256L, len(d)) / 2
411: r = self.bin2int(d)
412: if r >= m:
413: r = r - m * 2
414: return r
415:
416: def getResult(self):
417: return self.X, self.Y
418:
419: def getErrMsg(self):
420: return self.ErrMsg
421:
422: if __name__ == '__main__':
423: # filename = '317.jpg'
424: filename = 'DSCF0015.JPG'
425: J = JPG_SZ(filename)
426: if J.Read() == 0:
427: print '%s: (x, y) = (%d, %d)' % ((filename,) + J.getResult())
428: else:
429: print J.getErrMsg()
430:
431: