JPEGファイルの画像サイズを取り出すスクリプト
(最終更新:2003年2月10日)

以前↑のPython for Delphiのサンプルで作ったjpg_szなんですけどね、どうも僕が持っているデジカメ(っていうかDVカメラ)の画像でうまく動かないことがわかり、いろいろ調べたんですよ。 そうしたら一般にデジタルカメラで作られる.jpgのファイル、ファイル名からするとJPEGっぽいんですが実体はEXIFというフォーマットであるということがわかりました。
さらに調べていくとEXIFというフォーマットはTIFFのようにタグ付けしたデータを持っていると... これでは先のjpg_szではうまく読めないわけです。
そこで一念奮起してEXIFファイルにも対応できるようバージョンアップしました。
コードは以下の通り。拙作ではありますが、ダウンロードされるならjpg_sz2.pyをどうぞ。

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: