Merge pull request #1419 from wiredfool/tiff-ifd-rewrite

Tiff ImageFileDirectory Rewrite
This commit is contained in:
wiredfool 2015-09-20 10:36:19 +01:00
commit de34547f33
18 changed files with 1149 additions and 1071 deletions

View File

@ -217,7 +217,7 @@ def getiptcinfo(im):
while app[offset:offset+4] == b"8BIM": while app[offset:offset+4] == b"8BIM":
offset += 4 offset += 4
# resource code # resource code
code = JpegImagePlugin.i16(app, offset) code = i16(app, offset)
offset += 2 offset += 2
# resource name (usually empty) # resource name (usually empty)
name_len = i8(app[offset]) name_len = i8(app[offset])
@ -226,7 +226,7 @@ def getiptcinfo(im):
if offset & 1: if offset & 1:
offset += 1 offset += 1
# resource data block # resource data block
size = JpegImagePlugin.i32(app, offset) size = i32(app, offset)
offset += 4 offset += 4
if code == 0x0404: if code == 0x0404:
# 0x0404 contains IPTC/NAA data # 0x0404 contains IPTC/NAA data

View File

@ -36,7 +36,7 @@ import array
import struct import struct
import io import io
import warnings import warnings
from struct import unpack from struct import unpack_from
from PIL import Image, ImageFile, TiffImagePlugin, _binary from PIL import Image, ImageFile, TiffImagePlugin, _binary
from PIL.JpegPresets import presets from PIL.JpegPresets import presets
from PIL._util import isStringType from PIL._util import isStringType
@ -394,13 +394,6 @@ class JpegImageFile(ImageFile.ImageFile):
return _getmp(self) return _getmp(self)
def _fixup(value):
# Helper function for _getexif() and _getmp()
if len(value) == 1:
return value[0]
return value
def _getexif(self): def _getexif(self):
# Extract EXIF information. This method is highly experimental, # Extract EXIF information. This method is highly experimental,
# and is likely to be replaced with something better in a future # and is likely to be replaced with something better in a future
@ -414,12 +407,10 @@ def _getexif(self):
return None return None
file = io.BytesIO(data[6:]) file = io.BytesIO(data[6:])
head = file.read(8) head = file.read(8)
exif = {}
# process dictionary # process dictionary
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(file) info.load(file)
for key, value in info.items(): exif = dict(info)
exif[key] = _fixup(value)
# get exif extension # get exif extension
try: try:
# exif field 0x8769 is an offset pointer to the location # exif field 0x8769 is an offset pointer to the location
@ -429,24 +420,21 @@ def _getexif(self):
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
else: else:
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(file) info.load(file)
for key, value in info.items(): exif.update(info)
exif[key] = _fixup(value)
# get gpsinfo extension # get gpsinfo extension
try: try:
# exif field 0x8825 is an offset pointer to the location # exif field 0x8825 is an offset pointer to the location
# of the nested embedded gps exif ifd. # of the nested embedded gps exif ifd.
# It should be a long, but may be corrupted. # It should be a long, but may be corrupted.
file.seek(exif[0x8825]) file.seek(exif[0x8825])
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
else: else:
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(file) info.load(file)
exif[0x8825] = gps = {} exif[0x8825] = dict(info)
for key, value in info.items():
gps[key] = _fixup(value)
return exif return exif
@ -464,23 +452,22 @@ def _getmp(self):
file_contents = io.BytesIO(data) file_contents = io.BytesIO(data)
head = file_contents.read(8) head = file_contents.read(8)
endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<'
mp = {}
# process dictionary # process dictionary
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory_v2(head)
info.load(file_contents) info.load(file_contents)
for key, value in info.items(): mp = dict(info)
mp[key] = _fixup(value)
# it's an error not to have a number of images # it's an error not to have a number of images
try: try:
quant = mp[0xB001] quant = mp[0xB001]
except KeyError: except KeyError:
raise SyntaxError("malformed MP Index (no number of images)") raise SyntaxError("malformed MP Index (no number of images)")
# get MP entries # get MP entries
mpentries = []
try: try:
mpentries = [] rawmpentries = mp[0xB002]
for entrynum in range(0, quant): for entrynum in range(0, quant):
rawmpentry = mp[0xB002][entrynum * 16:(entrynum + 1) * 16] unpackedentry = unpack_from(
unpackedentry = unpack('{0}LLLHH'.format(endianness), rawmpentry) '{0}LLLHH'.format(endianness), rawmpentries, entrynum * 16)
labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1',
'EntryNo2') 'EntryNo2')
mpentry = dict(zip(labels, unpackedentry)) mpentry = dict(zip(labels, unpackedentry))

View File

@ -27,7 +27,6 @@ __version__ = "0.1"
# helpers # helpers
i16 = _binary.i16le i16 = _binary.i16le
i32 = _binary.i32le
## ##

View File

@ -24,7 +24,6 @@ __version__ = "0.2"
i8 = _binary.i8 i8 = _binary.i8
i16 = _binary.i16be i16 = _binary.i16be
i32 = _binary.i32be
def _accept(prefix): def _accept(prefix):

View File

@ -21,7 +21,6 @@ from PIL import Image, ImageFile, ImagePalette, _binary
__version__ = "0.3" __version__ = "0.3"
i16 = _binary.i16be
i32 = _binary.i32be i32 = _binary.i32be

View File

@ -28,7 +28,6 @@ __version__ = "0.3"
i8 = _binary.i8 i8 = _binary.i8
i16 = _binary.i16le i16 = _binary.i16le
i32 = _binary.i32le
MODES = { MODES = {

File diff suppressed because it is too large Load Diff

View File

@ -17,291 +17,299 @@
# well-known TIFF tags. # well-known TIFF tags.
## ##
from collections import namedtuple
class TagInfo(namedtuple("_TagInfo", "value name type length enum")):
__slots__ = []
def __new__(cls, value=None, name="unknown", type=4, length=0, enum=None):
return super(TagInfo, cls).__new__(
cls, value, name, type, length, enum or {})
def cvt_enum(self, value):
return self.enum.get(value, value)
## ##
# Map tag numbers (or tag number, tag value tuples) to tag names. # Map tag numbers to tag info.
#
# id: (Name, Type, Length, enum_values)
#
TAGS_V2 = {
TAGS = { 254: ("NewSubfileType", 4, 1),
255: ("SubfileType", 3, 1),
256: ("ImageWidth", 4, 1),
257: ("ImageLength", 4, 1),
258: ("BitsPerSample", 3, 0),
259: ("Compression", 3, 1,
{"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, "Group 4 Fax": 4,
"LZW": 5, "JPEG": 6, "PackBits": 32773}),
254: "NewSubfileType", 262: ("PhotometricInterpretation", 3, 1,
255: "SubfileType", {"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RBG Palette": 3,
256: "ImageWidth", "Transparency Mask": 4, "CMYK": 5, "YCbCr": 6, "CieLAB": 8,
257: "ImageLength", "CFA": 32803, # TIFF/EP, Adobe DNG
258: "BitsPerSample", "LinearRaw": 32892}), # Adobe DNG
263: ("Thresholding", 3, 1),
264: ("CellWidth", 3, 1),
265: ("CellHeight", 3, 1),
266: ("FillOrder", 3, 1),
269: ("DocumentName", 2, 1),
259: "Compression", 270: ("ImageDescription", 2, 1),
(259, 1): "Uncompressed", 271: ("Make", 2, 1),
(259, 2): "CCITT 1d", 272: ("Model", 2, 1),
(259, 3): "Group 3 Fax", 273: ("StripOffsets", 4, 0),
(259, 4): "Group 4 Fax", 274: ("Orientation", 3, 1),
(259, 5): "LZW", 277: ("SamplesPerPixel", 3, 1),
(259, 6): "JPEG", 278: ("RowsPerStrip", 4, 1),
(259, 32773): "PackBits", 279: ("StripByteCounts", 4, 0),
262: "PhotometricInterpretation", 280: ("MinSampleValue", 4, 0),
(262, 0): "WhiteIsZero", 281: ("MaxSampleValue", 3, 0),
(262, 1): "BlackIsZero", 282: ("XResolution", 5, 1),
(262, 2): "RGB", 283: ("YResolution", 5, 1),
(262, 3): "RGB Palette", 284: ("PlanarConfiguration", 3, 1, {"Contigous": 1, "Separate": 2}),
(262, 4): "Transparency Mask", 285: ("PageName", 2, 1),
(262, 5): "CMYK", 286: ("XPosition", 5, 1),
(262, 6): "YCbCr", 287: ("YPosition", 5, 1),
(262, 8): "CieLAB", 288: ("FreeOffsets", 4, 1),
(262, 32803): "CFA", # TIFF/EP, Adobe DNG 289: ("FreeByteCounts", 4, 1),
(262, 32892): "LinearRaw", # Adobe DNG
263: "Thresholding", 290: ("GrayResponseUnit", 3, 1),
264: "CellWidth", 291: ("GrayResponseCurve", 3, 0),
265: "CellHeight", 292: ("T4Options", 4, 1),
266: "FillOrder", 293: ("T6Options", 4, 1),
269: "DocumentName", 296: ("ResolutionUnit", 3, 1, {"inch": 1, "cm": 2}),
297: ("PageNumber", 3, 2),
270: "ImageDescription", 301: ("TransferFunction", 3, 0),
271: "Make", 305: ("Software", 2, 1),
272: "Model", 306: ("DateTime", 2, 1),
273: "StripOffsets",
274: "Orientation",
277: "SamplesPerPixel",
278: "RowsPerStrip",
279: "StripByteCounts",
280: "MinSampleValue", 315: ("Artist", 2, 1),
281: "MaxSampleValue", 316: ("HostComputer", 2, 1),
282: "XResolution", 317: ("Predictor", 3, 1),
283: "YResolution", 318: ("WhitePoint", 5, 2),
284: "PlanarConfiguration", 319: ("PrimaryChromaticies", 3, 6),
(284, 1): "Contigous",
(284, 2): "Separate",
285: "PageName", 320: ("ColorMap", 3, 0),
286: "XPosition", 321: ("HalftoneHints", 3, 2),
287: "YPosition", 322: ("TileWidth", 4, 1),
288: "FreeOffsets", 323: ("TileLength", 4, 1),
289: "FreeByteCounts", 324: ("TileOffsets", 4, 0),
325: ("TileByteCounts", 4, 0),
290: "GrayResponseUnit", 332: ("InkSet", 3, 1),
291: "GrayResponseCurve", 333: ("InkNames", 2, 1),
292: "T4Options", 334: ("NumberOfInks", 3, 1),
293: "T6Options", 336: ("DotRange", 3, 0),
296: "ResolutionUnit", 337: ("TargetPrinter", 2, 1),
297: "PageNumber", 338: ("ExtraSamples", 1, 0),
339: ("SampleFormat", 3, 0),
301: "TransferFunction", 340: ("SMinSampleValue", 12, 0),
305: "Software", 341: ("SMaxSampleValue", 12, 0),
306: "DateTime", 342: ("TransferRange", 3, 6),
315: "Artist",
316: "HostComputer",
317: "Predictor",
318: "WhitePoint",
319: "PrimaryChromaticies",
320: "ColorMap",
321: "HalftoneHints",
322: "TileWidth",
323: "TileLength",
324: "TileOffsets",
325: "TileByteCounts",
332: "InkSet",
333: "InkNames",
334: "NumberOfInks",
336: "DotRange",
337: "TargetPrinter",
338: "ExtraSamples",
339: "SampleFormat",
340: "SMinSampleValue",
341: "SMaxSampleValue",
342: "TransferRange",
347: "JPEGTables",
# obsolete JPEG tags # obsolete JPEG tags
512: "JPEGProc", 512: ("JPEGProc", 3, 1),
513: "JPEGInterchangeFormat", 513: ("JPEGInterchangeFormat", 4, 1),
514: "JPEGInterchangeFormatLength", 514: ("JPEGInterchangeFormatLength", 4, 1),
515: "JPEGRestartInterval", 515: ("JPEGRestartInterval", 3, 1),
517: "JPEGLosslessPredictors", 517: ("JPEGLosslessPredictors", 3, 0),
518: "JPEGPointTransforms", 518: ("JPEGPointTransforms", 3, 0),
519: "JPEGQTables", 519: ("JPEGQTables", 4, 0),
520: "JPEGDCTables", 520: ("JPEGDCTables", 4, 0),
521: "JPEGACTables", 521: ("JPEGACTables", 4, 0),
529: "YCbCrCoefficients", 529: ("YCbCrCoefficients", 5, 3),
530: "YCbCrSubSampling", 530: ("YCbCrSubSampling", 3, 2),
531: "YCbCrPositioning", 531: ("YCbCrPositioning", 3, 1),
532: "ReferenceBlackWhite", 532: ("ReferenceBlackWhite", 4, 0),
# XMP 33432: ("Copyright", 2, 1),
700: "XMP",
33432: "Copyright", # FIXME add more tags here
34665: ("ExifIFD", 3, 1),
# various extensions (should check specs for "official" names) # MPInfo
33723: "IptcNaaInfo", 45056: ("MPFVersion", 7, 1),
34377: "PhotoshopInfo", 45057: ("NumberOfImages", 4, 1),
45058: ("MPEntry", 7, 1),
45059: ("ImageUIDList", 7, 0),
45060: ("TotalFrames", 4, 1),
45313: ("MPIndividualNum", 4, 1),
45569: ("PanOrientation", 4, 1),
45570: ("PanOverlap_H", 5, 1),
45571: ("PanOverlap_V", 5, 1),
45572: ("BaseViewpointNum", 4, 1),
45573: ("ConvergenceAngle", 10, 1),
45574: ("BaselineLength", 5, 1),
45575: ("VerticalDivergence", 10, 1),
45576: ("AxisDistance_X", 10, 1),
45577: ("AxisDistance_Y", 10, 1),
45578: ("AxisDistance_Z", 10, 1),
45579: ("YawAngle", 10, 1),
45580: ("PitchAngle", 10, 1),
45581: ("RollAngle", 10, 1),
# Exif IFD 50741: ("MakerNoteSafety", 3, 1, {"Unsafe": 0, "Safe": 1}),
34665: "ExifIFD", 50780: ("BestQualityScale", 5, 1),
50838: ("ImageJMetaDataByteCounts", 4, 1),
# ICC Profile 50839: ("ImageJMetaData", 7, 1)
34675: "ICCProfile",
# Additional Exif Info
33434: "ExposureTime",
33437: "FNumber",
34850: "ExposureProgram",
34852: "SpectralSensitivity",
34853: "GPSInfoIFD",
34855: "ISOSpeedRatings",
34856: "OECF",
34864: "SensitivityType",
34865: "StandardOutputSensitivity",
34866: "RecommendedExposureIndex",
34867: "ISOSpeed",
34868: "ISOSpeedLatitudeyyy",
34869: "ISOSpeedLatitudezzz",
36864: "ExifVersion",
36867: "DateTimeOriginal",
36868: "DateTImeDigitized",
37121: "ComponentsConfiguration",
37122: "CompressedBitsPerPixel",
37377: "ShutterSpeedValue",
37378: "ApertureValue",
37379: "BrightnessValue",
37380: "ExposureBiasValue",
37381: "MaxApertureValue",
37382: "SubjectDistance",
37383: "MeteringMode",
37384: "LightSource",
37385: "Flash",
37386: "FocalLength",
37396: "SubjectArea",
37500: "MakerNote",
37510: "UserComment",
37520: "SubSec",
37521: "SubSecTimeOriginal",
37522: "SubsecTimeDigitized",
40960: "FlashPixVersion",
40961: "ColorSpace",
40962: "PixelXDimension",
40963: "PixelYDimension",
40964: "RelatedSoundFile",
40965: "InteroperabilityIFD",
41483: "FlashEnergy",
41484: "SpatialFrequencyResponse",
41486: "FocalPlaneXResolution",
41487: "FocalPlaneYResolution",
41488: "FocalPlaneResolutionUnit",
41492: "SubjectLocation",
41493: "ExposureIndex",
41495: "SensingMethod",
41728: "FileSource",
41729: "SceneType",
41730: "CFAPattern",
41985: "CustomRendered",
41986: "ExposureMode",
41987: "WhiteBalance",
41988: "DigitalZoomRatio",
41989: "FocalLengthIn35mmFilm",
41990: "SceneCaptureType",
41991: "GainControl",
41992: "Contrast",
41993: "Saturation",
41994: "Sharpness",
41995: "DeviceSettingDescription",
41996: "SubjectDistanceRange",
42016: "ImageUniqueID",
42032: "CameraOwnerName",
42033: "BodySerialNumber",
42034: "LensSpecification",
42035: "LensMake",
42036: "LensModel",
42037: "LensSerialNumber",
42240: "Gamma",
# MP Info
45056: "MPFVersion",
45057: "NumberOfImages",
45058: "MPEntry",
45059: "ImageUIDList",
45060: "TotalFrames",
45313: "MPIndividualNum",
45569: "PanOrientation",
45570: "PanOverlap_H",
45571: "PanOverlap_V",
45572: "BaseViewpointNum",
45573: "ConvergenceAngle",
45574: "BaselineLength",
45575: "VerticalDivergence",
45576: "AxisDistance_X",
45577: "AxisDistance_Y",
45578: "AxisDistance_Z",
45579: "YawAngle",
45580: "PitchAngle",
45581: "RollAngle",
# Adobe DNG
50706: "DNGVersion",
50707: "DNGBackwardVersion",
50708: "UniqueCameraModel",
50709: "LocalizedCameraModel",
50710: "CFAPlaneColor",
50711: "CFALayout",
50712: "LinearizationTable",
50713: "BlackLevelRepeatDim",
50714: "BlackLevel",
50715: "BlackLevelDeltaH",
50716: "BlackLevelDeltaV",
50717: "WhiteLevel",
50718: "DefaultScale",
50719: "DefaultCropOrigin",
50720: "DefaultCropSize",
50778: "CalibrationIlluminant1",
50779: "CalibrationIlluminant2",
50721: "ColorMatrix1",
50722: "ColorMatrix2",
50723: "CameraCalibration1",
50724: "CameraCalibration2",
50725: "ReductionMatrix1",
50726: "ReductionMatrix2",
50727: "AnalogBalance",
50728: "AsShotNeutral",
50729: "AsShotWhiteXY",
50730: "BaselineExposure",
50731: "BaselineNoise",
50732: "BaselineSharpness",
50733: "BayerGreenSplit",
50734: "LinearResponseLimit",
50735: "CameraSerialNumber",
50736: "LensInfo",
50737: "ChromaBlurRadius",
50738: "AntiAliasStrength",
50740: "DNGPrivateData",
50741: "MakerNoteSafety",
50780: "BestQualityScale",
# ImageJ
50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe
50839: "ImageJMetaData", # private tag registered with Adobe
} }
# Legacy Tags structure
# these tags aren't included above, but were in the previous versions
TAGS = {347: 'JPEGTables',
700: 'XMP',
# Additional Exif Info
33434: 'ExposureTime',
33437: 'FNumber',
33723: 'IptcNaaInfo',
34377: 'PhotoshopInfo',
34675: 'ICCProfile',
34850: 'ExposureProgram',
34852: 'SpectralSensitivity',
34853: 'GPSInfoIFD',
34855: 'ISOSpeedRatings',
34856: 'OECF',
34864: 'SensitivityType',
34865: 'StandardOutputSensitivity',
34866: 'RecommendedExposureIndex',
34867: 'ISOSpeed',
34868: 'ISOSpeedLatitudeyyy',
34869: 'ISOSpeedLatitudezzz',
36864: 'ExifVersion',
36867: 'DateTimeOriginal',
36868: 'DateTImeDigitized',
37121: 'ComponentsConfiguration',
37122: 'CompressedBitsPerPixel',
37377: 'ShutterSpeedValue',
37378: 'ApertureValue',
37379: 'BrightnessValue',
37380: 'ExposureBiasValue',
37381: 'MaxApertureValue',
37382: 'SubjectDistance',
37383: 'MeteringMode',
37384: 'LightSource',
37385: 'Flash',
37386: 'FocalLength',
37396: 'SubjectArea',
37500: 'MakerNote',
37510: 'UserComment',
37520: 'SubSec',
37521: 'SubSecTimeOriginal',
37522: 'SubsecTimeDigitized',
40960: 'FlashPixVersion',
40961: 'ColorSpace',
40962: 'PixelXDimension',
40963: 'PixelYDimension',
40964: 'RelatedSoundFile',
40965: 'InteroperabilityIFD',
41483: 'FlashEnergy',
41484: 'SpatialFrequencyResponse',
41486: 'FocalPlaneXResolution',
41487: 'FocalPlaneYResolution',
41488: 'FocalPlaneResolutionUnit',
41492: 'SubjectLocation',
41493: 'ExposureIndex',
41495: 'SensingMethod',
41728: 'FileSource',
41729: 'SceneType',
41730: 'CFAPattern',
41985: 'CustomRendered',
41986: 'ExposureMode',
41987: 'WhiteBalance',
41988: 'DigitalZoomRatio',
41989: 'FocalLengthIn35mmFilm',
41990: 'SceneCaptureType',
41991: 'GainControl',
41992: 'Contrast',
41993: 'Saturation',
41994: 'Sharpness',
41995: 'DeviceSettingDescription',
41996: 'SubjectDistanceRange',
42016: 'ImageUniqueID',
42032: 'CameraOwnerName',
42033: 'BodySerialNumber',
42034: 'LensSpecification',
42035: 'LensMake',
42036: 'LensModel',
42037: 'LensSerialNumber',
42240: 'Gamma',
# Adobe DNG
50706: 'DNGVersion',
50707: 'DNGBackwardVersion',
50708: 'UniqueCameraModel',
50709: 'LocalizedCameraModel',
50710: 'CFAPlaneColor',
50711: 'CFALayout',
50712: 'LinearizationTable',
50713: 'BlackLevelRepeatDim',
50714: 'BlackLevel',
50715: 'BlackLevelDeltaH',
50716: 'BlackLevelDeltaV',
50717: 'WhiteLevel',
50718: 'DefaultScale',
50719: 'DefaultCropOrigin',
50720: 'DefaultCropSize',
50721: 'ColorMatrix1',
50722: 'ColorMatrix2',
50723: 'CameraCalibration1',
50724: 'CameraCalibration2',
50725: 'ReductionMatrix1',
50726: 'ReductionMatrix2',
50727: 'AnalogBalance',
50728: 'AsShotNeutral',
50729: 'AsShotWhiteXY',
50730: 'BaselineExposure',
50731: 'BaselineNoise',
50732: 'BaselineSharpness',
50733: 'BayerGreenSplit',
50734: 'LinearResponseLimit',
50735: 'CameraSerialNumber',
50736: 'LensInfo',
50737: 'ChromaBlurRadius',
50738: 'AntiAliasStrength',
50740: 'DNGPrivateData',
50778: 'CalibrationIlluminant1',
50779: 'CalibrationIlluminant2',
}
def _populate():
for k, v in TAGS_V2.items():
# Populate legacy structure.
TAGS[k] = v[0]
if len(v) == 4:
for sk, sv in v[3].items():
TAGS[(k, sv)] = sk
TAGS_V2[k] = TagInfo(k, *v)
_populate()
## ##
# Map type numbers to type names. # Map type numbers to type names -- defined in ImageFileDirectory.
TYPES = { TYPES = {}
1: "byte", # was:
2: "ascii", # TYPES = {
3: "short", # 1: "byte",
4: "long", # 2: "ascii",
5: "rational", # 3: "short",
6: "signed byte", # 4: "long",
7: "undefined", # 5: "rational",
8: "signed short", # 6: "signed byte",
9: "signed long", # 7: "undefined",
10: "signed rational", # 8: "signed short",
11: "float", # 9: "signed long",
12: "double", # 10: "signed rational",
# 11: "float",
# 12: "double",
# }
}

View File

@ -43,11 +43,6 @@ class PillowTestCase(unittest.TestCase):
else: else:
print("=== orphaned temp file: %s" % path) print("=== orphaned temp file: %s" % path)
def assert_almost_equal(self, a, b, msg=None, eps=1e-6):
self.assertLess(
abs(a-b), eps,
msg or "got %r, expected %r" % (a, b))
def assert_deep_equal(self, a, b, msg=None): def assert_deep_equal(self, a, b, msg=None):
try: try:
self.assertEqual( self.assertEqual(

View File

@ -80,7 +80,7 @@ class TestImage(PillowTestCase):
ret = GimpGradientFile.sphere_increasing(middle, pos) ret = GimpGradientFile.sphere_increasing(middle, pos)
# Assert # Assert
self.assert_almost_equal(ret, 0.9682458365518543) self.assertAlmostEqual(ret, 0.9682458365518543)
def test_sphere_decreasing(self): def test_sphere_decreasing(self):
# Arrange # Arrange

View File

@ -370,7 +370,8 @@ class TestFileJpeg(PillowTestCase):
# Act # Act
# Shouldn't raise error # Shouldn't raise error
im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg") fn = "Tests/images/sugarshack_bad_mpo_header.jpg"
im = self.assert_warning(UserWarning, lambda: Image.open(fn))
# Assert # Assert
self.assertEqual(im.format, "JPEG") self.assertEqual(im.format, "JPEG")

View File

@ -1,8 +1,10 @@
from __future__ import print_function from __future__ import print_function
from helper import unittest, PillowTestCase, hopper, py3 from helper import unittest, PillowTestCase, hopper, py3
from ctypes import c_float
import io import io
import logging import logging
import itertools
import os import os
from PIL import Image, TiffImagePlugin from PIL import Image, TiffImagePlugin
@ -123,43 +125,45 @@ class TestFileLibTiff(LibTiffTestCase):
def test_write_metadata(self): def test_write_metadata(self):
""" Test metadata writing through libtiff """ """ Test metadata writing through libtiff """
img = Image.open('Tests/images/hopper_g4.tif') for legacy_api in [False, True]:
f = self.tempfile('temp.tiff') img = Image.open('Tests/images/hopper_g4.tif')
f = self.tempfile('temp.tiff')
img.save(f, tiffinfo=img.tag) img.save(f, tiffinfo=img.tag)
loaded = Image.open(f) if legacy_api:
original = img.tag.named()
else:
original = img.tag_v2.named()
original = img.tag.named() # PhotometricInterpretation is set from SAVE_INFO,
reloaded = loaded.tag.named() # not the original image.
ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber',
'PhotometricInterpretation']
# PhotometricInterpretation is set from SAVE_INFO, loaded = Image.open(f)
# not the original image. if legacy_api:
ignored = [ reloaded = loaded.tag.named()
'StripByteCounts', 'RowsPerStrip', else:
'PageNumber', 'PhotometricInterpretation'] reloaded = loaded.tag_v2.named()
for tag, value in reloaded.items(): for tag, value in itertools.chain(reloaded.items(),
if tag not in ignored: original.items()):
if tag.endswith('Resolution'): if tag not in ignored:
val = original[tag] val = original[tag]
self.assert_almost_equal( if tag.endswith('Resolution'):
val[0][0]/val[0][1], value[0][0]/value[0][1], if legacy_api:
msg="%s didn't roundtrip" % tag) self.assertEqual(
else: c_float(val[0][0] / val[0][1]).value,
self.assertEqual( c_float(value[0][0] / value[0][1]).value,
original[tag], value, "%s didn't roundtrip" % tag) msg="%s didn't roundtrip" % tag)
else:
for tag, value in original.items(): self.assertEqual(
if tag not in ignored: c_float(val).value, c_float(value).value,
if tag.endswith('Resolution'): msg="%s didn't roundtrip" % tag)
val = reloaded[tag] else:
self.assert_almost_equal( self.assertEqual(
val[0][0]/val[0][1], value[0][0]/value[0][1], val, value, msg="%s didn't roundtrip" % tag)
msg="%s didn't roundtrip" % tag)
else:
self.assertEqual(
value, reloaded[tag], "%s didn't roundtrip" % tag)
def test_g3_compression(self): def test_g3_compression(self):
i = Image.open('Tests/images/hopper_g4_500.tif') i = Image.open('Tests/images/hopper_g4_500.tif')
@ -228,7 +232,8 @@ class TestFileLibTiff(LibTiffTestCase):
orig.save(out) orig.save(out)
reread = Image.open(out) reread = Image.open(out)
self.assertEqual('temp.tif', reread.tag[269]) self.assertEqual('temp.tif', reread.tag_v2[269])
self.assertEqual('temp.tif', reread.tag[269][0])
def test_12bit_rawmode(self): def test_12bit_rawmode(self):
""" Are we generating the same interpretation """ Are we generating the same interpretation

View File

@ -74,14 +74,28 @@ class TestFileTiff(PillowTestCase):
from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION
filename = "Tests/images/pil168.tif" filename = "Tests/images/pil168.tif"
im = Image.open(filename) im = Image.open(filename)
assert isinstance(im.tag.tags[X_RESOLUTION][0], tuple)
assert isinstance(im.tag.tags[Y_RESOLUTION][0], tuple) #legacy api
# Try to read a file where X,Y_RESOLUTION are ints self.assert_(isinstance(im.tag[X_RESOLUTION][0], tuple))
im.tag.tags[X_RESOLUTION] = (72,) self.assert_(isinstance(im.tag[Y_RESOLUTION][0], tuple))
im.tag.tags[Y_RESOLUTION] = (72,)
im._setup() #v2 api
self.assert_(isinstance(im.tag_v2[X_RESOLUTION], float))
self.assert_(isinstance(im.tag_v2[Y_RESOLUTION], float))
self.assertEqual(im.info['dpi'], (72., 72.)) self.assertEqual(im.info['dpi'], (72., 72.))
def test_int_resolution(self):
from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION
filename = "Tests/images/pil168.tif"
im = Image.open(filename)
# Try to read a file where X,Y_RESOLUTION are ints
im.tag_v2[X_RESOLUTION] = 71
im.tag_v2[Y_RESOLUTION] = 71
im._setup()
self.assertEqual(im.info['dpi'], (71., 71.))
def test_invalid_file(self): def test_invalid_file(self):
invalid_file = "Tests/images/flower.jpg" invalid_file = "Tests/images/flower.jpg"
@ -89,8 +103,9 @@ class TestFileTiff(PillowTestCase):
lambda: TiffImagePlugin.TiffImageFile(invalid_file)) lambda: TiffImagePlugin.TiffImageFile(invalid_file))
def test_bad_exif(self): def test_bad_exif(self):
i = Image.open('Tests/images/hopper_bad_exif.jpg')
try: try:
Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() self.assert_warning(UserWarning, lambda: i._getexif())
except struct.error: except struct.error:
self.fail( self.fail(
"Bad EXIF data passed incorrect values to _binary unpack") "Bad EXIF data passed incorrect values to _binary unpack")
@ -98,7 +113,6 @@ class TestFileTiff(PillowTestCase):
def test_save_unsupported_mode(self): def test_save_unsupported_mode(self):
im = hopper("HSV") im = hopper("HSV")
outfile = self.tempfile("temp.tif") outfile = self.tempfile("temp.tif")
self.assertRaises(IOError, lambda: im.save(outfile)) self.assertRaises(IOError, lambda: im.save(outfile))
def test_little_endian(self): def test_little_endian(self):
@ -206,7 +220,6 @@ class TestFileTiff(PillowTestCase):
self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255))
def test___str__(self): def test___str__(self):
# Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
im = Image.open(filename) im = Image.open(filename)
@ -220,138 +233,82 @@ class TestFileTiff(PillowTestCase):
# Arrange # Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
im = Image.open(filename) im = Image.open(filename)
# v2 interface
# Act
ret = im.ifd.as_dict()
# Assert
self.assertIsInstance(ret, dict)
self.assertEqual( self.assertEqual(
ret, {256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,), im.tag_v2.as_dict(),
262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), {256: 55, 257: 43, 258: (8, 8, 8, 8), 259: 1,
279: (9460,), 282: ((720000, 10000),), 262: 2, 296: 2, 273: (8,), 338: (1,), 277: 4,
283: ((720000, 10000),), 284: (1,)}) 279: (9460,), 282: 72.0, 283: 72.0, 284: 1})
# legacy interface
self.assertEqual(
im.tag.as_dict(),
{256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,),
262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,),
279: (9460,), 282: ((720000, 10000),),
283: ((720000, 10000),), 284: (1,)})
def test__delitem__(self): def test__delitem__(self):
# Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
im = Image.open(filename) im = Image.open(filename)
len_before = len(im.ifd.as_dict()) len_before = len(im.ifd.as_dict())
# Act
del im.ifd[256] del im.ifd[256]
# Assert
len_after = len(im.ifd.as_dict()) len_after = len(im.ifd.as_dict())
self.assertEqual(len_before, len_after + 1) self.assertEqual(len_before, len_after + 1)
def test_load_byte(self): def test_load_byte(self):
# Arrange for legacy_api in [False, True]:
ifd = TiffImagePlugin.ImageFileDirectory() ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abc" data = b"abc"
ret = ifd.load_byte(data, legacy_api)
# Act self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99))
ret = ifd.load_byte(data)
# Assert
self.assertEqual(ret, b"abc")
def test_load_string(self): def test_load_string(self):
# Arrange ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd = TiffImagePlugin.ImageFileDirectory()
data = b"abc\0" data = b"abc\0"
ret = ifd.load_string(data, False)
# Act
ret = ifd.load_string(data)
# Assert
self.assertEqual(ret, "abc") self.assertEqual(ret, "abc")
def test_load_float(self): def test_load_float(self):
# Arrange ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd = TiffImagePlugin.ImageFileDirectory()
data = b"abcdabcd" data = b"abcdabcd"
ret = ifd.load_float(data, False)
# Act
ret = ifd.load_float(data)
# Assert
self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22))
def test_load_double(self): def test_load_double(self):
# Arrange ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd = TiffImagePlugin.ImageFileDirectory()
data = b"abcdefghabcdefgh" data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False)
# Act
ret = ifd.load_double(data)
# Assert
self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194))
def test_seek(self): def test_seek(self):
# Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
im = Image.open(filename) im = Image.open(filename)
# Act
im.seek(-1) im.seek(-1)
# Assert
self.assertEqual(im.tell(), 0) self.assertEqual(im.tell(), 0)
def test_seek_eof(self): def test_seek_eof(self):
# Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
im = Image.open(filename) im = Image.open(filename)
self.assertEqual(im.tell(), 0) self.assertEqual(im.tell(), 0)
# Act / Assert
self.assertRaises(EOFError, lambda: im.seek(1)) self.assertRaises(EOFError, lambda: im.seek(1))
def test__cvt_res_int(self): def test__limit_rational_int(self):
# Arrange from PIL.TiffImagePlugin import _limit_rational
from PIL.TiffImagePlugin import _cvt_res
value = 34 value = 34
ret = _limit_rational(value, 65536)
# Act
ret = _cvt_res(value)
# Assert
self.assertEqual(ret, (34, 1)) self.assertEqual(ret, (34, 1))
def test__cvt_res_float(self): def test__limit_rational_float(self):
# Arrange from PIL.TiffImagePlugin import _limit_rational
from PIL.TiffImagePlugin import _cvt_res
value = 22.3 value = 22.3
ret = _limit_rational(value, 65536)
# Act self.assertEqual(ret, (223, 10))
ret = _cvt_res(value)
# Assert
self.assertEqual(ret, (1461452, 65536))
def test__cvt_res_sequence(self):
# Arrange
from PIL.TiffImagePlugin import _cvt_res
value = [0, 1]
# Act
ret = _cvt_res(value)
# Assert
self.assertEqual(ret, [0, 1])
def test_4bit(self): def test_4bit(self):
# Arrange
test_file = "Tests/images/hopper_gray_4bpp.tif" test_file = "Tests/images/hopper_gray_4bpp.tif"
original = hopper("L") original = hopper("L")
# Act
im = Image.open(test_file) im = Image.open(test_file)
# Assert
self.assertEqual(im.size, (128, 128)) self.assertEqual(im.size, (128, 128))
self.assertEqual(im.mode, "L") self.assertEqual(im.mode, "L")
self.assert_image_similar(im, original, 7.3) self.assert_image_similar(im, original, 7.3)
@ -361,52 +318,50 @@ class TestFileTiff(PillowTestCase):
# Test TIFF with tag 297 (Page Number) having value of 0 0. # Test TIFF with tag 297 (Page Number) having value of 0 0.
# The first number is the current page number. # The first number is the current page number.
# The second is the total number of pages, zero means not available. # The second is the total number of pages, zero means not available.
# Arrange
outfile = self.tempfile("temp.tif") outfile = self.tempfile("temp.tif")
# Created by printing a page in Chrome to PDF, then: # Created by printing a page in Chrome to PDF, then:
# /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif
# -dNOPAUSE /tmp/test.pdf -c quit # -dNOPAUSE /tmp/test.pdf -c quit
infile = "Tests/images/total-pages-zero.tif" infile = "Tests/images/total-pages-zero.tif"
im = Image.open(infile) im = Image.open(infile)
# Act / Assert
# Should not divide by zero # Should not divide by zero
im.save(outfile) im.save(outfile)
def test_with_underscores(self): def test_with_underscores(self):
# Arrange: use underscores
kwargs = {'resolution_unit': 'inch', kwargs = {'resolution_unit': 'inch',
'x_resolution': 72, 'x_resolution': 72,
'y_resolution': 36} 'y_resolution': 36}
filename = self.tempfile("temp.tif") filename = self.tempfile("temp.tif")
# Act
hopper("RGB").save(filename, **kwargs) hopper("RGB").save(filename, **kwargs)
# Assert
from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION
im = Image.open(filename) im = Image.open(filename)
self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 72)
self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 36) # legacy interface
self.assertEqual(im.tag[X_RESOLUTION][0][0], 72)
self.assertEqual(im.tag[Y_RESOLUTION][0][0], 36)
# v2 interface
self.assertEqual(im.tag_v2[X_RESOLUTION], 72)
self.assertEqual(im.tag_v2[Y_RESOLUTION], 36)
def test_deprecation_warning_with_spaces(self): def test_deprecation_warning_with_spaces(self):
# Arrange: use spaces
kwargs = {'resolution unit': 'inch', kwargs = {'resolution unit': 'inch',
'x resolution': 36, 'x resolution': 36,
'y resolution': 72} 'y resolution': 72}
filename = self.tempfile("temp.tif") filename = self.tempfile("temp.tif")
# Act
self.assert_warning(DeprecationWarning, self.assert_warning(DeprecationWarning,
lambda: hopper("RGB").save(filename, **kwargs)) lambda: hopper("RGB").save(filename, **kwargs))
# Assert
from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION
im = Image.open(filename) im = Image.open(filename)
self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 36)
self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 72) # legacy interface
self.assertEqual(im.tag[X_RESOLUTION][0][0], 36)
self.assertEqual(im.tag[Y_RESOLUTION][0][0], 72)
# v2 interface
self.assertEqual(im.tag_v2[X_RESOLUTION], 36)
self.assertEqual(im.tag_v2[Y_RESOLUTION], 72)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,8 +1,13 @@
from __future__ import division
import io
import struct
from helper import unittest, PillowTestCase, hopper from helper import unittest, PillowTestCase, hopper
from PIL import Image, TiffImagePlugin, TiffTags from PIL import Image, TiffImagePlugin, TiffTags
tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS_V2.values())
class TestFileTiffMetadata(PillowTestCase): class TestFileTiffMetadata(PillowTestCase):
@ -15,62 +20,95 @@ class TestFileTiffMetadata(PillowTestCase):
img = hopper() img = hopper()
# Behaviour change: re #1416
# Pre ifd rewrite, ImageJMetaData was being written as a string(2),
# Post ifd rewrite, it's defined as arbitrary bytes(7). It should
# roundtrip with the actual bytes, rather than stripped text
# of the premerge tests.
#
# For text items, we still have to decode('ascii','replace') because
# the tiff file format can't take 8 bit bytes in that field.
basetextdata = "This is some arbitrary metadata for a text field" basetextdata = "This is some arbitrary metadata for a text field"
textdata = basetextdata + " \xff" bindata = basetextdata.encode('ascii') + b" \xff"
textdata = basetextdata + " " + chr(255)
reloaded_textdata = basetextdata + " ?"
floatdata = 12.345 floatdata = 12.345
doubledata = 67.89 doubledata = 67.89
info = TiffImagePlugin.ImageFileDirectory() info = TiffImagePlugin.ImageFileDirectory()
info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) ImageJMetaData = tag_ids['ImageJMetaData']
info[tag_ids['ImageJMetaData']] = textdata ImageJMetaDataByteCounts = tag_ids['ImageJMetaDataByteCounts']
ImageDescription = tag_ids['ImageDescription']
info[ImageJMetaDataByteCounts] = len(bindata)
info[ImageJMetaData] = bindata
info[tag_ids['RollAngle']] = floatdata info[tag_ids['RollAngle']] = floatdata
info.tagtype[tag_ids['RollAngle']] = 11 info.tagtype[tag_ids['RollAngle']] = 11
info[tag_ids['YawAngle']] = doubledata info[tag_ids['YawAngle']] = doubledata
info.tagtype[tag_ids['YawAngle']] = 12 info.tagtype[tag_ids['YawAngle']] = 12
info[ImageDescription] = textdata
f = self.tempfile("temp.tif") f = self.tempfile("temp.tif")
img.save(f, tiffinfo=info) img.save(f, tiffinfo=info)
loaded = Image.open(f) loaded = Image.open(f)
self.assertEqual(loaded.tag[50838], (len(basetextdata + " ?"),)) self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),))
self.assertEqual(loaded.tag[50839], basetextdata + " ?") self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], len(bindata))
self.assertAlmostEqual(loaded.tag[tag_ids['RollAngle']][0], floatdata,
places=5) self.assertEqual(loaded.tag[ImageJMetaData], bindata)
self.assertAlmostEqual(loaded.tag[tag_ids['YawAngle']][0], doubledata) self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata)
self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,))
self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata)
loaded_float = loaded.tag[tag_ids['RollAngle']][0]
self.assertAlmostEqual(loaded_float, floatdata, places=5)
loaded_double = loaded.tag[tag_ids['YawAngle']][0]
self.assertAlmostEqual(loaded_double, doubledata)
def test_read_metadata(self): def test_read_metadata(self):
img = Image.open('Tests/images/hopper_g4.tif') img = Image.open('Tests/images/hopper_g4.tif')
known = {'YResolution': ((4294967295, 113653537),), self.assertEqual({'YResolution': 4294967295 / 113653537,
'PlanarConfiguration': (1,), 'PlanarConfiguration': 1,
'BitsPerSample': (1,), 'BitsPerSample': (1,),
'ImageLength': (128,), 'ImageLength': 128,
'Compression': (4,), 'Compression': 4,
'FillOrder': (1,), 'FillOrder': 1,
'RowsPerStrip': (128,), 'RowsPerStrip': 128,
'ResolutionUnit': (3,), 'ResolutionUnit': 3,
'PhotometricInterpretation': (0,), 'PhotometricInterpretation': 0,
'PageNumber': (0, 1), 'PageNumber': (0, 1),
'XResolution': ((4294967295, 113653537),), 'XResolution': 4294967295 / 113653537,
'ImageWidth': (128,), 'ImageWidth': 128,
'Orientation': (1,), 'Orientation': 1,
'StripByteCounts': (1968,), 'StripByteCounts': (1968,),
'SamplesPerPixel': (1,), 'SamplesPerPixel': 1,
'StripOffsets': (8,), 'StripOffsets': (8,)
} }, img.tag_v2.named())
# self.assertEqual is equivalent, self.assertEqual({'YResolution': ((4294967295, 113653537),),
# but less helpful in telling what's wrong. 'PlanarConfiguration': (1,),
named = img.tag.named() 'BitsPerSample': (1,),
for tag, value in named.items(): 'ImageLength': (128,),
self.assertEqual(known[tag], value) 'Compression': (4,),
'FillOrder': (1,),
for tag, value in known.items(): 'RowsPerStrip': (128,),
self.assertEqual(value, named[tag]) 'ResolutionUnit': (3,),
'PhotometricInterpretation': (0,),
'PageNumber': (0, 1),
'XResolution': ((4294967295, 113653537),),
'ImageWidth': (128,),
'Orientation': (1,),
'StripByteCounts': (1968,),
'SamplesPerPixel': (1,),
'StripOffsets': (8,)
}, img.tag.named())
def test_write_metadata(self): def test_write_metadata(self):
""" Test metadata writing through the python code """ """ Test metadata writing through the python code """
@ -81,8 +119,8 @@ class TestFileTiffMetadata(PillowTestCase):
loaded = Image.open(f) loaded = Image.open(f)
original = img.tag.named() original = img.tag_v2.named()
reloaded = loaded.tag.named() reloaded = loaded.tag_v2.named()
ignored = [ ignored = [
'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] 'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets']
@ -101,6 +139,15 @@ class TestFileTiffMetadata(PillowTestCase):
self.assertEqual(tag_ids['MakerNoteSafety'], 50741) self.assertEqual(tag_ids['MakerNoteSafety'], 50741)
self.assertEqual(tag_ids['BestQualityScale'], 50780) self.assertEqual(tag_ids['BestQualityScale'], 50780)
def test_empty_metadata(self):
f = io.BytesIO(b'II*\x00\x08\x00\x00\x00')
head = f.read(8)
info = TiffImagePlugin.ImageFileDirectory(head)
try:
self.assert_warning(UserWarning, lambda: info.load(f))
except struct.error:
self.fail("Should not be struct errors there.")
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -144,14 +144,6 @@ can be found here.
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`TiffTags` Module
----------------------
.. automodule:: PIL.TiffTags
:members:
:undoc-members:
:show-inheritance:
:mod:`WalImageFile` Module :mod:`WalImageFile` Module
-------------------------- --------------------------

View File

@ -461,17 +461,37 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
**compression** **compression**
Compression mode. Compression mode.
.. versionadded:: 2.0.0
**dpi** **dpi**
Image resolution as an (xdpi, ydpi) tuple, where applicable. You can use Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use
the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed
information about the image resolution. information about the image resolution.
.. versionadded:: 1.1.5 .. versionadded:: 1.1.5
In addition, the :py:attr:`~PIL.Image.Image.tag` attribute contains a **resolution**
dictionary of decoded TIFF fields. Values are stored as either strings or Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a
tuples. Note that only short, long and ASCII tags are correctly unpacked by measurement in whichever unit is specified by the file.
this release.
.. versionadded:: 1.1.5
The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary of
TIFF metadata. The keys are numerical indexes from `~PIL.TiffTags.TAGS_V2`.
Values are strings or numbers for single items, multiple values are returned
in a tuple of values. Rational numbers are returned as a single value.
.. versionadded:: 3.0.0
For compatibility with legacy code, the
:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of
decoded TIFF fields as returned prior to version 3.0.0. Values are
returned as either strings or tuples of numeric values. Rational
numbers are returned as a tuple of ``(numerator, denominator)``.
.. deprecated:: 3.0.0
Saving Tiff Images Saving Tiff Images
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
@ -479,17 +499,23 @@ Saving Tiff Images
The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments:
**tiffinfo** **tiffinfo**
A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` object or dict A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict
object containing tiff tags and values. The TIFF field type is object containing tiff tags and values. The TIFF field type is
autodetected for Numeric and string values, any other types autodetected for Numeric and string values, any other types
require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
object and setting the type in object and setting the type in
:py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory.tagtype` with :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with
the appropriate numerical value from the appropriate numerical value from
``TiffTags.TYPES``. ``TiffTags.TYPES``.
.. versionadded:: 2.3.0 .. versionadded:: 2.3.0
For compatibility with legacy code, a
`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may be passed
in this field. This will be deprecated in a future version.
..versionadded:: 3.0.0
**compression** **compression**
A string containing the desired compression method for the A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression file. (valid only with libtiff installed) Valid compression

View File

@ -26,6 +26,7 @@ Reference
ImageTk ImageTk
ImageWin ImageWin
ExifTags ExifTags
TiffTags
OleFileIO OleFileIO
PSDraw PSDraw
PixelAccess PixelAccess

View File

@ -721,7 +721,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args)
pos = 0; pos = 0;
} }
TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename)); TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename));
encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE)); encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE));
@ -737,11 +736,9 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args)
return NULL; return NULL;
} }
// While fails on 64 bit machines, complains that pos is an int instead of a Py_ssize_t for (pos = 0; pos < d_size; pos++) {
// while (PyDict_Next(dir, &pos, &key, &value)) { key = PyList_GetItem(keys, pos);
for (pos=0;pos<d_size;pos++){ value = PyList_GetItem(values, pos);
key = PyList_GetItem(keys,pos);
value = PyList_GetItem(values,pos);
status = 0; status = 0;
TRACE(("Attempting to set key: %d\n", (int)PyInt_AsLong(key))); TRACE(("Attempting to set key: %d\n", (int)PyInt_AsLong(key)));
if (PyInt_Check(value)) { if (PyInt_Check(value)) {
@ -749,49 +746,53 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args)
status = ImagingLibTiffSetField(&encoder->state, status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key), (ttag_t) PyInt_AsLong(key),
PyInt_AsLong(value)); PyInt_AsLong(value));
} else if(PyBytes_Check(value)) {
TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value)));
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
PyBytes_AsString(value));
} else if(PyList_Check(value)) {
int len,i;
float *floatav;
int *intav;
TRACE(("Setting from List: %d \n", (int)PyInt_AsLong(key)));
len = (int)PyList_Size(value);
if (len) {
if (PyInt_Check(PyList_GetItem(value,0))) {
TRACE((" %d elements, setting as ints \n", len));
intav = malloc(sizeof(int)*len);
if (intav) {
for (i=0;i<len;i++) {
intav[i] = (int)PyInt_AsLong(PyList_GetItem(value,i));
}
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
intav);
free(intav);
}
} else {
TRACE((" %d elements, setting as floats \n", len));
floatav = malloc(sizeof(float)*len);
if (floatav) {
for (i=0;i<len;i++) {
floatav[i] = (float)PyFloat_AsDouble(PyList_GetItem(value,i));
}
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
floatav);
free(floatav);
}
}
}
} else if (PyFloat_Check(value)) { } else if (PyFloat_Check(value)) {
TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value))); TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value)));
status = ImagingLibTiffSetField(&encoder->state, status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key), (ttag_t) PyInt_AsLong(key),
(float)PyFloat_AsDouble(value)); (float)PyFloat_AsDouble(value));
} else if (PyBytes_Check(value)) {
TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value)));
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
PyBytes_AsString(value));
} else if (PyTuple_Check(value)) {
int len,i;
float *floatav;
int *intav;
TRACE(("Setting from Tuple: %d \n", (int)PyInt_AsLong(key)));
len = (int)PyTuple_Size(value);
if (len) {
if (PyInt_Check(PyTuple_GetItem(value,0))) {
TRACE((" %d elements, setting as ints \n", len));
intav = malloc(sizeof(int)*len);
if (intav) {
for (i=0;i<len;i++) {
intav[i] = (int)PyInt_AsLong(PyTuple_GetItem(value,i));
}
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
len, intav);
free(intav);
}
} else if (PyFloat_Check(PyTuple_GetItem(value,0))) {
TRACE((" %d elements, setting as floats \n", len));
floatav = malloc(sizeof(float)*len);
if (floatav) {
for (i=0;i<len;i++) {
floatav[i] = (float)PyFloat_AsDouble(PyTuple_GetItem(value,i));
}
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
len, floatav);
free(floatav);
}
} else {
TRACE(("Unhandled type in tuple for key %d : %s \n",
(int)PyInt_AsLong(key),
PyBytes_AsString(PyObject_Str(value))));
}
}
} else { } else {
TRACE(("Unhandled type for key %d : %s \n", TRACE(("Unhandled type for key %d : %s \n",
(int)PyInt_AsLong(key), (int)PyInt_AsLong(key),