This commit is contained in:
Antony Lee 2015-07-02 02:15:00 +00:00
commit f79388f67c
9 changed files with 668 additions and 956 deletions

View File

@ -37,7 +37,7 @@ __version__ = "0.6"
import array import array
import struct import struct
import io import io
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
@ -393,13 +393,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
@ -413,12 +406,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(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:
file.seek(exif[0x8769]) file.seek(exif[0x8769])
@ -427,8 +418,7 @@ def _getexif(self):
else: else:
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory(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:
file.seek(exif[0x8825]) file.seek(exif[0x8825])
@ -437,9 +427,7 @@ def _getexif(self):
else: else:
info = TiffImagePlugin.ImageFileDirectory(head) info = TiffImagePlugin.ImageFileDirectory(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
@ -457,23 +445,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(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
try:
mpentries = [] mpentries = []
try:
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))

File diff suppressed because it is too large Load Diff

View File

@ -17,291 +17,154 @@
# 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.
TAGS = { TAGS = {
254: "NewSubfileType", 254: ("NewSubfileType", 4, 1),
255: "SubfileType", 255: ("SubfileType", 3, 1),
256: "ImageWidth", 256: ("ImageWidth", 4, 1),
257: "ImageLength", 257: ("ImageLength", 4, 1),
258: "BitsPerSample", 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}),
259: "Compression", 262: ("PhotometricInterpretation", 3, 1,
(259, 1): "Uncompressed", {"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RBG Palette": 3,
(259, 2): "CCITT 1d", "Transparency Mask": 4, "CMYK": 5, "YCbCr": 6, "CieLAB": 8,
(259, 3): "Group 3 Fax", "CFA": 32803, # TIFF/EP, Adobe DNG
(259, 4): "Group 4 Fax", "LinearRaw": 32892}), # Adobe DNG
(259, 5): "LZW", 263: ("Thresholding", 3, 1),
(259, 6): "JPEG", 264: ("CellWidth", 3, 1),
(259, 32773): "PackBits", 265: ("CellHeight", 3, 1),
266: ("FillOrder", 3, 1),
269: ("DocumentName", 2, 1),
262: "PhotometricInterpretation", 270: ("ImageDescription", 2, 1),
(262, 0): "WhiteIsZero", 271: ("Make", 2, 1),
(262, 1): "BlackIsZero", 272: ("Model", 2, 1),
(262, 2): "RGB", 273: ("StripOffsets", 4, 0),
(262, 3): "RGB Palette", 274: ("Orientation", 3, 1),
(262, 4): "Transparency Mask", 277: ("SamplesPerPixel", 3, 1),
(262, 5): "CMYK", 278: ("RowsPerStrip", 4, 1),
(262, 6): "YCbCr", 279: ("StripByteCounts", 4, 0),
(262, 8): "CieLAB",
(262, 32803): "CFA", # TIFF/EP, Adobe DNG
(262, 32892): "LinearRaw", # Adobe DNG
263: "Thresholding", 280: ("MinSampleValue", 4, 0),
264: "CellWidth", 281: ("MaxSampleValue", 3, 0),
265: "CellHeight", 282: ("XResolution", 5, 1),
266: "FillOrder", 283: ("YResolution", 5, 1),
269: "DocumentName", 284: ("PlanarConfiguration", 3, 1, {"Contigous": 1, "Separate": 2}),
285: ("PageName", 2, 1),
286: ("XPosition", 5, 1),
287: ("YPosition", 5, 1),
288: ("FreeOffsets", 4, 1),
289: ("FreeByteCounts", 4, 1),
270: "ImageDescription", 290: ("GrayResponseUnit", 3, 1),
271: "Make", 291: ("GrayResponseCurve", 3, 0),
272: "Model", 292: ("T4Options", 4, 1),
273: "StripOffsets", 293: ("T6Options", 4, 1),
274: "Orientation", 296: ("ResolutionUnit", 3, 1, {"inch": 1, "cm": 2}),
277: "SamplesPerPixel", 297: ("PageNumber", 3, 2),
278: "RowsPerStrip",
279: "StripByteCounts",
280: "MinSampleValue", 301: ("TransferFunction", 3, 0),
281: "MaxSampleValue", 305: ("Software", 2, 1),
282: "XResolution", 306: ("DateTime", 2, 1),
283: "YResolution",
284: "PlanarConfiguration",
(284, 1): "Contigous",
(284, 2): "Separate",
285: "PageName", 315: ("Artist", 2, 1),
286: "XPosition", 316: ("HostComputer", 2, 1),
287: "YPosition", 317: ("Predictor", 3, 1),
288: "FreeOffsets", 318: ("WhitePoint", 5, 2),
289: "FreeByteCounts", 319: ("PrimaryChromaticies", 3, 6),
290: "GrayResponseUnit", 320: ("ColorMap", 3, 0),
291: "GrayResponseCurve", 321: ("HalftoneHints", 3, 2),
292: "T4Options", 322: ("TileWidth", 4, 1),
293: "T6Options", 323: ("TileLength", 4, 1),
296: "ResolutionUnit", 324: ("TileOffsets", 4, 0),
297: "PageNumber", 325: ("TileByteCounts", 4, 0),
301: "TransferFunction", 332: ("InkSet", 3, 1),
305: "Software", 333: ("InkNames", 2, 1),
306: "DateTime", 334: ("NumberOfInks", 3, 1),
336: ("DotRange", 3, 0),
337: ("TargetPrinter", 2, 1),
338: ("ExtraSamples", 1, 0),
339: ("SampleFormat", 3, 0),
315: "Artist", 340: ("SMinSampleValue", 12, 0),
316: "HostComputer", 341: ("SMaxSampleValue", 12, 0),
317: "Predictor", 342: ("TransferRange", 3, 6),
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)
33723: "IptcNaaInfo",
34377: "PhotoshopInfo",
# Exif IFD
34665: "ExifIFD",
# ICC Profile
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",
# MPInfo # MPInfo
45056: "MPFVersion", 45056: ("MPFVersion", 7, 1),
45057: "NumberOfImages", 45057: ("NumberOfImages", 4, 1),
45058: "MPEntry", 45058: ("MPEntry", 7, 1),
45059: "ImageUIDList", 45059: ("ImageUIDList", 7, 0),
45060: "TotalFrames", 45060: ("TotalFrames", 4, 1),
45313: "MPIndividualNum", 45313: ("MPIndividualNum", 4, 1),
45569: "PanOrientation", 45569: ("PanOrientation", 4, 1),
45570: "PanOverlap_H", 45570: ("PanOverlap_H", 5, 1),
45571: "PanOverlap_V", 45571: ("PanOverlap_V", 5, 1),
45572: "BaseViewpointNum", 45572: ("BaseViewpointNum", 4, 1),
45573: "ConvergenceAngle", 45573: ("ConvergenceAngle", 10, 1),
45574: "BaselineLength", 45574: ("BaselineLength", 5, 1),
45575: "VerticalDivergence", 45575: ("VerticalDivergence", 10, 1),
45576: "AxisDistance_X", 45576: ("AxisDistance_X", 10, 1),
45577: "AxisDistance_Y", 45577: ("AxisDistance_Y", 10, 1),
45578: "AxisDistance_Z", 45578: ("AxisDistance_Z", 10, 1),
45579: "YawAngle", 45579: ("YawAngle", 10, 1),
45580: "PitchAngle", 45580: ("PitchAngle", 10, 1),
45581: "RollAngle", 45581: ("RollAngle", 10, 1),
# Adobe DNG 50741: ("MakerNoteSafety", 3, 1, {0: "Unsafe", 1: "Safe"}),
50706: "DNGVersion", 50780: ("BestQualityScale", 5, 1),
50707: "DNGBackwardVersion", 50838: ("ImageJMetaDataByteCounts", 4, 1),
50708: "UniqueCameraModel", 50839: ("ImageJMetaData", 7, 1)
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
} }
for k, v in TAGS.items():
TAGS[k] = TagInfo(k, *v)
del k, v
## ##
# Map type numbers to type names. # Map type numbers to type names -- defined in ImageFileDirectory.
TYPES = { TYPES = {}
1: "byte",
2: "ascii",
3: "short",
4: "long",
5: "rational",
6: "signed byte",
7: "undefined",
8: "signed short",
9: "signed long",
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

@ -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
import os from ctypes import c_float
import io import io
import itertools
import os
from PIL import Image, TiffImagePlugin from PIL import Image, TiffImagePlugin
@ -120,43 +122,40 @@ class TestFileLibTiff(LibTiffTestCase):
def test_write_metadata(self): def test_write_metadata(self):
""" Test metadata writing through libtiff """ """ Test metadata writing through libtiff """
for legacy_api in [False, True]:
img = Image.open('Tests/images/hopper_g4.tif') img = Image.open('Tests/images/hopper_g4.tif')
img.tag.legacy_api = legacy_api
f = self.tempfile('temp.tiff') f = self.tempfile('temp.tiff')
img.save(f, tiffinfo=img.tag) img.save(f, tiffinfo=img.tag)
loaded = Image.open(f)
original = img.tag.named() original = img.tag.named()
reloaded = loaded.tag.named()
# PhotometricInterpretation is set from SAVE_INFO, # PhotometricInterpretation is set from SAVE_INFO,
# not the original image. # not the original image.
ignored = [ ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber',
'StripByteCounts', 'RowsPerStrip', 'PhotometricInterpretation']
'PageNumber', 'PhotometricInterpretation']
for tag, value in reloaded.items(): loaded = Image.open(f)
loaded.tag.legacy_api = legacy_api
reloaded = loaded.tag.named()
for tag, value in itertools.chain(reloaded.items(),
original.items()):
if tag not in ignored: if tag not in ignored:
if tag.endswith('Resolution'):
val = original[tag] val = original[tag]
self.assert_almost_equal(
val[0][0]/val[0][1], value[0][0]/value[0][1],
msg="%s didn't roundtrip" % tag)
else:
self.assertEqual(
original[tag], value, "%s didn't roundtrip" % tag)
for tag, value in original.items():
if tag not in ignored:
if tag.endswith('Resolution'): if tag.endswith('Resolution'):
val = reloaded[tag] if legacy_api:
self.assert_almost_equal( self.assertEqual(
val[0][0]/val[0][1], value[0][0]/value[0][1], c_float(val[0][0] / val[0][1]).value,
c_float(value[0][0] / value[0][1]).value,
msg="%s didn't roundtrip" % tag) msg="%s didn't roundtrip" % tag)
else: else:
self.assertEqual( self.assertEqual(
value, reloaded[tag], "%s didn't roundtrip" % tag) c_float(val).value, c_float(value).value,
msg="%s didn't roundtrip" % tag)
else:
self.assertEqual(
val, value, msg="%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')

View File

@ -70,20 +70,22 @@ class TestFileTiff(PillowTestCase):
def test_xyres_tiff(self): def test_xyres_tiff(self):
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"
for legacy_api in [False, True]:
im = Image.open(filename) im = Image.open(filename)
assert isinstance(im.tag.tags[X_RESOLUTION][0], tuple) im.tag.legacy_api = legacy_api
assert isinstance(im.tag.tags[Y_RESOLUTION][0], tuple) if legacy_api:
assert isinstance(im.tag[X_RESOLUTION][0], tuple)
assert isinstance(im.tag[Y_RESOLUTION][0], tuple)
# Try to read a file where X,Y_RESOLUTION are ints # Try to read a file where X,Y_RESOLUTION are ints
im.tag.tags[X_RESOLUTION] = (72,) im.tag[X_RESOLUTION] = (72,)
im.tag.tags[Y_RESOLUTION] = (72,) im.tag[Y_RESOLUTION] = (72,)
im.tag.legacy_api = False # _setup assumes the new API.
im._setup() im._setup()
self.assertEqual(im.info['dpi'], (72., 72.)) self.assertEqual(im.info['dpi'], (72., 72.))
def test_bad_exif(self): def test_bad_exif(self):
try: image = Image.open('Tests/images/hopper_bad_exif.jpg')
Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() self.assertRaises(Exception, image._getexif)
except struct.error:
self.fail("Bad EXIF data should not pass incorrect values to _binary unpack")
def test_little_endian(self): def test_little_endian(self):
im = Image.open('Tests/images/16bit.cropped.tif') im = Image.open('Tests/images/16bit.cropped.tif')
@ -197,7 +199,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)
@ -210,139 +211,81 @@ class TestFileTiff(PillowTestCase):
def test_as_dict(self): def test_as_dict(self):
# Arrange # Arrange
filename = "Tests/images/pil136.tiff" filename = "Tests/images/pil136.tiff"
for legacy_api in [False, True]:
im = Image.open(filename) im = Image.open(filename)
im.tag.legacy_api = legacy_api
# 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.as_dict(),
{256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,),
262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), 262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,),
279: (9460,), 282: ((720000, 10000),), 279: (9460,), 282: ((720000, 10000),),
283: ((720000, 10000),), 284: (1,)}) 283: ((720000, 10000),), 284: (1,)} if legacy_api else
{256: 55, 257: 43, 258: (8, 8, 8, 8), 259: 1,
262: 2, 296: 2, 273: (8,), 338: (1,), 277: 4,
279: (9460,), 282: 72.0, 283: 72.0, 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()
ifd.legacy_api = legacy_api
data = b"abc" data = b"abc"
# Act
ret = ifd.load_byte(data) ret = ifd.load_byte(data)
self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99))
# Assert
self.assertEqual(ret, b"abc")
def test_load_string(self): def test_load_string(self):
# Arrange
ifd = TiffImagePlugin.ImageFileDirectory() ifd = TiffImagePlugin.ImageFileDirectory()
data = b"abc\0" data = b"abc\0"
# Act
ret = ifd.load_string(data) 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() ifd = TiffImagePlugin.ImageFileDirectory()
data = b"abcdabcd" data = b"abcdabcd"
# Act
ret = ifd.load_float(data) 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() ifd = TiffImagePlugin.ImageFileDirectory()
data = b"abcdefghabcdefgh" data = b"abcdefghabcdefgh"
# Act
ret = ifd.load_double(data) 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)
@ -352,52 +295,45 @@ 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
for legacy_api in [False, True]:
im = Image.open(filename) im = Image.open(filename)
self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 72) im.tag.legacy_api = legacy_api
self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 36) self.assertEqual(im.tag[X_RESOLUTION][0][0] if legacy_api
else im.tag[X_RESOLUTION], 72)
self.assertEqual(im.tag[Y_RESOLUTION][0][0] if legacy_api
else im.tag[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
for legacy_api in [False, True]:
im = Image.open(filename) im = Image.open(filename)
self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 36) im.tag.legacy_api = legacy_api
self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 72) self.assertEqual(im.tag[X_RESOLUTION][0][0] if legacy_api
else im.tag[X_RESOLUTION], 36)
self.assertEqual(im.tag[Y_RESOLUTION][0][0] if legacy_api
else im.tag[Y_RESOLUTION], 72)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,8 +1,10 @@
from __future__ import division
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.values())
class TestFileTiffMetadata(PillowTestCase): class TestFileTiffMetadata(PillowTestCase):
@ -15,17 +17,15 @@ class TestFileTiffMetadata(PillowTestCase):
img = hopper() img = hopper()
textdata = "This is some arbitrary metadata for a text field" textdata = b"This is some arbitrary metadata for a text field"
floatdata = 12.345 floatdata = 12.345
doubledata = 67.89 doubledata = 67.89
info = TiffImagePlugin.ImageFileDirectory() info = TiffImagePlugin.ImageFileDirectory()
info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata)
info[tag_ids['ImageJMetaData']] = textdata info[tag_ids['ImageJMetaData']] = textdata
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
@ -33,16 +33,26 @@ class TestFileTiffMetadata(PillowTestCase):
img.save(f, tiffinfo=info) img.save(f, tiffinfo=info)
for legacy_api in [False, True]:
loaded = Image.open(f) loaded = Image.open(f)
loaded.tag.legacy_api = legacy_api
self.assertEqual(loaded.tag[50838], (len(textdata),)) self.assertEqual(loaded.tag[50838],
(len(textdata),) if legacy_api else len(textdata))
self.assertEqual(loaded.tag[50839], textdata) self.assertEqual(loaded.tag[50839], textdata)
self.assertAlmostEqual(loaded.tag[tag_ids['RollAngle']][0], floatdata, loaded_float = loaded.tag[tag_ids['RollAngle']]
places=5) if legacy_api:
self.assertAlmostEqual(loaded.tag[tag_ids['YawAngle']][0], doubledata) loaded_float = loaded_float[0]
self.assertAlmostEqual(loaded_float, floatdata, places=5)
loaded_double = loaded.tag[tag_ids['YawAngle']]
if legacy_api:
loaded_double = loaded_double[0]
self.assertAlmostEqual(loaded_double, doubledata)
def test_read_metadata(self): def test_read_metadata(self):
for legacy_api in [False, True]:
img = Image.open('Tests/images/hopper_g4.tif') img = Image.open('Tests/images/hopper_g4.tif')
img.tag.legacy_api = legacy_api
known = {'YResolution': ((4294967295, 113653537),), known = {'YResolution': ((4294967295, 113653537),),
'PlanarConfiguration': (1,), 'PlanarConfiguration': (1,),
@ -59,17 +69,27 @@ class TestFileTiffMetadata(PillowTestCase):
'Orientation': (1,), 'Orientation': (1,),
'StripByteCounts': (1968,), 'StripByteCounts': (1968,),
'SamplesPerPixel': (1,), 'SamplesPerPixel': (1,),
'StripOffsets': (8,), 'StripOffsets': (8,)
} if legacy_api else {
'YResolution': 4294967295 / 113653537,
'PlanarConfiguration': 1,
'BitsPerSample': (1,),
'ImageLength': 128,
'Compression': 4,
'FillOrder': 1,
'RowsPerStrip': 128,
'ResolutionUnit': 3,
'PhotometricInterpretation': 0,
'PageNumber': (0, 1),
'XResolution': 4294967295 / 113653537,
'ImageWidth': 128,
'Orientation': 1,
'StripByteCounts': (1968,),
'SamplesPerPixel': 1,
'StripOffsets': (8,)
} }
# self.assertEqual is equivalent, self.assertEqual(known, img.tag.named())
# but less helpful in telling what's wrong.
named = img.tag.named()
for tag, value in named.items():
self.assertEqual(known[tag], value)
for tag, value in known.items():
self.assertEqual(value, named[tag])
def test_write_metadata(self): def test_write_metadata(self):
""" Test metadata writing through the python code """ """ Test metadata writing through the python code """

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,8 +736,6 @@ 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
// while (PyDict_Next(dir, &pos, &key, &value)) {
for (pos = 0; pos < d_size; pos++) { for (pos = 0; pos < d_size; pos++) {
key = PyList_GetItem(keys, pos); key = PyList_GetItem(keys, pos);
value = PyList_GetItem(values, pos); value = PyList_GetItem(values, pos);
@ -749,28 +746,33 @@ 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 (PyFloat_Check(value)) {
TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value)));
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
(float)PyFloat_AsDouble(value));
} else if (PyBytes_Check(value)) { } else if (PyBytes_Check(value)) {
TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value))); TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value)));
status = ImagingLibTiffSetField(&encoder->state, status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key), (ttag_t) PyInt_AsLong(key),
PyBytes_AsString(value)); PyBytes_AsString(value));
} else if(PyList_Check(value)) { } else if (PyTuple_Check(value)) {
int len,i; int len,i;
float *floatav; float *floatav;
int *intav; int *intav;
TRACE(("Setting from List: %d \n", (int)PyInt_AsLong(key))); TRACE(("Setting from Tuple: %d \n", (int)PyInt_AsLong(key)));
len = (int)PyList_Size(value); len = (int)PyTuple_Size(value);
if (len) { if (len) {
if (PyInt_Check(PyList_GetItem(value,0))) { if (PyInt_Check(PyTuple_GetItem(value,0))) {
TRACE((" %d elements, setting as ints \n", len)); TRACE((" %d elements, setting as ints \n", len));
intav = malloc(sizeof(int)*len); intav = malloc(sizeof(int)*len);
if (intav) { if (intav) {
for (i=0;i<len;i++) { for (i=0;i<len;i++) {
intav[i] = (int)PyInt_AsLong(PyList_GetItem(value,i)); intav[i] = (int)PyInt_AsLong(PyTuple_GetItem(value,i));
} }
status = ImagingLibTiffSetField(&encoder->state, status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key), (ttag_t) PyInt_AsLong(key),
intav); len, intav);
free(intav); free(intav);
} }
} else { } else {
@ -778,20 +780,15 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args)
floatav = malloc(sizeof(float)*len); floatav = malloc(sizeof(float)*len);
if (floatav) { if (floatav) {
for (i=0;i<len;i++) { for (i=0;i<len;i++) {
floatav[i] = (float)PyFloat_AsDouble(PyList_GetItem(value,i)); floatav[i] = (float)PyFloat_AsDouble(PyTuple_GetItem(value,i));
} }
status = ImagingLibTiffSetField(&encoder->state, status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key), (ttag_t) PyInt_AsLong(key),
floatav); len, floatav);
free(floatav); free(floatav);
} }
} }
} }
} else if (PyFloat_Check(value)) {
TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value)));
status = ImagingLibTiffSetField(&encoder->state,
(ttag_t) PyInt_AsLong(key),
(float)PyFloat_AsDouble(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),