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: