主にプログラミングに関して。Python, .NET Framework(C#), JavaScript, その他いくらか。
記事にあるサンプルやコードは要検証。使用に際しては責任を負いかねます

スポンサーサイト

                
tags:
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

Python: Exifを読み書きするモジュールを作る 事始め

                
tags: python
 PILでJPEGをリサイズした後の出力はExifが抜け落ちてしまう。これに対してモジュールを一個書いた。
https://github.com/hMatoba/Python-exifkeeper
 それだけではやっぱりPythonでExifを使うのにいろいろ不便がある。もっと自在にExifを使いたい。読んだり書いたり。

 PythonでExifを読む手段はいくらかある。Pyexiv2というexiv2のラッパーが読み書き両方できて便利らしい。そのほかにもPyPIでExifをいじるのに使えそうなのが一つか二つぐらいある。しかしただExifをいじりたいのではなく、画像の編集と組み合わせて使いたい。

 GoogleAppEngineでも使えるPILには、JPEGを読み込んで、Exifを辞書型として返す機能が備わっている。ただしExifを書き込むには、Exifのプロトコルにそったバイト列を自分で用意せねばならず、こっちはちょっと敷居が高い。けどピュアPythonで便利に使えるものが欲しいので、書いてしまうことにする。そういう要請がPillowにもあったし。Python2.7と3.4へ対応したもので下記のように使えることがゴール。
img = Image.open("foo.jpg")
img.resize((400, 300))

exif_dict = {"XResolution": 400,
"YResolution": 300,
"Make": "maker name",
"DateTime", "2006:02:14 22:00:00"}
exif_bytes = exifdump(exif_dict)

img.save("newfoo.jpg", exif=exif_bytes)


 以前にPILにExifを読むメソッドが用意されていることを知らずに途中まで書いたスクリプトがあったので、それをExifを読める形にしつつExifの仕様を理解し、そこからExifを書けるように拡張する。

参考
********
Exif仕様
http://www.jeita.or.jp/cgi-bin/standard/list.cgi?cateid=1&subcateid=4

解説
http://www2.airnet.ne.jp/~kenshi/exif.html
http://ryouto.jp/f6exif/exif.html

Exifタグ
http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html
http://www.exiv2.org/metadata.html

バイト列とPython
http://docs.python.jp/2/library/struct.html#struct-format-strings
********

 まだ慣れないPython3.4はあとで対応をするようにして、まず2.7に対応するように突貫で書いて直して直して。現状が下記。
"""Pure Python"""

import io
import struct


TAGS = {
'GPSInfo': {0: {'group': 'GPSVersionID', 'type': 'Byte'},
1: {'group': 'GPSLatitudeRef', 'type': 'Ascii'},
2: {'group': 'GPSLatitude', 'type': 'Rational'},
3: {'group': 'GPSLongitudeRef', 'type': 'Ascii'},
4: {'group': 'GPSLongitude', 'type': 'Rational'},
5: {'group': 'GPSAltitudeRef', 'type': 'Byte'},
6: {'group': 'GPSAltitude', 'type': 'Rational'},
7: {'group': 'GPSTimeStamp', 'type': 'Rational'},
8: {'group': 'GPSSatellites', 'type': 'Ascii'},
9: {'group': 'GPSStatus', 'type': 'Ascii'},
10: {'group': 'GPSMeasureMode', 'type': 'Ascii'},
11: {'group': 'GPSDOP', 'type': 'Rational'},
12: {'group': 'GPSSpeedRef', 'type': 'Ascii'},
13: {'group': 'GPSSpeed', 'type': 'Rational'},
14: {'group': 'GPSTrackRef', 'type': 'Ascii'},
15: {'group': 'GPSTrack', 'type': 'Rational'},
16: {'group': 'GPSImgDirectionRef', 'type': 'Ascii'},
17: {'group': 'GPSImgDirection', 'type': 'Rational'},
18: {'group': 'GPSMapDatum', 'type': 'Ascii'},
19: {'group': 'GPSDestLatitudeRef', 'type': 'Ascii'},
20: {'group': 'GPSDestLatitude', 'type': 'Rational'},
21: {'group': 'GPSDestLongitudeRef', 'type': 'Ascii'},
22: {'group': 'GPSDestLongitude', 'type': 'Rational'},
23: {'group': 'GPSDestBearingRef', 'type': 'Ascii'},
24: {'group': 'GPSDestBearing', 'type': 'Rational'},
25: {'group': 'GPSDestDistanceRef', 'type': 'Ascii'},
26: {'group': 'GPSDestDistance', 'type': 'Rational'},
27: {'group': 'GPSProcessingMethod', 'type': 'Undefined'},
28: {'group': 'GPSAreaInformation', 'type': 'Undefined'},
29: {'group': 'GPSDateStamp', 'type': 'Ascii'},
30: {'group': 'GPSDifferential', 'type': 'Short'}},
'Image': {11: {'group': 'ProcessingSoftware', 'type': 'Ascii'},
254: {'group': 'NewSubfileType', 'type': 'Long'},
255: {'group': 'SubfileType', 'type': 'Short'},
256: {'group': 'ImageWidth', 'type': 'Long'},
257: {'group': 'ImageLength', 'type': 'Long'},
258: {'group': 'BitsPerSample', 'type': 'Short'},
259: {'group': 'Compression', 'type': 'Short'},
262: {'group': 'PhotometricInterpretation', 'type': 'Short'},
263: {'group': 'Threshholding', 'type': 'Short'},
264: {'group': 'CellWidth', 'type': 'Short'},
265: {'group': 'CellLength', 'type': 'Short'},
266: {'group': 'FillOrder', 'type': 'Short'},
269: {'group': 'DocumentName', 'type': 'Ascii'},
270: {'group': 'ImageDescription', 'type': 'Ascii'},
271: {'group': 'Make', 'type': 'Ascii'},
272: {'group': 'Model', 'type': 'Ascii'},
273: {'group': 'StripOffsets', 'type': 'Long'},
274: {'group': 'Orientation', 'type': 'Short'},
277: {'group': 'SamplesPerPixel', 'type': 'Short'},
278: {'group': 'RowsPerStrip', 'type': 'Long'},
279: {'group': 'StripByteCounts', 'type': 'Long'},
282: {'group': 'XResolution', 'type': 'Rational'},
283: {'group': 'YResolution', 'type': 'Rational'},
284: {'group': 'PlanarConfiguration', 'type': 'Short'},
290: {'group': 'GrayResponseUnit', 'type': 'Short'},
291: {'group': 'GrayResponseCurve', 'type': 'Short'},
292: {'group': 'T4Options', 'type': 'Long'},
293: {'group': 'T6Options', 'type': 'Long'},
296: {'group': 'ResolutionUnit', 'type': 'Short'},
301: {'group': 'TransferFunction', 'type': 'Short'},
305: {'group': 'Software', 'type': 'Ascii'},
306: {'group': 'DateTime', 'type': 'Ascii'},
315: {'group': 'Artist', 'type': 'Ascii'},
316: {'group': 'HostComputer', 'type': 'Ascii'},
317: {'group': 'Predictor', 'type': 'Short'},
318: {'group': 'WhitePoint', 'type': 'Rational'},
319: {'group': 'PrimaryChromaticities', 'type': 'Rational'},
320: {'group': 'ColorMap', 'type': 'Short'},
321: {'group': 'HalftoneHints', 'type': 'Short'},
322: {'group': 'TileWidth', 'type': 'Short'},
323: {'group': 'TileLength', 'type': 'Short'},
324: {'group': 'TileOffsets', 'type': 'Short'},
325: {'group': 'TileByteCounts', 'type': 'Short'},
330: {'group': 'SubIFDs', 'type': 'Long'},
332: {'group': 'InkSet', 'type': 'Short'},
333: {'group': 'InkNames', 'type': 'Ascii'},
334: {'group': 'NumberOfInks', 'type': 'Short'},
336: {'group': 'DotRange', 'type': 'Byte'},
337: {'group': 'TargetPrinter', 'type': 'Ascii'},
338: {'group': 'ExtraSamples', 'type': 'Short'},
339: {'group': 'SampleFormat', 'type': 'Short'},
340: {'group': 'SMinSampleValue', 'type': 'Short'},
341: {'group': 'SMaxSampleValue', 'type': 'Short'},
342: {'group': 'TransferRange', 'type': 'Short'},
343: {'group': 'ClipPath', 'type': 'Byte'},
344: {'group': 'XClipPathUnits', 'type': 'SShort'},
345: {'group': 'YClipPathUnits', 'type': 'SShort'},
346: {'group': 'Indexed', 'type': 'Short'},
347: {'group': 'JPEGTables', 'type': 'Undefined'},
351: {'group': 'OPIProxy', 'type': 'Short'},
512: {'group': 'JPEGProc', 'type': 'Long'},
513: {'group': 'JPEGInterchangeFormat', 'type': 'Long'},
514: {'group': 'JPEGInterchangeFormatLength', 'type': 'Long'},
515: {'group': 'JPEGRestartInterval', 'type': 'Short'},
517: {'group': 'JPEGLosslessPredictors', 'type': 'Short'},
518: {'group': 'JPEGPointTransforms', 'type': 'Short'},
519: {'group': 'JPEGQTables', 'type': 'Long'},
520: {'group': 'JPEGDCTables', 'type': 'Long'},
521: {'group': 'JPEGACTables', 'type': 'Long'},
529: {'group': 'YCbCrCoefficients', 'type': 'Rational'},
530: {'group': 'YCbCrSubSampling', 'type': 'Short'},
531: {'group': 'YCbCrPositioning', 'type': 'Short'},
532: {'group': 'ReferenceBlackWhite', 'type': 'Rational'},
700: {'group': 'XMLPacket', 'type': 'Byte'},
18246: {'group': 'Rating', 'type': 'Short'},
18249: {'group': 'RatingPercent', 'type': 'Short'},
32781: {'group': 'ImageID', 'type': 'Ascii'},
33421: {'group': 'CFARepeatPatternDim', 'type': 'Short'},
33422: {'group': 'CFAPattern', 'type': 'Byte'},
33423: {'group': 'BatteryLevel', 'type': 'Rational'},
33432: {'group': 'Copyright', 'type': 'Ascii'},
33434: {'group': 'ExposureTime', 'type': 'Rational'},
33437: {'group': 'FNumber', 'type': 'Rational'},
33723: {'group': 'IPTCNAA', 'type': 'Long'},
34377: {'group': 'ImageResources', 'type': 'Byte'},
34665: {'group': 'ExifTag', 'type': 'Long'},
34675: {'group': 'InterColorProfile', 'type': 'Undefined'},
34850: {'group': 'ExposureProgram', 'type': 'Short'},
34852: {'group': 'SpectralSensitivity', 'type': 'Ascii'},
34853: {'group': 'GPSTag', 'type': 'Long'},
34855: {'group': 'ISOSpeedRatings', 'type': 'Short'},
34856: {'group': 'OECF', 'type': 'Undefined'},
34857: {'group': 'Interlace', 'type': 'Short'},
34858: {'group': 'TimeZoneOffset', 'type': 'SShort'},
34859: {'group': 'SelfTimerMode', 'type': 'Short'},
36867: {'group': 'DateTimeOriginal', 'type': 'Ascii'},
37122: {'group': 'CompressedBitsPerPixel', 'type': 'Rational'},
37377: {'group': 'ShutterSpeedValue', 'type': 'SRational'},
37378: {'group': 'ApertureValue', 'type': 'Rational'},
37379: {'group': 'BrightnessValue', 'type': 'SRational'},
37380: {'group': 'ExposureBiasValue', 'type': 'SRational'},
37381: {'group': 'MaxApertureValue', 'type': 'Rational'},
37382: {'group': 'SubjectDistance', 'type': 'SRational'},
37383: {'group': 'MeteringMode', 'type': 'Short'},
37384: {'group': 'LightSource', 'type': 'Short'},
37385: {'group': 'Flash', 'type': 'Short'},
37386: {'group': 'FocalLength', 'type': 'Rational'},
37387: {'group': 'FlashEnergy', 'type': 'Rational'},
37388: {'group': 'SpatialFrequencyResponse', 'type': 'Undefined'},
37389: {'group': 'Noise', 'type': 'Undefined'},
37390: {'group': 'FocalPlaneXResolution', 'type': 'Rational'},
37391: {'group': 'FocalPlaneYResolution', 'type': 'Rational'},
37392: {'group': 'FocalPlaneResolutionUnit', 'type': 'Short'},
37393: {'group': 'ImageNumber', 'type': 'Long'},
37394: {'group': 'SecurityClassification', 'type': 'Ascii'},
37395: {'group': 'ImageHistory', 'type': 'Ascii'},
37396: {'group': 'SubjectLocation', 'type': 'Short'},
37397: {'group': 'ExposureIndex', 'type': 'Rational'},
37398: {'group': 'TIFFEPStandardID', 'type': 'Byte'},
37399: {'group': 'SensingMethod', 'type': 'Short'},
40091: {'group': 'XPTitle', 'type': 'Byte'},
40092: {'group': 'XPComment', 'type': 'Byte'},
40093: {'group': 'XPAuthor', 'type': 'Byte'},
40094: {'group': 'XPKeywords', 'type': 'Byte'},
40095: {'group': 'XPSubject', 'type': 'Byte'},
50341: {'group': 'PrintImageMatching', 'type': 'Undefined'},
50706: {'group': 'DNGVersion', 'type': 'Byte'},
50707: {'group': 'DNGBackwardVersion', 'type': 'Byte'},
50708: {'group': 'UniqueCameraModel', 'type': 'Ascii'},
50709: {'group': 'LocalizedCameraModel', 'type': 'Byte'},
50710: {'group': 'CFAPlaneColor', 'type': 'Byte'},
50711: {'group': 'CFALayout', 'type': 'Short'},
50712: {'group': 'LinearizationTable', 'type': 'Short'},
50713: {'group': 'BlackLevelRepeatDim', 'type': 'Short'},
50714: {'group': 'BlackLevel', 'type': 'Rational'},
50715: {'group': 'BlackLevelDeltaH', 'type': 'SRational'},
50716: {'group': 'BlackLevelDeltaV', 'type': 'SRational'},
50717: {'group': 'WhiteLevel', 'type': 'Short'},
50718: {'group': 'DefaultScale', 'type': 'Rational'},
50719: {'group': 'DefaultCropOrigin', 'type': 'Short'},
50720: {'group': 'DefaultCropSize', 'type': 'Short'},
50721: {'group': 'ColorMatrix1', 'type': 'SRational'},
50722: {'group': 'ColorMatrix2', 'type': 'SRational'},
50723: {'group': 'CameraCalibration1', 'type': 'SRational'},
50724: {'group': 'CameraCalibration2', 'type': 'SRational'},
50725: {'group': 'ReductionMatrix1', 'type': 'SRational'},
50726: {'group': 'ReductionMatrix2', 'type': 'SRational'},
50727: {'group': 'AnalogBalance', 'type': 'Rational'},
50728: {'group': 'AsShotNeutral', 'type': 'Short'},
50729: {'group': 'AsShotWhiteXY', 'type': 'Rational'},
50730: {'group': 'BaselineExposure', 'type': 'SRational'},
50731: {'group': 'BaselineNoise', 'type': 'Rational'},
50732: {'group': 'BaselineSharpness', 'type': 'Rational'},
50733: {'group': 'BayerGreenSplit', 'type': 'Long'},
50734: {'group': 'LinearResponseLimit', 'type': 'Rational'},
50735: {'group': 'CameraSerialNumber', 'type': 'Ascii'},
50736: {'group': 'LensInfo', 'type': 'Rational'},
50737: {'group': 'ChromaBlurRadius', 'type': 'Rational'},
50738: {'group': 'AntiAliasStrength', 'type': 'Rational'},
50739: {'group': 'ShadowScale', 'type': 'SRational'},
50740: {'group': 'DNGPrivateData', 'type': 'Byte'},
50741: {'group': 'MakerNoteSafety', 'type': 'Short'},
50778: {'group': 'CalibrationIlluminant1', 'type': 'Short'},
50779: {'group': 'CalibrationIlluminant2', 'type': 'Short'},
50780: {'group': 'BestQualityScale', 'type': 'Rational'},
50781: {'group': 'RawDataUniqueID', 'type': 'Byte'},
50827: {'group': 'OriginalRawFileName', 'type': 'Byte'},
50828: {'group': 'OriginalRawFileData', 'type': 'Undefined'},
50829: {'group': 'ActiveArea', 'type': 'Short'},
50830: {'group': 'MaskedAreas', 'type': 'Short'},
50831: {'group': 'AsShotICCProfile', 'type': 'Undefined'},
50832: {'group': 'AsShotPreProfileMatrix', 'type': 'SRational'},
50833: {'group': 'CurrentICCProfile', 'type': 'Undefined'},
50834: {'group': 'CurrentPreProfileMatrix', 'type': 'SRational'},
50879: {'group': 'ColorimetricReference', 'type': 'Short'},
50931: {'group': 'CameraCalibrationSignature', 'type': 'Byte'},
50932: {'group': 'ProfileCalibrationSignature', 'type': 'Byte'},
50934: {'group': 'AsShotProfileName', 'type': 'Byte'},
50935: {'group': 'NoiseReductionApplied', 'type': 'Rational'},
50936: {'group': 'ProfileName', 'type': 'Byte'},
50937: {'group': 'ProfileHueSatMapDims', 'type': 'Long'},
50938: {'group': 'ProfileHueSatMapData1', 'type': 'Float'},
50939: {'group': 'ProfileHueSatMapData2', 'type': 'Float'},
50940: {'group': 'ProfileToneCurve', 'type': 'Float'},
50941: {'group': 'ProfileEmbedPolicy', 'type': 'Long'},
50942: {'group': 'ProfileCopyright', 'type': 'Byte'},
50964: {'group': 'ForwardMatrix1', 'type': 'SRational'},
50965: {'group': 'ForwardMatrix2', 'type': 'SRational'},
50966: {'group': 'PreviewApplicationName', 'type': 'Byte'},
50967: {'group': 'PreviewApplicationVersion', 'type': 'Byte'},
50968: {'group': 'PreviewSettingsName', 'type': 'Byte'},
50969: {'group': 'PreviewSettingsDigest', 'type': 'Byte'},
50970: {'group': 'PreviewColorSpace', 'type': 'Long'},
50971: {'group': 'PreviewDateTime', 'type': 'Ascii'},
50972: {'group': 'RawImageDigest', 'type': 'Undefined'},
50973: {'group': 'OriginalRawFileDigest', 'type': 'Undefined'},
50974: {'group': 'SubTileBlockSize', 'type': 'Long'},
50975: {'group': 'RowInterleaveFactor', 'type': 'Long'},
50981: {'group': 'ProfileLookTableDims', 'type': 'Long'},
50982: {'group': 'ProfileLookTableData', 'type': 'Float'},
51008: {'group': 'OpcodeList1', 'type': 'Undefined'},
51009: {'group': 'OpcodeList2', 'type': 'Undefined'},
51022: {'group': 'OpcodeList3', 'type': 'Undefined'},
51041: {'group': 'NoiseProfile', 'type': 'Double'}},
'Iop': {1: {'group': 'InteroperabilityIndex', 'type': 'Ascii'},
2: {'group': 'InteroperabilityVersion', 'type': 'Undefined'},
4096: {'group': 'RelatedImageFileFormat', 'type': 'Ascii'},
4097: {'group': 'RelatedImageWidth', 'type': 'Long'},
4098: {'group': 'RelatedImageLength', 'type': 'Long'}},
'Photo': {33434: {'group': 'ExposureTime', 'type': 'Rational'},
33437: {'group': 'FNumber', 'type': 'Rational'},
34850: {'group': 'ExposureProgram', 'type': 'Short'},
34852: {'group': 'SpectralSensitivity', 'type': 'Ascii'},
34855: {'group': 'ISOSpeedRatings', 'type': 'Short'},
34856: {'group': 'OECF', 'type': 'Undefined'},
34864: {'group': 'SensitivityType', 'type': 'Short'},
34865: {'group': 'StandardOutputSensitivity', 'type': 'Long'},
34866: {'group': 'RecommendedExposureIndex', 'type': 'Long'},
34867: {'group': 'ISOSpeed', 'type': 'Long'},
34868: {'group': 'ISOSpeedLatitudeyyy', 'type': 'Long'},
34869: {'group': 'ISOSpeedLatitudezzz', 'type': 'Long'},
36864: {'group': 'ExifVersion', 'type': 'Undefined'},
36867: {'group': 'DateTimeOriginal', 'type': 'Ascii'},
36868: {'group': 'DateTimeDigitized', 'type': 'Ascii'},
37121: {'group': 'ComponentsConfiguration', 'type': 'Undefined'},
37122: {'group': 'CompressedBitsPerPixel', 'type': 'Rational'},
37377: {'group': 'ShutterSpeedValue', 'type': 'SRational'},
37378: {'group': 'ApertureValue', 'type': 'Rational'},
37379: {'group': 'BrightnessValue', 'type': 'SRational'},
37380: {'group': 'ExposureBiasValue', 'type': 'SRational'},
37381: {'group': 'MaxApertureValue', 'type': 'Rational'},
37382: {'group': 'SubjectDistance', 'type': 'Rational'},
37383: {'group': 'MeteringMode', 'type': 'Short'},
37384: {'group': 'LightSource', 'type': 'Short'},
37385: {'group': 'Flash', 'type': 'Short'},
37386: {'group': 'FocalLength', 'type': 'Rational'},
37396: {'group': 'SubjectArea', 'type': 'Short'},
37500: {'group': 'MakerNote', 'type': 'Undefined'},
37510: {'group': 'UserComment', 'type': 'Comment'},
37520: {'group': 'SubSecTime', 'type': 'Ascii'},
37521: {'group': 'SubSecTimeOriginal', 'type': 'Ascii'},
37522: {'group': 'SubSecTimeDigitized', 'type': 'Ascii'},
40960: {'group': 'FlashpixVersion', 'type': 'Undefined'},
40961: {'group': 'ColorSpace', 'type': 'Short'},
40962: {'group': 'PixelXDimension', 'type': 'Long'},
40963: {'group': 'PixelYDimension', 'type': 'Long'},
40964: {'group': 'RelatedSoundFile', 'type': 'Ascii'},
40965: {'group': 'InteroperabilityTag', 'type': 'Long'},
41483: {'group': 'FlashEnergy', 'type': 'Rational'},
41484: {'group': 'SpatialFrequencyResponse', 'type': 'Undefined'},
41486: {'group': 'FocalPlaneXResolution', 'type': 'Rational'},
41487: {'group': 'FocalPlaneYResolution', 'type': 'Rational'},
41488: {'group': 'FocalPlaneResolutionUnit', 'type': 'Short'},
41492: {'group': 'SubjectLocation', 'type': 'Short'},
41493: {'group': 'ExposureIndex', 'type': 'Rational'},
41495: {'group': 'SensingMethod', 'type': 'Short'},
41728: {'group': 'FileSource', 'type': 'Undefined'},
41729: {'group': 'SceneType', 'type': 'Undefined'},
41730: {'group': 'CFAPattern', 'type': 'Undefined'},
41985: {'group': 'CustomRendered', 'type': 'Short'},
41986: {'group': 'ExposureMode', 'type': 'Short'},
41987: {'group': 'WhiteBalance', 'type': 'Short'},
41988: {'group': 'DigitalZoomRatio', 'type': 'Rational'},
41989: {'group': 'FocalLengthIn35mmFilm', 'type': 'Short'},
41990: {'group': 'SceneCaptureType', 'type': 'Short'},
41991: {'group': 'GainControl', 'type': 'Short'},
41992: {'group': 'Contrast', 'type': 'Short'},
41993: {'group': 'Saturation', 'type': 'Short'},
41994: {'group': 'Sharpness', 'type': 'Short'},
41995: {'group': 'DeviceSettingDescription', 'type': 'Undefined'},
41996: {'group': 'SubjectDistanceRange', 'type': 'Short'},
42016: {'group': 'ImageUniqueID', 'type': 'Ascii'},
42032: {'group': 'CameraOwnerName', 'type': 'Ascii'},
42033: {'group': 'BodySerialNumber', 'type': 'Ascii'},
42034: {'group': 'LensSpecification', 'type': 'Rational'},
42035: {'group': 'LensMake', 'type': 'Ascii'},
42036: {'group': 'LensModel', 'type': 'Ascii'},
42037: {'group': 'LensSerialNumber', 'type': 'Ascii'}}}

TYPES = {
"Byte":1,
"Ascii":2,
"Short":3,
"Long":4,
"Rational":5,
"Undefined":7,
"SLong":9,
"SRational":10}

POINTERS = (34665, 34853)

TIFF_HEADER_LENGTH = 8

def printbytes(data):
print(':'.join(x.encode('hex') for x in data))

def reverse(data):
return data[::-1]

def hexstr2value(data):
value = int(data.encode("hex"), 16)
return value

def _split_into_segments(data):
"""Slices JPEG meta data into a list from JPEG binary data.
"""
head = 0
segments = []

while 1:
if data[head: head + 2] == b"\xff\xd8":
head += 2
else:
length = hexstr2value(data[head + 2] * 256 + data[head + 3])
endPoint = head + length + 2
seg = data[head: endPoint]
segments.append(seg)
head = endPoint

if (head >= len(data)) or (data[head: head + 2] == b"\xff\xda"):
break

return segments


def _get_exif(segments):
"""Returns Exif from JPEG meta data list
"""
for seg in segments:
if seg[0:2] == b"\xff\xe1":
return seg
return None

class ExifReader(object):
def __init__(self, data):
if data[0:2] == b"\xff\xd8":
pass
else:
with open(data, 'rb') as f:
data = f.read()

segments = _split_into_segments(data)
exif = _get_exif(segments)

if exif:
self.exif_str = exif[10:]
else:
self.exif_str = ""

def get_exif_ifd(self):
endian = self.exif_str[0:2]
self.little_endian = True if endian == "II" else False
exif_dict = {}
gps_dict = {}

pointer = hexstr2value(self.exif_str[4:8] if not self.little_endian else reverse(self.exif_str[4:8]))
zeroth_dict = self.get_ifd_info(pointer)

if 34665 in zeroth_dict:
pointer = int(zeroth_dict[34665][2], 16)
exif_dict = self.get_ifd_info(pointer)

if 34853 in exif_dict:
pointer = int(exif_dict[34853][2], 16)
gps_dict = self.get_ifd_info(pointer)

return zeroth_dict, exif_dict, gps_dict

def get_ifd_info(self, pointer):
ifd_dict = {}
tag_count = hexstr2value(self.exif_str[pointer: pointer+2] if not self.little_endian else reverse(self.exif_str[pointer: pointer+2]))
offset = pointer + 2
for x in range(tag_count):
pointer = offset + 12 * x
tag_code = hexstr2value(self.exif_str[pointer: pointer+2])
value_type = hexstr2value(self.exif_str[pointer+2: pointer+4])
value_num = hexstr2value(self.exif_str[pointer+4: pointer+8])
value = self.exif_str[pointer+8: pointer+12]
ifd_dict.update({tag_code:[value_type, value_num, value.encode("hex")]})
return ifd_dict

def get_info(self, val):
data = None

if val[0] == 1: # BYTE
data = int(val[2][0:2], 16)
elif val[0] == 2: # ASCII
if val[1] > 4:
pointer = int(val[2], 16)
data = self.exif_str[pointer: pointer+val[1]]
else:
pointer = int(val[2], 16)
data = val[2][0: 2 * val[1]].decode("hex")
elif val[0] == 3: # SHORT
data = int(val[2][0:4], 16)
elif val[0] == 4: # LONG
data = int(val[2][0:8], 16)
elif val[0] == 5: # RATIONAL
pointer = int(val[2], 16)
data = (int(self.exif_str[pointer: pointer + 4].encode("hex"), 16),
int(self.exif_str[pointer + 4: pointer + 8].encode("hex"), 16))
elif val[0] == 7: # UNDEFINED BYTES
if val[1] > 4:
pointer = int(val[2], 16)
data = self.exif_str[pointer: pointer+val[1]]
else:
pointer = int(val[2], 16)
data = val[2][0: 2 * val[1]]
elif val[0] == 9: # SLONG
data = int(val[2][0:8], 16)
elif val[0] == 10: # SRATIONAL
pointer = int(val[2], 16)
data = (struct.unpack(">l", self.exif_str[pointer: pointer + 4])[0],
struct.unpack(">l", self.exif_str[pointer + 4: pointer + 8])[0])

return data


def load_from_file(filename):
exifReader = ExifReader(filename)
zeroth_ifd, exif_ifd, gps_ifd = exifReader.get_exif_ifd()

zeroth_dict = {TAGS["Image"][key]["group"]: exifReader.get_info(zeroth_ifd[key])
for key in zeroth_ifd if key in TAGS["Image"]}
exif_dict = {TAGS["Photo"][key]["group"]: exifReader.get_info(exif_ifd[key])
for key in exif_ifd if key in TAGS["Photo"]}
gps_dict = {TAGS["GPSInfo"][key]["group"]: exifReader.get_info(gps_ifd[key])
for key in gps_ifd if key in TAGS["GPSInfo"]}

return zeroth_dict, exif_dict, gps_dict


def load(exif_bytes):
pass


def dump(zeroth_ifd, exif_ifd={}, gps_ifd={}):
exif_bytes = "Exif\x00\x00\x4d\x4d\x00\x2a\x00\x00\x00\x08"
exif_bytes += dict_to_bytes(zeroth_ifd, "Image", 0)
return exif_bytes


def dict_to_bytes(ifd_dict, group, ifd_offset):
tag_count = len(ifd_dict)
entry_header = struct.pack(">H", tag_count)
entries_length = 2 + tag_count * 12 + 4
entries = ""
values = ""
next_ifd_is = False

for n, key in enumerate(ifd_dict):
if key in POINTERS:
next_ifd_is = True
pointer_key = key
continue
raw_value = ifd_dict[key]
key_str = struct.pack(">I", key)[2:4]
value_type = TAGS[group][key]["type"]
type_str = struct.pack(">I", TYPES[value_type])[2:4]
if value_type == "Byte":
length = 1
value_str = struct.pack('>I', raw_value)[4] + "\x00"
elif value_type == "Short":
length = 2
value_str = struct.pack('>I', raw_value)[2:4] + "\x00" * 2
elif value_type == "Long":
length = 4
value_str = struct.pack('>I', raw_value)
elif value_type == "SLong":
length = 4
value_str = struct.pack('>i', raw_value)
elif value_type == "Ascii":
if len(raw_value) > 4:
length = len(raw_value)
if length % 4:
new_value = raw_value + " " * (4 - length % 4)
length = length + (4 - length % 4)
else:
new_value = raw_value
offset = TIFF_HEADER_LENGTH + ifd_offset + entries_length + len(values)
value_str = struct.pack(">I", offset)
values += new_value
else:
length = len(raw_value)
value_str = raw_value + "\x00" * (4 - length)
elif value_type == "Rational":
length = 1
num, den = raw_value
new_value = struct.pack(">L", num) + struct.pack(">L", den)
offset = TIFF_HEADER_LENGTH + ifd_offset + entries_length + len(values)
value_str = struct.pack(">I", offset)
values += new_value
elif value_type == "SRational":
length = 1
num, den = raw_value
new_value = struct.pack(">l", num) + struct.pack(">l", den)
offset = TIFF_HEADER_LENGTH + ifd_offset + entries_length + len(values)
value_str = struct.pack(">I", offset)
values += new_value
elif value_type == "Undefined":
if len(raw_value) > 4:
length = len(raw_value)
if length % 4:
new_value = raw_value + " " * (4 - length % 4)
length = length + (4 - length % 4)
else:
new_value = raw_value
offset = TIFF_HEADER_LENGTH + ifd_offset + entries_length + len(values)
value_str = struct.pack(">I", offset)
values += new_value
else:
length = len(raw_value)
value_str = raw_value + "\x00" * (4 - length)

## print key, value_type, length, raw_value
## printbytes(key_str + type_str + length_str + value_str)
length_str = struct.pack(">I", length)
entries += key_str + type_str + length_str + value_str
## print entries, len(entries)
## print values, len(values)
if next_ifd_is:
pointer_value = TIFF_HEADER_LENGTH + entries_length + 12 + 4 + len(values)
pointer_str = struct.pack(">I", pointer_value)
if group == "Image":
key = 34665
elif group == "Photo":
key = 34853
entryies += struck.pack(">H", key) + "\x00\01" + pointer_str

ifd_str = entry_header + entries + "\x00\x00\x00\x00" + values
## print(len(entry_header), len(entries), len(values), "total: ", len(ifd_str))
return ifd_str


def read_test(input_file):
zeroth_dict, exif_dict, gps_dict = load_from_file(input_file)

print("0th IFD")
for key in zeroth_dict:
print(key, zeroth_dict[key])

print("\nEXIF IFD")
for key in exif_dict:
if isinstance(exif_dict[key], (str, bytes)) and len(exif_dict[key]) > 30:
print(key, exif_dict[key][:10], len(exif_dict[key]))
else:
print(key, exif_dict[key])

print("\nGPS IFD")
for key in gps_dict:
if isinstance(gps_dict[key], (str, bytes)) and len(gps_dict[key]) > 30:
print(key, gps_dict[key][:10], len(exif_dict[key]))
else:
print(key, gps_dict[key])


def write_test(input_file, output_file):
zeroth_ifd = {282: (96, 1),
283: (96, 1),
296: 2,
305: 'paint.net 4.0.3'}

exif_bytes = dump(zeroth_ifd=zeroth_ifd)

im = Image.open(input_file)
im.thumbnail((100, 100), Image.ANTIALIAS)
im.save(output_file, exif=exif_bytes)

i = Image.open(output_file) # check generated file can be loaded by standard tool
print(i._getexif())


if __name__ == "__main__":
from PIL import Image
read_test(r"C:\users\foo\01.jpg")
write_test(r"c:\users\foo\foo.jpg", r"c:\users\foo\newfoo.jpg")


 適当に手元にあったJPEGを使って実行したread_testの出力が下記。それなり形になっている。
0th IFD
('YResolution', (72, 1))
('ResolutionUnit', 2)
('ExifTag', 218)
('Make', 'CASIO COMPUTER CO.,LTD \x00')
('PrintImageMatching', 'PrintIM\x000250\x00\x00\x00\x04\x00\x01\x00\x16\x00\x16\x00\x02\x01\x00\x00\x00\x01\x00\x05\x00\x00\x00\x01\x01\x00\x00\x00\x00')
('DateTime', '2006:02:14 22:00:58\x00')
('YCbCrPositioning', 1)
('XResolution', (72, 1))
('Model', 'QV-R51 \x00')
('Software', 'paint.net 4.0.3\x00')

EXIF IFD
('LightSource', 1)
('ColorSpace', 1)
('ExposureMode', 0)
('Flash', 16)
('FlashpixVersion', '30313030')
('SceneCaptureType', 1)
('MeteringMode', 5)
('ExifVersion', '30323231')
('ExposureBiasValue', (0, 3))
('Saturation', 2)
('Contrast', 2)
('MakerNote', 'QVC\x00\x00\x00\x009\x00\x02', 33120)
('ExposureProgram', 8)
('FocalLengthIn35mmFilm', 69)
('PixelXDimension', 1920)
('PixelYDimension', 2560)
('DateTimeDigitized', '2006:02:14 22:00:58\x00')
('DateTimeOriginal', '2006:02:14 22:00:58\x00')
('WhiteBalance', 1)
('CompressedBitsPerPixel', (1843200, 4915200))
('FNumber', (62, 10))
('CustomRendered', 0)
('FocalLength', (1420, 100))
('ComponentsConfiguration', '01020300')
('ExposureTime', (1, 160))
('FileSource', '03')
('MaxApertureValue', (30, 10))
('Sharpness', 2)
('GainControl', 0)
('DigitalZoomRatio', (0, 0))

GPS IFD


 write_testの出力が下記。まだ0th IFDの書き込みしか実装してないし出力もごく短いが、PILのモジュールでもちゃんとExifが返されているので、不正なバイト列を作ってしまっているということはなさそう。
{296: (2, 0), 305: u'paint.net 4.0.3 ', 282: (96, 1), 283: (96, 1)}


 とりあえずの動作はさせられたので、仕様に照らしてもっとまともなテストを書きつつ、Exif書き込みをExif IFD、GPS IFDに対応を広げる、リファクタリングなどを進めていく。PyPIにのっけるところまで整える。
            

コメントの投稿

非公開コメント

プロフィール

hMatoba

Author:hMatoba
Github

最新記事
リンク
作ったものなど
月別アーカイブ
カテゴリ
タグリスト

検索フォーム
Amazon
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。