From 8042e324bdb3c7147e6b993d3ebe3f9fa9bdbce1 Mon Sep 17 00:00:00 2001 From: Bryant Mairs Date: Mon, 18 Apr 2016 19:56:26 -0700 Subject: [PATCH 01/16] Add tests for opening 2-5 layer uint16 greyscale TIFFs. --- Tests/images/uint16_2_4660.tif | Bin 0 -> 558 bytes Tests/images/uint16_3_4660.tif | Bin 0 -> 770 bytes Tests/images/uint16_4_4660.tif | Bin 0 -> 980 bytes Tests/images/uint16_5_4660.tif | Bin 0 -> 1186 bytes Tests/test_file_tiff.py | 25 +++++++++++++++++++++++++ 5 files changed, 25 insertions(+) create mode 100644 Tests/images/uint16_2_4660.tif create mode 100644 Tests/images/uint16_3_4660.tif create mode 100644 Tests/images/uint16_4_4660.tif create mode 100644 Tests/images/uint16_5_4660.tif diff --git a/Tests/images/uint16_2_4660.tif b/Tests/images/uint16_2_4660.tif new file mode 100644 index 0000000000000000000000000000000000000000..188ff2af7b1caf7c0b512fcb76dcbd426a8df8f4 GIT binary patch literal 558 zcmebD)MDUZU|`^3U|?isU<9(bfS3`9%>-mK0mTG>kQpitQpbkG7Gz`r>zxM_7ln$0 s^oT*(AT!0GY9;_R%OL3ug0g{17=nRpkUmDRX$%Y|LZ(8ac<_V)01aF^xc~qF literal 0 HcmV?d00001 diff --git a/Tests/images/uint16_3_4660.tif b/Tests/images/uint16_3_4660.tif new file mode 100644 index 0000000000000000000000000000000000000000..3c2bc86587ef6ce3b1d2a558ba8dd040b221b900 GIT binary patch literal 770 zcmebD)MDUZU|`^3U|?isU<9(bfS3`9%>-mK1I6Y6F*8&gq>c@VEy&0M*1HNQE(#R~ z=@EmnL1v0W)kH8cFvuY34Fa-(LSPUKWP|iAf${|yK#&m(O@vH^%!EeC(GZ|+2mk=6 C?N|i> literal 0 HcmV?d00001 diff --git a/Tests/images/uint16_4_4660.tif b/Tests/images/uint16_4_4660.tif new file mode 100644 index 0000000000000000000000000000000000000000..ae8252d78a721e796e3a16445db3d7c50374873b GIT binary patch literal 980 zcmebD)MDUZU|`^3U|?isU<9(bfS3`9%>-n#0LA73F*8&gq>c@VEy&0M*1H8LE(#R~ z=@EmnL1v0W)hIACFvuY34Fa-(LJZ4*I2gzV>01NDARqvR5FR7Y4iGdEG8HltG8Y=9 MN5f$>9bo1I0ISP&9{>OV literal 0 HcmV?d00001 diff --git a/Tests/images/uint16_5_4660.tif b/Tests/images/uint16_5_4660.tif new file mode 100644 index 0000000000000000000000000000000000000000..df92724c35989846d14183fa9648f2948806528f GIT binary patch literal 1186 zcmebD)MDUZU|`^3U|?isU<9(bfS3`9%>-n#0>$P5F*8&gq>c@VEy&0M*1HQRE(#R~ z=@EmnL1v0W)x2P4V30x58w6yt0OeNzaWIe#(zgMKK|lZwA$*|A;Lt?KRLD%oT*yLb OlrF{<^=!(YLf5( literal 0 HcmV?d00001 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 8cad252724c..39ae7f11dd4 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -916,3 +916,28 @@ def test_fd_leak(self, tmp_path: Path) -> None: # this should not fail, as load should have closed the file pointer, # and close should have closed the mmap os.remove(tmpfile) + + # Created with ImageMagick: convert hopper.jpg hopper_jpg.tif + # Contains JPEGTables (347) tag + infile = "Tests/images/hopper_jpg.tif" + im = Image.open(infile) + + # Act / Assert + # Should not raise UnicodeDecodeError or anything else + im.save(outfile) + + def test_open_tiff_uint16_multiband(self): + """Test opening multiband TIFFs and reading all channels.""" + base_value = 4660 + for i in range(2, 6): + infile = "Tests/images/uint16_{}_{}.tif".format(i, base_value) + im = Image.open(infile) + im.load() + pixel = [base_value + j for j in range(0, i)] + actual_pixel = im.getpixel((0,0)) + if type(actual_pixel) is int: + actual_pixel = [actual_pixel] + self.assertEqual(actual_pixel, pixel) + +if __name__ == '__main__': + unittest.main() From b8b47e4f24b9d2371fca8e3f2634d516a960a231 Mon Sep 17 00:00:00 2001 From: Bryant Mairs Date: Tue, 19 Apr 2016 08:02:13 -0700 Subject: [PATCH 02/16] Add open() support for 2-5 band uint16 TIFFs. This only gets us to opening the first band, it doesn't actually allow the other bands to be retrieved. --- PIL/TiffImagePlugin.py | 1509 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1509 insertions(+) create mode 100644 PIL/TiffImagePlugin.py diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py new file mode 100644 index 00000000000..670962e689f --- /dev/null +++ b/PIL/TiffImagePlugin.py @@ -0,0 +1,1509 @@ +# +# The Python Imaging Library. +# $Id$ +# +# TIFF file handling +# +# TIFF is a flexible, if somewhat aged, image file format originally +# defined by Aldus. Although TIFF supports a wide variety of pixel +# layouts and compression methods, the name doesn't really stand for +# "thousands of incompatible file formats," it just feels that way. +# +# To read TIFF data from a stream, the stream must be seekable. For +# progressive decoding, make sure to use TIFF files where the tag +# directory is placed first in the file. +# +# History: +# 1995-09-01 fl Created +# 1996-05-04 fl Handle JPEGTABLES tag +# 1996-05-18 fl Fixed COLORMAP support +# 1997-01-05 fl Fixed PREDICTOR support +# 1997-08-27 fl Added support for rational tags (from Perry Stoll) +# 1998-01-10 fl Fixed seek/tell (from Jan Blom) +# 1998-07-15 fl Use private names for internal variables +# 1999-06-13 fl Rewritten for PIL 1.0 (1.0) +# 2000-10-11 fl Additional fixes for Python 2.0 (1.1) +# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) +# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) +# 2001-12-18 fl Added workaround for broken Matrox library +# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) +# 2003-05-19 fl Check FILLORDER tag +# 2003-09-26 fl Added RGBa support +# 2004-02-24 fl Added DPI support; fixed rational write support +# 2005-02-07 fl Added workaround for broken Corel Draw 10 files +# 2006-01-09 fl Added support for float/double tags (from Russell Nelson) +# +# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. +# Copyright (c) 1995-1997 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + +from __future__ import division, print_function + +from PIL import Image, ImageFile +from PIL import ImagePalette +from PIL import _binary +from PIL import TiffTags + +import collections +from fractions import Fraction +from numbers import Number, Rational + +import io +import itertools +import os +import struct +import sys +import warnings + +from .TiffTags import TYPES + + +__version__ = "1.3.5" +DEBUG = False # Needs to be merged with the new logging approach. + +# Set these to true to force use of libtiff for reading or writing. +READ_LIBTIFF = False +WRITE_LIBTIFF = False +IFD_LEGACY_API = True + +II = b"II" # little-endian (Intel style) +MM = b"MM" # big-endian (Motorola style) + +i8 = _binary.i8 +o8 = _binary.o8 + +# +# -------------------------------------------------------------------- +# Read TIFF files + +# a few tag names, just to make the code below a bit more readable +IMAGEWIDTH = 256 +IMAGELENGTH = 257 +BITSPERSAMPLE = 258 +COMPRESSION = 259 +PHOTOMETRIC_INTERPRETATION = 262 +FILLORDER = 266 +IMAGEDESCRIPTION = 270 +STRIPOFFSETS = 273 +SAMPLESPERPIXEL = 277 +ROWSPERSTRIP = 278 +STRIPBYTECOUNTS = 279 +X_RESOLUTION = 282 +Y_RESOLUTION = 283 +PLANAR_CONFIGURATION = 284 +RESOLUTION_UNIT = 296 +SOFTWARE = 305 +DATE_TIME = 306 +ARTIST = 315 +PREDICTOR = 317 +COLORMAP = 320 +TILEOFFSETS = 324 +EXTRASAMPLES = 338 +SAMPLEFORMAT = 339 +JPEGTABLES = 347 +COPYRIGHT = 33432 +IPTC_NAA_CHUNK = 33723 # newsphoto properties +PHOTOSHOP_CHUNK = 34377 # photoshop properties +ICCPROFILE = 34675 +EXIFIFD = 34665 +XMP = 700 + +# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java +IMAGEJ_META_DATA_BYTE_COUNTS = 50838 +IMAGEJ_META_DATA = 50839 + +COMPRESSION_INFO = { + # Compression => pil compression name + 1: "raw", + 2: "tiff_ccitt", + 3: "group3", + 4: "group4", + 5: "tiff_lzw", + 6: "tiff_jpeg", # obsolete + 7: "jpeg", + 8: "tiff_adobe_deflate", + 32771: "tiff_raw_16", # 16-bit padding + 32773: "packbits", + 32809: "tiff_thunderscan", + 32946: "tiff_deflate", + 34676: "tiff_sgilog", + 34677: "tiff_sgilog24", +} + +COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()]) + +OPEN_INFO = { + # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, + # ExtraSamples) => mode, rawmode + (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (II, 1, (1,), 1, (1,), ()): ("1", "1"), + (MM, 1, (1,), 1, (1,), ()): ("1", "1"), + (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), + (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), + + (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), + (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), + (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), + (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), + + (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), + (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), + (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), + + (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (II, 1, (1,), 1, (8,), ()): ("L", "L"), + (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), + (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), + + (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), + + (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), + (II, 1, (1,), 1, (16,16), (0,)): ("I;16", "I;16"), + (II, 1, (1,), 1, (16,16,16), (0,0,)): ("I;16", "I;16"), + (II, 1, (1,), 1, (16,16,16,16), (0,0,0,)): ("I;16", "I;16"), + (II, 1, (1,), 1, (16,16,16,16,16), (0,0,0,0,)): ("I;16", "I;16"), + (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), + (II, 1, (2,), 1, (16,), ()): ("I;16S", "I;16S"), + (MM, 1, (2,), 1, (16,), ()): ("I;16BS", "I;16BS"), + + (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), + (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), + (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), + (MM, 1, (2,), 1, (32,), ()): ("I;32BS", "I;32BS"), + (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), + + (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + + (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + + (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (II, 3, (1,), 1, (8,), ()): ("P", "P"), + (MM, 3, (1,), 1, (8,), ()): ("P", "P"), + (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), + (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), + + (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + + (II, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + + (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), + (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), +} + +PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"] + + +def _accept(prefix): + return prefix[:4] in PREFIXES + + +def _limit_rational(val, max_val): + inv = abs(val) > 1 + n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) + return n_d[::-1] if inv else n_d + +## +# Wrapper for TIFF IFDs. + +_load_dispatch = {} +_write_dispatch = {} + + +class IFDRational(Rational): + """ Implements a rational class where 0/0 is a legal value to match + the in the wild use of exif rationals. + + e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used + """ + + """ If the denominator is 0, store this as a float('nan'), otherwise store + as a fractions.Fraction(). Delegate as appropriate + + """ + + __slots__ = ('_numerator', '_denominator', '_val') + + def __init__(self, value, denominator=1): + """ + :param value: either an integer numerator, a + float/rational/other number, or an IFDRational + :param denominator: Optional integer denominator + """ + self._denominator = denominator + self._numerator = value + self._val = float(1) + + if type(value) == Fraction: + self._numerator = value.numerator + self._denominator = value.denominator + self._val = value + + if type(value) == IFDRational: + self._denominator = value.denominator + self._numerator = value.numerator + self._val = value._val + return + + if denominator == 0: + self._val = float('nan') + return + + elif denominator == 1: + if sys.hexversion < 0x2070000 and type(value) == float: + # python 2.6 is different. + self._val = Fraction.from_float(value) + else: + self._val = Fraction(value) + else: + self._val = Fraction(value, denominator) + + @property + def numerator(a): + return a._numerator + + @property + def denominator(a): + return a._denominator + + def limit_rational(self, max_denominator): + """ + + :param max_denominator: Integer, the maximum denominator value + :returns: Tuple of (numerator, denominator) + """ + + if self.denominator == 0: + return (self.numerator, self.denominator) + + f = self._val.limit_denominator(max_denominator) + return (f.numerator, f.denominator) + + def __repr__(self): + return str(float(self._val)) + + def __hash__(self): + return self._val.__hash__() + + def __eq__(self, other): + return self._val == other + + def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + return delegate + + """ a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul', + 'truediv', 'rtruediv', 'floordiv', + 'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg', + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', + 'ceil', 'floor', 'round'] + print "\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a) + """ + + __add__ = _delegate('__add__') + __radd__ = _delegate('__radd__') + __sub__ = _delegate('__sub__') + __rsub__ = _delegate('__rsub__') + __div__ = _delegate('__div__') + __rdiv__ = _delegate('__rdiv__') + __mul__ = _delegate('__mul__') + __rmul__ = _delegate('__rmul__') + __truediv__ = _delegate('__truediv__') + __rtruediv__ = _delegate('__rtruediv__') + __floordiv__ = _delegate('__floordiv__') + __rfloordiv__ = _delegate('__rfloordiv__') + __mod__ = _delegate('__mod__') + __rmod__ = _delegate('__rmod__') + __pow__ = _delegate('__pow__') + __rpow__ = _delegate('__rpow__') + __pos__ = _delegate('__pos__') + __neg__ = _delegate('__neg__') + __abs__ = _delegate('__abs__') + __trunc__ = _delegate('__trunc__') + __lt__ = _delegate('__lt__') + __gt__ = _delegate('__gt__') + __le__ = _delegate('__le__') + __ge__ = _delegate('__ge__') + __nonzero__ = _delegate('__nonzero__') + __ceil__ = _delegate('__ceil__') + __floor__ = _delegate('__floor__') + __round__ = _delegate('__round__') + + +class ImageFileDirectory_v2(collections.MutableMapping): + """This class represents a TIFF tag directory. To speed things up, we + don't decode tags unless they're asked for. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v2() + ifd[key] = 'Some Data' + ifd.tagtype[key] = 2 + print(ifd[key]) + 'Some Data' + + Individual values are returned as the strings or numbers, sequences are + returned as tuples of the values. + + The tiff metadata type of each item is stored in a dictionary of + tag types in + `~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types + are read from a tiff file, guessed from the type added, or added + manually. + + Data Structures: + + * self.tagtype = {} + + * Key: numerical tiff tag number + * Value: integer corresponding to the data type from `~PIL.TiffTags.TYPES` + + .. versionadded:: 3.0.0 + """ + """ + Documentation: + + 'internal' data structures: + * self._tags_v2 = {} Key: numerical tiff tag number + Value: decoded data, as tuple for multiple values + * self._tagdata = {} Key: numerical tiff tag number + Value: undecoded byte string from file + * self._tags_v1 = {} Key: numerical tiff tag number + Value: decoded data in the v1 format + + Tags will be found in the private attributes self._tagdata, and in + self._tags_v2 once decoded. + + Self.legacy_api is a value for internal use, and shouldn't be + changed from outside code. In cooperation with the + ImageFileDirectory_v1 class, if legacy_api is true, then decoded + tags will be populated into both _tags_v1 and _tags_v2. _Tags_v2 + will be used if this IFD is used in the TIFF save routine. Tags + should be read from tags_v1 if legacy_api == true. + + """ + + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): + """Initialize an ImageFileDirectory. + + To construct an ImageFileDirectory from a real file, pass the 8-byte + magic header to the constructor. To only set the endianness, pass it + as the 'prefix' keyword argument. + + :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets + endianness. + :param prefix: Override the endianness of the file. + """ + if ifh[:4] not in PREFIXES: + raise SyntaxError("not a TIFF file (header %r not valid)" % ifh) + self._prefix = prefix if prefix is not None else ifh[:2] + if self._prefix == MM: + self._endian = ">" + elif self._prefix == II: + self._endian = "<" + else: + raise SyntaxError("not a TIFF IFD") + self.reset() + self.next, = self._unpack("L", ifh[4:]) + self._legacy_api = False + + prefix = property(lambda self: self._prefix) + offset = property(lambda self: self._offset) + legacy_api = property(lambda self: self._legacy_api) + + @legacy_api.setter + def legacy_api(self, value): + raise Exception("Not allowing setting of legacy api") + + def reset(self): + self._tags_v1 = {} # will remain empty if legacy_api is false + self._tags_v2 = {} # main tag storage + self._tagdata = {} + self.tagtype = {} # added 2008-06-05 by Florian Hoech + self._next = None + self._offset = None + + def __str__(self): + return str(dict(self)) + + def as_dict(self): + """Return a dictionary of the image's tags. + + .. deprecated:: 3.0.0 + """ + warnings.warn("as_dict() is deprecated. " + + "Please use dict(ifd) instead.", DeprecationWarning) + return dict(self) + + def named(self): + """ + :returns: dict of name|key: value + + Returns the complete tag dictionary, with named tags where possible. + """ + return dict((TiffTags.lookup(code).name, value) + for code, value in self.items()) + + def __len__(self): + return len(set(self._tagdata) | set(self._tags_v2)) + + def __getitem__(self, tag): + if tag not in self._tags_v2: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + self[tag] = handler(self, data, self.legacy_api) # check type + val = self._tags_v2[tag] + if self.legacy_api and not isinstance(val, (tuple, bytes)): + val = val, + return val + + def __contains__(self, tag): + return tag in self._tags_v2 or tag in self._tagdata + + if bytes is str: + def has_key(self, tag): + return tag in self + + def __setitem__(self, tag, value): + self._setitem(tag, value, self.legacy_api) + + def _setitem(self, tag, value, legacy_api): + basetypes = (Number, bytes, str) + if bytes is str: + basetypes += unicode, + + info = TiffTags.lookup(tag) + values = [value] if isinstance(value, basetypes) else value + + if tag not in self.tagtype: + if info.type: + self.tagtype[tag] = info.type + else: + self.tagtype[tag] = 7 + if all(isinstance(v, IFDRational) for v in values): + self.tagtype[tag] = 5 + elif all(isinstance(v, int) for v in values): + if all(v < 2 ** 16 for v in values): + self.tagtype[tag] = 3 + else: + self.tagtype[tag] = 4 + elif all(isinstance(v, float) for v in values): + self.tagtype[tag] = 12 + else: + if bytes is str: + # Never treat data as binary by default on Python 2. + self.tagtype[tag] = 2 + else: + if all(isinstance(v, str) for v in values): + self.tagtype[tag] = 2 + + if self.tagtype[tag] == 7 and bytes is not str: + values = [value.encode("ascii", 'replace') if isinstance(value, str) else value + for value in values] + + values = tuple(info.cvt_enum(value) for value in values) + + dest = self._tags_v1 if legacy_api else self._tags_v2 + + if info.length == 1: + if legacy_api and self.tagtype[tag] in [5, 10]: + values = values, + dest[tag], = values + else: + dest[tag] = values + + def __delitem__(self, tag): + self._tags_v2.pop(tag, None) + self._tags_v1.pop(tag, None) + self._tagdata.pop(tag, None) + + def __iter__(self): + return iter(set(self._tagdata) | set(self._tags_v2)) + + def _unpack(self, fmt, data): + return struct.unpack(self._endian + fmt, data) + + def _pack(self, fmt, *values): + return struct.pack(self._endian + fmt, *values) + + def _register_loader(idx, size): + def decorator(func): + from PIL.TiffTags import TYPES + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func + return func + return decorator + + def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func + return func + return decorator + + def _register_basic(idx_fmt_name): + from PIL.TiffTags import TYPES + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( + self._unpack("{0}{1}".format(len(data) // size, fmt), data)) + _write_dispatch[idx] = lambda self, *values: ( + b"".join(self._pack(fmt, value) for value in values)) + + list(map(_register_basic, + [(3, "H", "short"), (4, "L", "long"), + (6, "b", "signed byte"), (8, "h", "signed short"), + (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) + + @_register_loader(1, 1) # Basic type, except for the legacy API. + def load_byte(self, data, legacy_api=True): + return (data if legacy_api else + tuple(map(ord, data) if bytes is str else data)) + + @_register_writer(1) # Basic type, except for the legacy API. + def write_byte(self, data): + return data + + @_register_loader(2, 1) + def load_string(self, data, legacy_api=True): + if data.endswith(b"\0"): + data = data[:-1] + return data.decode("latin-1", "replace") + + @_register_writer(2) + def write_string(self, value): + # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if sys.version_info[0] == 2: + value = value.decode('ascii', 'replace') + return b"" + value.encode('ascii', 'replace') + b"\0" + + @_register_loader(5, 8) + def load_rational(self, data, legacy_api=True): + vals = self._unpack("{0}L".format(len(data) // 4), data) + combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + return tuple(combine(num, denom) + for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(5) + def write_rational(self, *values): + return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31)) + for frac in values) + + @_register_loader(7, 1) + def load_undefined(self, data, legacy_api=True): + return data + + @_register_writer(7) + def write_undefined(self, value): + return value + + @_register_loader(10, 8) + def load_signed_rational(self, data, legacy_api=True): + vals = self._unpack("{0}l".format(len(data) // 4), data) + combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) + return tuple(combine(num, denom) + for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(10) + def write_signed_rational(self, *values): + return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30)) + for frac in values) + + def _ensure_read(self, fp, size): + ret = fp.read(size) + if len(ret) != size: + raise IOError("Corrupt EXIF data. " + + "Expecting to read %d bytes but only got %d. " % + (size, len(ret))) + return ret + + def load(self, fp): + + self.reset() + self._offset = fp.tell() + + try: + for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): + tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) + if DEBUG: + tagname = TiffTags.lookup(tag).name + typname = TYPES.get(typ, "unknown") + print("tag: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") + + try: + unit_size, handler = self._load_dispatch[typ] + except KeyError: + if DEBUG: + print("- unsupported type", typ) + continue # ignore unsupported type + size = count * unit_size + if size > 4: + here = fp.tell() + offset, = self._unpack("L", data) + if DEBUG: + print("Tag Location: %s - Data Location: %s" % + (here, offset), end=" ") + fp.seek(offset) + data = ImageFile._safe_read(fp, size) + fp.seek(here) + else: + data = data[:size] + + if len(data) != size: + warnings.warn("Possibly corrupt EXIF data. " + "Expecting to read %d bytes but only got %d. " + "Skipping tag %s" % (size, len(data), tag)) + continue + + self._tagdata[tag] = data + self.tagtype[tag] = typ + + if DEBUG: + if size > 32: + print("- value: " % size) + else: + print("- value:", self[tag]) + + self.next, = self._unpack("L", self._ensure_read(fp, 4)) + except IOError as msg: + warnings.warn(str(msg)) + return + + def save(self, fp): + + if fp.tell() == 0: # skip TIFF header on subsequent pages + # tiff header -- PIL always starts the first IFD at offset 8 + fp.write(self._prefix + self._pack("HL", 42, 8)) + + # FIXME What about tagdata? + fp.write(self._pack("H", len(self._tags_v2))) + + entries = [] + offset = fp.tell() + len(self._tags_v2) * 12 + 4 + stripoffsets = None + + # pass 1: convert tags to binary format + # always write tags in ascending order + for tag, value in sorted(self._tags_v2.items()): + if tag == STRIPOFFSETS: + stripoffsets = len(entries) + typ = self.tagtype.get(tag) + if DEBUG: + print("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) + if DEBUG: + tagname = TiffTags.lookup(tag).name + typname = TYPES.get(typ, "unknown") + print("save: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") + if len(data) >= 16: + print("- value: " % len(data)) + else: + print("- value:", values) + + # count is sum of lengths for string and arbitrary data + count = len(data) if typ in [2, 7] else len(values) + # figure out if data fits into the entry + if len(data) <= 4: + entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) + else: + entries.append((tag, typ, count, self._pack("L", offset), data)) + offset += (len(data) + 1) // 2 * 2 # pad to word + + # update strip offset data to point beyond auxiliary data + if stripoffsets is not None: + tag, typ, count, value, data = entries[stripoffsets] + if data: + raise NotImplementedError( + "multistrip support not yet implemented") + value = self._pack("L", self._unpack("L", value)[0] + offset) + entries[stripoffsets] = tag, typ, count, value, data + + # pass 2: write entries to file + for tag, typ, count, value, data in entries: + if DEBUG > 1: + print(tag, typ, count, repr(value), repr(data)) + fp.write(self._pack("HHL4s", tag, typ, count, value)) + + # -- overwrite here for multi-page -- + fp.write(b"\0\0\0\0") # end of entries + + # pass 3: write auxiliary data to file + for tag, typ, count, value, data in entries: + fp.write(data) + if len(data) & 1: + fp.write(b"\0") + + return offset + +ImageFileDirectory_v2._load_dispatch = _load_dispatch +ImageFileDirectory_v2._write_dispatch = _write_dispatch +for idx, name in TYPES.items(): + name = name.replace(" ", "_") + setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1]) + setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx]) +del _load_dispatch, _write_dispatch, idx, name + + +# Legacy ImageFileDirectory support. +class ImageFileDirectory_v1(ImageFileDirectory_v2): + """This class represents the **legacy** interface to a TIFF tag directory. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v1() + ifd[key] = 'Some Data' + ifd.tagtype[key] = 2 + print ifd[key] + ('Some Data',) + + Also contains a dictionary of tag types as read from the tiff image file, + `~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. + + Values are returned as a tuple. + + .. deprecated:: 3.0.0 + """ + def __init__(self, *args, **kwargs): + ImageFileDirectory_v2.__init__(self, *args, **kwargs) + self._legacy_api = True + + tags = property(lambda self: self._tags_v1) + tagdata = property(lambda self: self._tagdata) + + @classmethod + def from_v2(cls, original): + """ Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + + """ + + ifd = cls(prefix=original.prefix) + ifd._tagdata = original._tagdata + ifd.tagtype = original.tagtype + ifd.next = original.next # an indicator for multipage tiffs + return ifd + + def to_v2(self): + """ Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + + """ + + ifd = ImageFileDirectory_v2(prefix=self.prefix) + ifd._tagdata = dict(self._tagdata) + ifd.tagtype = dict(self.tagtype) + ifd._tags_v2 = dict(self._tags_v2) + return ifd + + def __contains__(self, tag): + return tag in self._tags_v1 or tag in self._tagdata + + def __len__(self): + return len(set(self._tagdata) | set(self._tags_v1)) + + def __iter__(self): + return iter(set(self._tagdata) | set(self._tags_v1)) + + def __setitem__(self, tag, value): + for legacy_api in (False, True): + self._setitem(tag, value, legacy_api) + + def __getitem__(self, tag): + if tag not in self._tags_v1: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + for legacy in (False, True): + self._setitem(tag, handler(self, data, legacy), legacy) + val = self._tags_v1[tag] + if not isinstance(val, (tuple, bytes)): + val = val, + return val + + +# undone -- switch this pointer when IFD_LEGACY_API == False +ImageFileDirectory = ImageFileDirectory_v1 + + +## +# Image plugin for TIFF files. + +class TiffImageFile(ImageFile.ImageFile): + + format = "TIFF" + format_description = "Adobe TIFF" + + def _open(self): + "Open the first image in a TIFF file" + + # Header + ifh = self.fp.read(8) + + # image file directory (tag dictionary) + self.tag_v2 = ImageFileDirectory_v2(ifh) + + # legacy tag/ifd entries will be filled in later + self.tag = self.ifd = None + + # setup frame pointers + self.__first = self.__next = self.tag_v2.next + self.__frame = -1 + self.__fp = self.fp + self._frame_pos = [] + self._n_frames = None + self._is_animated = None + + if DEBUG: + print("*** TiffImageFile._open ***") + print("- __first:", self.__first) + print("- ifh: ", ifh) + + # and load the first frame + self._seek(0) + + @property + def n_frames(self): + if self._n_frames is None: + current = self.tell() + try: + while True: + self._seek(self.tell() + 1) + except EOFError: + self._n_frames = self.tell() + 1 + self.seek(current) + return self._n_frames + + @property + def is_animated(self): + if self._is_animated is None: + current = self.tell() + + try: + self.seek(1) + self._is_animated = True + except EOFError: + self._is_animated = False + + self.seek(current) + return self._is_animated + + def seek(self, frame): + "Select a given frame as current image" + self._seek(max(frame, 0)) # Questionable backwards compatibility. + # Create a new core image object on second and + # subsequent frames in the image. Image may be + # different size/mode. + Image._decompression_bomb_check(self.size) + self.im = Image.core.new(self.mode, self.size) + + def _seek(self, frame): + self.fp = self.__fp + while len(self._frame_pos) <= frame: + if not self.__next: + raise EOFError("no more images in TIFF file") + if DEBUG: + print("Seeking to frame %s, on frame %s, " + "__next %s, location: %s" % + (frame, self.__frame, self.__next, self.fp.tell())) + # reset python3 buffered io handle in case fp + # was passed to libtiff, invalidating the buffer + self.fp.tell() + self.fp.seek(self.__next) + self._frame_pos.append(self.__next) + if DEBUG: + print("Loading tags, location: %s" % self.fp.tell()) + self.tag_v2.load(self.fp) + self.__next = self.tag_v2.next + self.__frame += 1 + self.fp.seek(self._frame_pos[frame]) + self.tag_v2.load(self.fp) + # fill the legacy tag/ifd entries + self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) + self.__frame = frame + self._setup() + + def tell(self): + "Return the current frame number" + return self.__frame + + def _decoder(self, rawmode, layer, tile=None): + "Setup decoder contexts" + + args = None + if rawmode == "RGB" and self._planar_configuration == 2: + rawmode = rawmode[layer] + compression = self._compression + if compression == "raw": + args = (rawmode, 0, 1) + elif compression == "jpeg": + args = rawmode, "" + if JPEGTABLES in self.tag_v2: + # Hack to handle abbreviated JPEG headers + # FIXME This will fail with more than one value + self.tile_prefix, = self.tag_v2[JPEGTABLES] + elif compression == "packbits": + args = rawmode + elif compression == "tiff_lzw": + args = rawmode + if PREDICTOR in self.tag_v2: + # Section 14: Differencing Predictor + self.decoderconfig = (self.tag_v2[PREDICTOR],) + + if ICCPROFILE in self.tag_v2: + self.info['icc_profile'] = self.tag_v2[ICCPROFILE] + + return args + + def load(self): + if self.use_load_libtiff: + return self._load_libtiff() + return super(TiffImageFile, self).load() + + def _load_libtiff(self): + """ Overload method triggered when we detect a compressed tiff + Calls out to libtiff """ + + pixel = Image.Image.load(self) + + if self.tile is None: + raise IOError("cannot load this image") + if not self.tile: + return pixel + + self.load_prepare() + + if not len(self.tile) == 1: + raise IOError("Not exactly one tile") + + # (self._compression, (extents tuple), + # 0, (rawmode, self._compression, fp)) + extents = self.tile[0][1] + args = self.tile[0][3] + (self.tag_v2.offset,) + decoder = Image._getdecoder(self.mode, 'libtiff', args, + self.decoderconfig) + try: + decoder.setimage(self.im, extents) + except ValueError: + raise IOError("Couldn't set the image") + + if hasattr(self.fp, "getvalue"): + # We've got a stringio like thing passed in. Yay for all in memory. + # The decoder needs the entire file in one shot, so there's not + # a lot we can do here other than give it the entire file. + # unless we could do something like get the address of the + # underlying string for stringio. + # + # Rearranging for supporting byteio items, since they have a fileno + # that returns an IOError if there's no underlying fp. Easier to + # deal with here by reordering. + if DEBUG: + print("have getvalue. just sending in a string from getvalue") + n, err = decoder.decode(self.fp.getvalue()) + elif hasattr(self.fp, "fileno"): + # we've got a actual file on disk, pass in the fp. + if DEBUG: + print("have fileno, calling fileno version of the decoder.") + self.fp.seek(0) + # 4 bytes, otherwise the trace might error out + n, err = decoder.decode(b"fpfp") + else: + # we have something else. + if DEBUG: + print("don't have fileno or getvalue. just reading") + # UNDONE -- so much for that buffer size thing. + n, err = decoder.decode(self.fp.read()) + + self.tile = [] + self.readonly = 0 + # libtiff closed the fp in a, we need to close self.fp, if possible + if hasattr(self.fp, 'close'): + if not self.__next: + self.fp.close() + self.fp = None # might be shared + + if err < 0: + raise IOError(err) + + self.load_end() + + return Image.Image.load(self) + + def _setup(self): + "Setup this image object based on current tags" + + if 0xBC01 in self.tag_v2: + raise IOError("Windows Media Photo files not yet supported") + + # extract relevant tags + self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] + self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) + + # photometric is a required tag, but not everyone is reading + # the specification + photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) + + fillorder = self.tag_v2.get(FILLORDER, 1) + + if DEBUG: + print("*** Summary ***") + print("- compression:", self._compression) + print("- photometric_interpretation:", photo) + print("- planar_configuration:", self._planar_configuration) + print("- fill_order:", fillorder) + + # size + xsize = self.tag_v2.get(IMAGEWIDTH) + ysize = self.tag_v2.get(IMAGELENGTH) + self.size = xsize, ysize + + if DEBUG: + print("- size:", self.size) + + format = self.tag_v2.get(SAMPLEFORMAT, (1,)) + if len(format) > 1 and max(format) == min(format) == 1: + # SAMPLEFORMAT is properly per band, so an RGB image will + # be (1,1,1). But, we don't support per band pixel types, + # and anything more than one band is a uint8. So, just + # take the first element. Revisit this if adding support + # for more exotic images. + format = (1,) + + # mode: check photometric interpretation and bits per pixel + key = ( + self.tag_v2.prefix, photo, format, fillorder, + self.tag_v2.get(BITSPERSAMPLE, (1,)), + self.tag_v2.get(EXTRASAMPLES, ()) + ) + if DEBUG: + print("format key:", key) + try: + self.mode, rawmode = OPEN_INFO[key] + except KeyError: + if DEBUG: + print("- unsupported format") + raise SyntaxError("unknown pixel mode") + + if DEBUG: + print("- raw mode:", rawmode) + print("- pil mode:", self.mode) + + self.info["compression"] = self._compression + + xres = self.tag_v2.get(X_RESOLUTION, 1) + yres = self.tag_v2.get(Y_RESOLUTION, 1) + + if xres and yres: + resunit = self.tag_v2.get(RESOLUTION_UNIT, 1) + if resunit == 2: # dots per inch + self.info["dpi"] = xres, yres + elif resunit == 3: # dots per centimeter. convert to dpi + self.info["dpi"] = xres * 2.54, yres * 2.54 + else: # No absolute unit of measurement + self.info["resolution"] = xres, yres + + # build tile descriptors + x = y = l = 0 + self.tile = [] + self.use_load_libtiff = False + if STRIPOFFSETS in self.tag_v2: + # striped image + offsets = self.tag_v2[STRIPOFFSETS] + h = self.tag_v2.get(ROWSPERSTRIP, ysize) + w = self.size[0] + if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", + "group4", "tiff_jpeg", + "tiff_adobe_deflate", + "tiff_thunderscan", + "tiff_deflate", + "tiff_sgilog", + "tiff_sgilog24", + "tiff_raw_16"]: + # if DEBUG: + # print "Activating g4 compression for whole file" + + # Decoder expects entire file as one tile. + # There's a buffer size limit in load (64k) + # so large g4 images will fail if we use that + # function. + # + # Setup the one tile for the whole image, then + # use the _load_libtiff function. + + self.use_load_libtiff = True + + # To be nice on memory footprint, if there's a + # file descriptor, use that instead of reading + # into a string in python. + + # libtiff closes the file descriptor, so pass in a dup. + try: + fp = hasattr(self.fp, "fileno") and \ + os.dup(self.fp.fileno()) + # flush the file descriptor, prevents error on pypy 2.4+ + # should also eliminate the need for fp.tell for py3 + # in _seek + if hasattr(self.fp, "flush"): + self.fp.flush() + except IOError: + # io.BytesIO have a fileno, but returns an IOError if + # it doesn't use a file descriptor. + fp = False + + # libtiff handles the fillmode for us, so 1;IR should + # actually be 1;I. Including the R double reverses the + # bits, so stripes of the image are reversed. See + # https://github.com/python-pillow/Pillow/issues/279 + if fillorder == 2: + key = ( + self.tag_v2.prefix, photo, format, 1, + self.tag_v2.get(BITSPERSAMPLE, (1,)), + self.tag_v2.get(EXTRASAMPLES, ()) + ) + if DEBUG: + print("format key:", key) + # this should always work, since all the + # fillorder==2 modes have a corresponding + # fillorder=1 mode + self.mode, rawmode = OPEN_INFO[key] + # libtiff always returns the bytes in native order. + # we're expecting image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + if self.mode in ('I;16B', 'I;16') and 'I;16' in rawmode: + rawmode = 'I;16N' + + # Offset in the tile tuple is 0, we go from 0,0 to + # w,h, and we only do this once -- eds + a = (rawmode, self._compression, fp) + self.tile.append( + (self._compression, + (0, 0, w, ysize), + 0, a)) + a = None + + else: + for i in range(len(offsets)): + a = self._decoder(rawmode, l, i) + self.tile.append( + (self._compression, + (0, min(y, ysize), w, min(y+h, ysize)), + offsets[i], a)) + if DEBUG: + print("tiles: ", self.tile) + y = y + h + if y >= self.size[1]: + x = y = 0 + l += 1 + a = None + elif TILEOFFSETS in self.tag_v2: + # tiled image + w = self.tag_v2.get(322) + h = self.tag_v2.get(323) + a = None + for o in self.tag_v2[TILEOFFSETS]: + if not a: + a = self._decoder(rawmode, l) + # FIXME: this doesn't work if the image size + # is not a multiple of the tile size... + self.tile.append( + (self._compression, + (x, y, x+w, y+h), + o, a)) + x = x + w + if x >= self.size[0]: + x, y = 0, y + h + if y >= self.size[1]: + x = y = 0 + l += 1 + a = None + else: + if DEBUG: + print("- unsupported data organization") + raise SyntaxError("unknown data organization") + + # fixup palette descriptor + + if self.mode == "P": + palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] + self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) +# +# -------------------------------------------------------------------- +# Write TIFF files + +# little endian is default except for image modes with +# explicit big endian byte-order + +SAVE_INFO = { + # mode => rawmode, byteorder, photometrics, + # sampleformat, bitspersample, extra + "1": ("1", II, 1, 1, (1,), None), + "L": ("L", II, 1, 1, (8,), None), + "LA": ("LA", II, 1, 1, (8, 8), 2), + "P": ("P", II, 3, 1, (8,), None), + "PA": ("PA", II, 3, 1, (8, 8), 2), + "I": ("I;32S", II, 1, 2, (32,), None), + "I;16": ("I;16", II, 1, 1, (16,), None), + "I;16S": ("I;16S", II, 1, 2, (16,), None), + "F": ("F;32F", II, 1, 3, (32,), None), + "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), + "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), + "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), + "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), + "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), + "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), + + "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), + "I;16B": ("I;16B", MM, 1, 1, (16,), None), + "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), + "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), +} + + +def _save(im, fp, filename): + + try: + rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] + except KeyError: + raise IOError("cannot write mode %s as TIFF" % im.mode) + + ifd = ImageFileDirectory_v2(prefix=prefix) + + compression = im.encoderinfo.get('compression', + im.info.get('compression', 'raw')) + + libtiff = WRITE_LIBTIFF or compression != 'raw' + + # required for color libtiff images + ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1) + + ifd[IMAGEWIDTH] = im.size[0] + ifd[IMAGELENGTH] = im.size[1] + + # write any arbitrary tags passed in as an ImageFileDirectory + info = im.encoderinfo.get("tiffinfo", {}) + if DEBUG: + print("Tiffinfo Keys: %s" % list(info)) + if isinstance(info, ImageFileDirectory_v1): + info = info.to_v2() + for key in info: + ifd[key] = info.get(key) + try: + ifd.tagtype[key] = info.tagtype[key] + except: + pass # might not be an IFD, Might not have populated type + + # additions written by Greg Couch, gregc@cgl.ucsf.edu + # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com + if hasattr(im, 'tag_v2'): + # preserve tags from original TIFF image file + for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, + IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): + if key in im.tag_v2: + ifd[key] = im.tag_v2[key] + ifd.tagtype[key] = im.tag_v2.tagtype.get(key, None) + + # preserve ICC profile (should also work when saving other formats + # which support profiles as TIFF) -- 2008-06-06 Florian Hoech + if "icc_profile" in im.info: + ifd[ICCPROFILE] = im.info["icc_profile"] + + for key, name in [(IMAGEDESCRIPTION, "description"), + (X_RESOLUTION, "resolution"), + (Y_RESOLUTION, "resolution"), + (X_RESOLUTION, "x_resolution"), + (Y_RESOLUTION, "y_resolution"), + (RESOLUTION_UNIT, "resolution_unit"), + (SOFTWARE, "software"), + (DATE_TIME, "date_time"), + (ARTIST, "artist"), + (COPYRIGHT, "copyright")]: + name_with_spaces = name.replace("_", " ") + if "_" in name and name_with_spaces in im.encoderinfo: + warnings.warn("%r is deprecated; use %r instead" % + (name_with_spaces, name), DeprecationWarning) + ifd[key] = im.encoderinfo[name.replace("_", " ")] + if name in im.encoderinfo: + ifd[key] = im.encoderinfo[name] + + dpi = im.encoderinfo.get("dpi") + if dpi: + ifd[RESOLUTION_UNIT] = 2 + ifd[X_RESOLUTION] = dpi[0] + ifd[Y_RESOLUTION] = dpi[1] + + if bits != (1,): + ifd[BITSPERSAMPLE] = bits + if len(bits) != 1: + ifd[SAMPLESPERPIXEL] = len(bits) + if extra is not None: + ifd[EXTRASAMPLES] = extra + if format != 1: + ifd[SAMPLEFORMAT] = format + + ifd[PHOTOMETRIC_INTERPRETATION] = photo + + if im.mode == "P": + lut = im.im.getpalette("RGB", "RGB;L") + ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut) + + # data orientation + stride = len(bits) * ((im.size[0]*bits[0]+7)//8) + ifd[ROWSPERSTRIP] = im.size[1] + ifd[STRIPBYTECOUNTS] = stride * im.size[1] + ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer + # no compression by default: + ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) + + if libtiff: + if DEBUG: + print("Saving using libtiff encoder") + print("Items: %s" % sorted(ifd.items())) + _fp = 0 + if hasattr(fp, "fileno"): + try: + fp.seek(0) + _fp = os.dup(fp.fileno()) + except io.UnsupportedOperation: + pass + + # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library + # based on the data in the strip. + blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS] + atts = {} + # bits per sample is a single short in the tiff directory, not a list. + atts[BITSPERSAMPLE] = bits[0] + # Merge the ones that we have with (optional) more bits from + # the original file, e.g x,y resolution so that we can + # save(load('')) == original file. + legacy_ifd = {} + if hasattr(im, 'tag'): + legacy_ifd = im.tag.to_v2() + for tag, value in itertools.chain(ifd.items(), + getattr(im, 'tag_v2', {}).items(), + legacy_ifd.items()): + # Libtiff can only process certain core items without adding + # them to the custom dictionary. It will segfault if it attempts + # to add a custom tag without the dictionary entry + # + # UNDONE -- add code for the custom dictionary + if tag not in TiffTags.LIBTIFF_CORE: + continue + if tag not in atts and tag not in blocklist: + if isinstance(value, unicode if bytes is str else str): + atts[tag] = value.encode('ascii', 'replace') + b"\0" + elif isinstance(value, IFDRational): + atts[tag] = float(value) + else: + atts[tag] = value + + if DEBUG: + print("Converted items: %s" % sorted(atts.items())) + + # libtiff always expects the bytes in native order. + # we're storing image byte order. So, if the rawmode + # contains I;16, we need to convert from native to image + # byte order. + if im.mode in ('I;16B', 'I;16'): + rawmode = 'I;16N' + + a = (rawmode, compression, _fp, filename, atts) + # print(im.mode, compression, a, im.encoderconfig) + e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) + e.setimage(im.im, (0, 0)+im.size) + while True: + # undone, change to self.decodermaxblock: + l, s, d = e.encode(16*1024) + if not _fp: + fp.write(d) + if s: + break + if s < 0: + raise IOError("encoder error %d when writing image file" % s) + + else: + offset = ifd.save(fp) + + ImageFile._save(im, fp, [ + ("raw", (0, 0)+im.size, offset, (rawmode, stride, 1)) + ]) + + # -- helper for multi-page save -- + if "_debug_multipage" in im.encoderinfo: + # just to access o32 and o16 (using correct byte order) + im._debug_multipage = ifd + +# +# -------------------------------------------------------------------- +# Register + +Image.register_open(TiffImageFile.format, TiffImageFile, _accept) +Image.register_save(TiffImageFile.format, _save) + +Image.register_extension(TiffImageFile.format, ".tif") +Image.register_extension(TiffImageFile.format, ".tiff") + +Image.register_mime(TiffImageFile.format, "image/tiff") From 4e958509be1ad3035c8c888c9d8234d1e83c8668 Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sat, 1 Jun 2024 14:34:24 +0000 Subject: [PATCH 03/16] XXX disable PyImaging_MapBuffer --- src/PIL/ImageFile.py | 3 +++ src/_imaging.c | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 69e7ee54811..c8ad69f9b06 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -206,6 +206,9 @@ def load(self): except AttributeError: seek = self.fp.seek + # XXX hack202406 disable unmodified code path + use_mmap = False + if use_mmap: # try memory mapping decoder_name, extents, offset, args = self.tile[0] diff --git a/src/_imaging.c b/src/_imaging.c index ddc8d288550..caf02029dcd 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -96,7 +96,8 @@ /* Configuration stuff. Feel free to undef things you don't need. */ #define WITH_IMAGECHOPS /* ImageChops support */ #define WITH_IMAGEDRAW /* ImageDraw support */ -#define WITH_MAPPING /* use memory mapping to read some file formats */ +// XXX hack202406 disable unmodified code path +// #define WITH_MAPPING /* use memory mapping to read some file formats */ #define WITH_IMAGEPATH /* ImagePath stuff */ #define WITH_ARROW /* arrow graphics stuff (experimental) */ #define WITH_EFFECTS /* special effects */ From fb49794d6e3c1cf989831d36f99649f5ff87543e Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sat, 1 Jun 2024 14:56:34 +0000 Subject: [PATCH 04/16] TIFF sample format table --- src/PIL/TiffImagePlugin.py | 46 ++++++++++++++++++++++++++++++++------ src/_imaging.c | 4 ++-- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index b89144803a5..d2ab012f0fd 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -60,6 +60,7 @@ from .TiffTags import TYPES logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) # XXX hack202406 # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False @@ -182,6 +183,43 @@ (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), + (II, 1, (1, 1), 1, (16, 16), (0,)): ("I;16", "I;16"), + ( + II, + 1, + (1, 1, 1), + 1, + (16, 16, 16), + ( + 0, + 0, + ), + ): ("I;16", "I;16"), + ( + II, + 1, + (1, 1, 1, 1), + 1, + (16, 16, 16, 16), + ( + 0, + 0, + 0, + ), + ): ("I;16", "I;16"), + ( + II, + 1, + (1, 1, 1, 1, 1), + 1, + (16, 16, 16, 16, 16), + ( + 0, + 0, + 0, + 0, + ), + ): ("I;16", "I;16"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), @@ -1379,13 +1417,7 @@ def _setup(self): logger.debug("- size: %s", self.size) sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) - if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1: - # SAMPLEFORMAT is properly per band, so an RGB image will - # be (1,1,1). But, we don't support per band pixel types, - # and anything more than one band is a uint8. So, just - # take the first element. Revisit this if adding support - # for more exotic images. - sample_format = (1,) + logger.debug("- sample_format: %s", sample_format) bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) diff --git a/src/_imaging.c b/src/_imaging.c index caf02029dcd..ded88c2f2a9 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -94,8 +94,8 @@ #include /* Configuration stuff. Feel free to undef things you don't need. */ -#define WITH_IMAGECHOPS /* ImageChops support */ -#define WITH_IMAGEDRAW /* ImageDraw support */ +#define WITH_IMAGECHOPS /* ImageChops support */ +#define WITH_IMAGEDRAW /* ImageDraw support */ // XXX hack202406 disable unmodified code path // #define WITH_MAPPING /* use memory mapping to read some file formats */ #define WITH_IMAGEPATH /* ImagePath stuff */ From a878715fe7c681da73d8e42fa1cd6d4c25a220be Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sat, 1 Jun 2024 18:06:14 +0000 Subject: [PATCH 05/16] introduce multi-band format (TIFF only) --- Tests/test_file_tiff.py | 13 +++++++++++ src/PIL/ImageFile.py | 4 +++- src/PIL/TiffImagePlugin.py | 13 +++++++---- src/_imaging.c | 46 ++++++++++++++++++++++++++++++++----- src/decode.c | 25 +++++++++++++++++++- src/libImaging/Access.c | 11 +++++++++ src/libImaging/Imaging.h | 16 +++++++++---- src/libImaging/Point.c | 4 ++-- src/libImaging/RankFilter.c | 3 ++- src/libImaging/RawDecode.c | 5 ++-- src/libImaging/Storage.c | 33 ++++++++++++++++++-------- src/map.c | 3 ++- 12 files changed, 142 insertions(+), 34 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 39ae7f11dd4..4219d4a6c91 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -892,6 +892,19 @@ def test_oom(self, test_file: str) -> None: with Image.open(test_file): pass + def test_open_tiff_uint16_multiband(self): + """Test opening multiband TIFFs and reading all channels.""" + base_value = 4660 + for i in range(1, 6): + infile = f"Tests/images/uint16_{i}_{base_value}.tif" + im = Image.open(infile) + im.load() + pixel = tuple([base_value + j for j in range(0, i)]) + actual_pixel = im.getpixel((0, 0)) + if isinstance(actual_pixel, int): + actual_pixel = (actual_pixel,) + assert actual_pixel == pixel + @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index c8ad69f9b06..720702ab3ab 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -118,6 +118,8 @@ def __init__(self, fp=None, filename=None): self.readonly = 1 # until we know better + self.newconfig = () + self.decoderconfig = () self.decodermaxblock = MAXBLOCK @@ -319,7 +321,7 @@ def load(self): def load_prepare(self) -> None: # create image memory if necessary if not self.im or self.im.mode != self.mode or self.im.size != self.size: - self.im = Image.core.new(self.mode, self.size) + self.im = Image.core.new(self.mode, self.size, *self.newconfig) # create palette (optional) if self.mode == "P": Image.Image.load(self) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d2ab012f0fd..0eeeb551a82 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -60,7 +60,7 @@ from .TiffTags import TYPES logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) # XXX hack202406 +logger.setLevel(logging.DEBUG) # XXX hack202406 # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False @@ -183,7 +183,7 @@ (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"), (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), - (II, 1, (1, 1), 1, (16, 16), (0,)): ("I;16", "I;16"), + (II, 1, (1, 1), 1, (16, 16), (0,)): ("MB", "MB"), ( II, 1, @@ -194,7 +194,7 @@ 0, 0, ), - ): ("I;16", "I;16"), + ): ("MB", "MB"), ( II, 1, @@ -206,7 +206,7 @@ 0, 0, ), - ): ("I;16", "I;16"), + ): ("MB", "MB"), ( II, 1, @@ -219,7 +219,7 @@ 0, 0, ), - ): ("I;16", "I;16"), + ): ("MB", "MB"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"), (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), @@ -1474,6 +1474,9 @@ def _setup(self): logger.debug("- raw mode: %s", rawmode) logger.debug("- pil mode: %s", self.mode) + if self.mode == "MB": + assert max(bps_tuple) == min(bps_tuple) + self.newconfig = (max(bps_tuple), samples_per_pixel) self.info["compression"] = self._compression diff --git a/src/_imaging.c b/src/_imaging.c index ded88c2f2a9..e3647422fb2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -310,7 +310,7 @@ getbands(const char *mode) { int bands; /* FIXME: add primitive to libImaging to avoid extra allocation */ - im = ImagingNew(mode, 0, 0); + im = ImagingNew(mode, 0, 0, -1, -1); if (!im) { return -1; } @@ -434,6 +434,35 @@ float16tofloat32(const FLOAT16 in) { return out[0]; } +static inline PyObject * +getpixel_mb(Imaging im, ImagingAccess access, int x, int y) { + UINT8 pixel[im->pixelsize]; + access->get_pixel(im, x, y, &pixel); + + PyObject *tuple = PyTuple_New(im->bands); + if (tuple == NULL) { + return NULL; + } + + UINT8 *pos = pixel; + for (int i = 0; i < im->bands; ++i) { + switch (im->depth) { + case CHAR_BIT: + PyTuple_SET_ITEM(tuple, i, PyLong_FromLong(*pos)); + break; + case 2 * CHAR_BIT: + PyTuple_SET_ITEM(tuple, i, PyLong_FromLong(*(UINT16 *)pos)); + break; + case 4 * CHAR_BIT: + PyTuple_SET_ITEM(tuple, i, PyLong_FromLong(*(INT32 *)pos)); + break; + } + pos += im->depth / CHAR_BIT; + } + + return tuple; +} + static inline PyObject * getpixel(Imaging im, ImagingAccess access, int x, int y) { union { @@ -455,6 +484,10 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { return NULL; } + if (im->type == IMAGING_TYPE_MB) { + return getpixel_mb(im, access, x, y); + } + access->get_pixel(im, x, y, &pixel); switch (im->type) { @@ -685,13 +718,13 @@ _fill(PyObject *self, PyObject *args) { static PyObject * _new(PyObject *self, PyObject *args) { char *mode; - int xsize, ysize; + int xsize, ysize, depth = -1, bands = -1; - if (!PyArg_ParseTuple(args, "s(ii)", &mode, &xsize, &ysize)) { + if (!PyArg_ParseTuple(args, "s(ii)|ii", &mode, &xsize, &ysize, &depth, &bands)) { return NULL; } - return PyImagingNew(ImagingNew(mode, xsize, ysize)); + return PyImagingNew(ImagingNew(mode, xsize, ysize, depth, bands)); } static PyObject * @@ -1714,7 +1747,8 @@ _quantize(ImagingObject *self, PyObject *args) { if (!self->image->xsize || !self->image->ysize) { /* no content; return an empty image */ - return PyImagingNew(ImagingNew("P", self->image->xsize, self->image->ysize)); + return PyImagingNew( + ImagingNew("P", self->image->xsize, self->image->ysize, -1, -1)); } return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); @@ -2782,7 +2816,7 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { return NULL; } - im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize); + im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize, -1, -1); if (!im) { free(text); return ImagingError_MemoryError(); diff --git a/src/decode.c b/src/decode.c index ea2f3af8012..af08010188a 100644 --- a/src/decode.c +++ b/src/decode.c @@ -291,11 +291,33 @@ static PyTypeObject ImagingDecoderType = { /* -------------------------------------------------------------------- */ -int +static void +mb_shuffle_passthru(UINT8 *dst, const UINT8 *src, Imaging im, ImagingCodecState state) { + state->shuffle(dst, src, state->xsize); +} + +static void +shuffle_mb_unavail(UINT8 *dst, const UINT8 *src, int pixels) { + abort(); +} + +static void +mb_shuffle(UINT8 *dst, const UINT8 *src, Imaging im, ImagingCodecState state) { + memcpy(dst, src, state->xsize * im->pixelsize); +} + +static int get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmode) { int bits; ImagingShuffler unpack; + if (strcmp(mode, IMAGING_MODE_MB) == 0) { + decoder->state.shuffle = shuffle_mb_unavail; + decoder->state.mb_shuffle = mb_shuffle; + decoder->state.bits = -1; + return 0; + } + unpack = ImagingFindUnpacker(mode, rawmode, &bits); if (!unpack) { Py_DECREF(decoder); @@ -304,6 +326,7 @@ get_unpacker(ImagingDecoderObject *decoder, const char *mode, const char *rawmod } decoder->state.shuffle = unpack; + decoder->state.mb_shuffle = mb_shuffle_passthru; decoder->state.bits = bits; return 0; diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 3a5e918e837..f567bdbabdb 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -133,6 +133,11 @@ get_pixel_32B(Imaging im, int x, int y, void *color) { #endif } +static void +get_pixel_mb(Imaging im, int x, int y, void *color) { + memcpy(color, &im->image[y][x * im->pixelsize], im->pixelsize); +} + /* store individual pixel */ static void @@ -183,6 +188,11 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { memcpy(&im->image32[y][x], color, sizeof(INT32)); } +static void +put_pixel_mb(Imaging im, int x, int y, void *color) { + memcpy(&im->image[y][x * im->pixelsize], color, im->pixelsize); +} + void ImagingAccessInit() { #define ADD(mode_, get_pixel_, put_pixel_) \ @@ -222,6 +232,7 @@ ImagingAccessInit() { ADD("YCbCr", get_pixel_32, put_pixel_32); ADD("LAB", get_pixel_32, put_pixel_32); ADD("HSV", get_pixel_32, put_pixel_32); + ADD("MB", get_pixel_mb, put_pixel_mb); } ImagingAccess diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 1f2c03e934f..ee2f6ea9d17 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -68,10 +68,13 @@ typedef struct ImagingPaletteInstance *ImagingPalette; #define IMAGING_TYPE_INT32 1 #define IMAGING_TYPE_FLOAT32 2 #define IMAGING_TYPE_SPECIAL 3 /* check mode for details */ +#define IMAGING_TYPE_MB 4 /* multi-band format */ #define IMAGING_MODE_LENGTH \ 6 + 1 /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", "YCbCr", "BGR;xy") */ +#define IMAGING_MODE_MB "MB" /* multi-band format */ + typedef struct { char *ptr; int size; @@ -80,9 +83,9 @@ typedef struct { struct ImagingMemoryInstance { /* Format */ char mode[IMAGING_MODE_LENGTH]; /* Band names ("1", "L", "P", "RGB", "RGBA", "CMYK", - "YCbCr", "BGR;xy") */ + "YCbCr", "BGR;xy", "MB") */ int type; /* Data type (IMAGING_TYPE_*) */ - int depth; /* Depth (ignored in this version) */ + int depth; /* Sample size (1, 2, or 4) in multi-band format */ int bands; /* Number of bands (1, 2, 3, or 4) */ int xsize; /* Image dimension. */ int ysize; @@ -173,7 +176,7 @@ extern void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); extern Imaging -ImagingNew(const char *mode, int xsize, int ysize); +ImagingNew(const char *mode, int xsize, int ysize, int depth, int bands); extern Imaging ImagingNewDirty(const char *mode, int xsize, int ysize); extern Imaging @@ -185,9 +188,10 @@ extern Imaging ImagingNewBlock(const char *mode, int xsize, int ysize); extern Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize); +ImagingNewPrologue(const char *mode, int xsize, int ysize, int depth, int bands); extern Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int structure_size); +ImagingNewPrologueSubtype( + const char *mode, int xsize, int ysize, int depth, int bands, int structure_size); extern void ImagingCopyPalette(Imaging destination, Imaging source); @@ -663,6 +667,8 @@ struct ImagingCodecStateInstance { int ystep; int xsize, ysize, xoff, yoff; ImagingShuffler shuffle; + void (*mb_shuffle)( + UINT8 *dst, const UINT8 *src, Imaging im, ImagingCodecState state); int bits, bytes; UINT8 *buffer; void *context; diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index dd06f3940d1..3d54008cbc3 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -152,7 +152,7 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { goto mode_mismatch; } - imOut = ImagingNew(mode, imIn->xsize, imIn->ysize); + imOut = ImagingNew(mode, imIn->xsize, imIn->ysize, -1, -1); if (!imOut) { return NULL; } @@ -214,7 +214,7 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) { return (Imaging)ImagingError_ModeError(); } - imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize); + imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize, -1, -1); if (!imOut) { return NULL; } diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index 73a6baecbb2..da40c892f11 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -84,7 +84,8 @@ MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) return (Imaging)ImagingError_ValueError("bad rank value"); } - imOut = ImagingNew(im->mode, im->xsize - 2 * margin, im->ysize - 2 * margin); + imOut = + ImagingNew(im->mode, im->xsize - 2 * margin, im->ysize - 2 * margin, -1, -1); if (!imOut) { return NULL; } diff --git a/src/libImaging/RawDecode.c b/src/libImaging/RawDecode.c index 24abe48041f..8d8d240512e 100644 --- a/src/libImaging/RawDecode.c +++ b/src/libImaging/RawDecode.c @@ -71,10 +71,11 @@ ImagingRawDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt } /* Unpack data */ - state->shuffle( + state->mb_shuffle( (UINT8 *)im->image[state->y + state->yoff] + state->xoff * im->pixelsize, ptr, - state->xsize); + im, + state); ptr += state->bytes; bytes -= state->bytes; diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index b27195a3587..cdf62ceb3c7 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -42,7 +42,8 @@ */ Imaging -ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { +ImagingNewPrologueSubtype( + const char *mode, int xsize, int ysize, int depth, int bands, int size) { Imaging im; /* linesize overflow check, roughly the current largest space req'd */ @@ -190,6 +191,17 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->pixelsize = 4; im->linesize = xsize * 4; + } else if (strcmp(mode, IMAGING_MODE_MB) == 0) { + if (bands <= 0 || depth <= 0) { + return (Imaging)ImagingError_ValueError( + "multi-band missing bands and depth"); + } + im->bands = bands; + im->depth = depth; + im->pixelsize = depth * bands; + im->linesize = xsize * im->pixelsize; + im->type = IMAGING_TYPE_MB; + } else { free(im); return (Imaging)ImagingError_ValueError("unrecognized image mode"); @@ -225,9 +237,9 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize) { +ImagingNewPrologue(const char *mode, int xsize, int ysize, int depth, int bands) { return ImagingNewPrologueSubtype( - mode, xsize, ysize, sizeof(struct ImagingMemoryInstance)); + mode, xsize, ysize, depth, bands, sizeof(struct ImagingMemoryInstance)); } void @@ -485,15 +497,16 @@ ImagingAllocateBlock(Imaging im) { * Create a new, internally allocated, image. */ -Imaging -ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { +static Imaging +ImagingNewInternal( + const char *mode, int xsize, int ysize, int depth, int bands, int dirty) { Imaging im; if (xsize < 0 || ysize < 0) { return (Imaging)ImagingError_ValueError("bad image size"); } - im = ImagingNewPrologue(mode, xsize, ysize); + im = ImagingNewPrologue(mode, xsize, ysize, depth, bands); if (!im) { return NULL; } @@ -514,13 +527,13 @@ ImagingNewInternal(const char *mode, int xsize, int ysize, int dirty) { } Imaging -ImagingNew(const char *mode, int xsize, int ysize) { - return ImagingNewInternal(mode, xsize, ysize, 0); +ImagingNew(const char *mode, int xsize, int ysize, int depth, int bands) { + return ImagingNewInternal(mode, xsize, ysize, depth, bands, 0); } Imaging ImagingNewDirty(const char *mode, int xsize, int ysize) { - return ImagingNewInternal(mode, xsize, ysize, 1); + return ImagingNewInternal(mode, xsize, ysize, -1, -1, 1); } Imaging @@ -531,7 +544,7 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { return (Imaging)ImagingError_ValueError("bad image size"); } - im = ImagingNewPrologue(mode, xsize, ysize); + im = ImagingNewPrologue(mode, xsize, ysize, -1, -1); if (!im) { return NULL; } diff --git a/src/map.c b/src/map.c index c298bd1482a..7b4514751f6 100644 --- a/src/map.c +++ b/src/map.c @@ -119,7 +119,8 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { return NULL; } - im = ImagingNewPrologueSubtype(mode, xsize, ysize, sizeof(ImagingBufferInstance)); + im = ImagingNewPrologueSubtype( + mode, xsize, ysize, -1, -1, sizeof(ImagingBufferInstance)); if (!im) { PyBuffer_Release(&view); return NULL; From 3d7c45a9c0bda10c78af6171504df6f72ed90259 Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sat, 1 Jun 2024 20:32:41 +0000 Subject: [PATCH 06/16] re-enable PyImaging_MapBuffer --- src/PIL/ImageFile.py | 5 +---- src/PIL/TiffImagePlugin.py | 2 -- src/_imaging.c | 7 +++---- src/map.c | 15 ++++++++++----- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 720702ab3ab..700f3b106a2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -208,9 +208,6 @@ def load(self): except AttributeError: seek = self.fp.seek - # XXX hack202406 disable unmodified code path - use_mmap = False - if use_mmap: # try memory mapping decoder_name, extents, offset, args = self.tile[0] @@ -232,7 +229,7 @@ def load(self): msg = "buffer is not large enough" raise OSError(msg) self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, offset, args + self.map, self.size, decoder_name, offset, args, *self.newconfig ) readonly = 1 # After trashing self.im, diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0eeeb551a82..90f671032e3 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1417,8 +1417,6 @@ def _setup(self): logger.debug("- size: %s", self.size) sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,)) - logger.debug("- sample_format: %s", sample_format) - bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) if photo in (2, 6, 8): # RGB, YCbCr, LAB diff --git a/src/_imaging.c b/src/_imaging.c index e3647422fb2..a8aabc0ddc5 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -94,10 +94,9 @@ #include /* Configuration stuff. Feel free to undef things you don't need. */ -#define WITH_IMAGECHOPS /* ImageChops support */ -#define WITH_IMAGEDRAW /* ImageDraw support */ -// XXX hack202406 disable unmodified code path -// #define WITH_MAPPING /* use memory mapping to read some file formats */ +#define WITH_IMAGECHOPS /* ImageChops support */ +#define WITH_IMAGEDRAW /* ImageDraw support */ +#define WITH_MAPPING /* use memory mapping to read some file formats */ #define WITH_IMAGEPATH /* ImagePath stuff */ #define WITH_ARROW /* arrow graphics stuff (experimental) */ #define WITH_EFFECTS /* special effects */ diff --git a/src/map.c b/src/map.c index 7b4514751f6..46db0aedee2 100644 --- a/src/map.c +++ b/src/map.c @@ -61,10 +61,11 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { int xsize, ysize; int stride; int ystep; + int depth = -1, bands = -1; if (!PyArg_ParseTuple( args, - "O(ii)sn(sii)", + "O(ii)sn(sii)|ii", &target, &xsize, &ysize, @@ -72,7 +73,9 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { &offset, &mode, &stride, - &ystep)) { + &ystep, + &depth, + &bands)) { return NULL; } @@ -82,10 +85,12 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } if (stride <= 0) { - if (!strcmp(mode, "L") || !strcmp(mode, "P")) { + if (strcmp(mode, "L") == 0 || strcmp(mode, "P") == 0) { stride = xsize; - } else if (!strncmp(mode, "I;16", 4)) { + } else if (strncmp(mode, "I;16", 4) == 0) { stride = xsize * 2; + } else if (strcmp(mode, IMAGING_MODE_MB) == 0) { + stride = xsize * depth * bands; } else { stride = xsize * 4; } @@ -120,7 +125,7 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } im = ImagingNewPrologueSubtype( - mode, xsize, ysize, -1, -1, sizeof(ImagingBufferInstance)); + mode, xsize, ysize, depth, bands, sizeof(ImagingBufferInstance)); if (!im) { PyBuffer_Release(&view); return NULL; From ac7a915fe6d95622b2dc1d69b207db9b0d37daaf Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sat, 1 Jun 2024 21:29:37 +0000 Subject: [PATCH 07/16] TIFF more entries in OPEN_INFO --- src/PIL/TiffImagePlugin.py | 9 +++++++++ src/libImaging/Access.c | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 90f671032e3..9d3c3c0f86f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -234,7 +234,9 @@ (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, (1, 1, 1), 1, (8, 8, 8), ()): ("RGB", "RGB"), (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (MM, 2, (1, 1, 1), 1, (8, 8, 8), ()): ("RGB", "RGB"), (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples @@ -247,11 +249,13 @@ (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (MM, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), @@ -260,13 +264,16 @@ (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), + (II, 2, (1, 1, 1), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), + (II, 2, (1, 1, 1, 1), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), + (MM, 2, (1, 1, 1, 1), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), @@ -300,9 +307,11 @@ # JPEG compressed images handled by LibTiff and auto-converted to RGBX # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), + (II, 6, (1, 1, 1), 1, (8, 8, 8), ()): ("RGB", "RGBX"), (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), + # XXX hack202406 these entries allow all TIFF tests to pass, but more may be needed } MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index f567bdbabdb..1a5603a140b 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -189,7 +189,7 @@ put_pixel_32(Imaging im, int x, int y, const void *color) { } static void -put_pixel_mb(Imaging im, int x, int y, void *color) { +put_pixel_mb(Imaging im, int x, int y, const void *color) { memcpy(&im->image[y][x * im->pixelsize], color, im->pixelsize); } From fb2fcd8e2ab198888180aec97b1e0ffea6ca1e50 Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sat, 1 Jun 2024 22:41:57 +0000 Subject: [PATCH 08/16] cgetpixel_mb constant size buffer --- src/_imaging.c | 3 ++- src/libImaging/Storage.c | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index a8aabc0ddc5..c9818f50baa 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -435,7 +435,8 @@ float16tofloat32(const FLOAT16 in) { static inline PyObject * getpixel_mb(Imaging im, ImagingAccess access, int x, int y) { - UINT8 pixel[im->pixelsize]; + UINT8 pixel[sizeof(INT32) * 6]; + assert(im->pixelsize <= sizeof(pixel)); access->get_pixel(im, x, y, &pixel); PyObject *tuple = PyTuple_New(im->bands); diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index cdf62ceb3c7..382dbfa49be 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -198,7 +198,7 @@ ImagingNewPrologueSubtype( } im->bands = bands; im->depth = depth; - im->pixelsize = depth * bands; + im->pixelsize = depth / CHAR_BIT * bands; im->linesize = xsize * im->pixelsize; im->type = IMAGING_TYPE_MB; From 76046019f0399d9d43cca375fe78a22e5c5259f9 Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sat, 1 Jun 2024 22:42:11 +0000 Subject: [PATCH 09/16] mb_shuffle big endian --- src/decode.c | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/decode.c b/src/decode.c index af08010188a..3a9c77c611b 100644 --- a/src/decode.c +++ b/src/decode.c @@ -303,7 +303,33 @@ shuffle_mb_unavail(UINT8 *dst, const UINT8 *src, int pixels) { static void mb_shuffle(UINT8 *dst, const UINT8 *src, Imaging im, ImagingCodecState state) { - memcpy(dst, src, state->xsize * im->pixelsize); + int size = state->xsize * im->pixelsize; +#ifdef WORDS_BIGENDIAN + switch (im->depth) { + default: + abort(); + return; + case 4 * CHAR_BIT: { + for (int i = 0; i < size; i += 4) { + dst[i] = src[i + 3]; + dst[i + 1] = src[i + 2]; + dst[i + 2] = src[i + 1]; + dst[i + 3] = src[i]; + } + return; + } + case 2 * CHAR_BIT: { + for (int i = 0; i < size; i += 2) { + dst[i] = src[i + 1]; + dst[i + 1] = src[i]; + } + return; + case CHAR_BIT: + // fallthrough + } + } +#endif + memcpy(dst, src, size); } static int From 05840543699a420d329a01def145a618fac6bbef Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sun, 2 Jun 2024 12:27:51 +0000 Subject: [PATCH 10/16] copy() with multi-band format --- Tests/test_file_tiff.py | 18 +++++-- src/_imaging.c | 31 +++++++----- src/libImaging/AlphaComposite.c | 3 +- src/libImaging/Bands.c | 7 +-- src/libImaging/Blend.c | 3 +- src/libImaging/BoxBlur.c | 3 +- src/libImaging/Chops.c | 2 +- src/libImaging/Crop.c | 2 +- src/libImaging/Effects.c | 6 +-- src/libImaging/Fill.c | 4 +- src/libImaging/Filter.c | 5 +- src/libImaging/Imaging.h | 16 ++++-- src/libImaging/Matrix.c | 4 +- src/libImaging/ModeFilter.c | 2 +- src/libImaging/Negative.c | 2 +- src/libImaging/Offset.c | 2 +- src/libImaging/Point.c | 4 +- src/libImaging/Quant.c | 2 +- src/libImaging/RankFilter.c | 4 +- src/libImaging/Reduce.c | 4 +- src/libImaging/Resample.c | 5 +- src/libImaging/Storage.c | 90 ++++++++++++++++----------------- src/map.c | 4 +- 23 files changed, 126 insertions(+), 97 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4219d4a6c91..a4366f7fbb7 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -894,16 +894,24 @@ def test_oom(self, test_file: str) -> None: def test_open_tiff_uint16_multiband(self): """Test opening multiband TIFFs and reading all channels.""" + + def getpixel00(im: Image.Image): + actual_pixel = im.getpixel((0, 0)) + if isinstance(actual_pixel, int): + actual_pixel = (actual_pixel,) + return actual_pixel + base_value = 4660 for i in range(1, 6): + pixel = tuple([base_value + j for j in range(0, i)]) infile = f"Tests/images/uint16_{i}_{base_value}.tif" im = Image.open(infile) + im.load() - pixel = tuple([base_value + j for j in range(0, i)]) - actual_pixel = im.getpixel((0, 0)) - if isinstance(actual_pixel, int): - actual_pixel = (actual_pixel,) - assert actual_pixel == pixel + assert getpixel00(im) == pixel + + im.copy() + assert getpixel00(im) == pixel @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/src/_imaging.c b/src/_imaging.c index c9818f50baa..b216f41f937 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -309,7 +309,7 @@ getbands(const char *mode) { int bands; /* FIXME: add primitive to libImaging to avoid extra allocation */ - im = ImagingNew(mode, 0, 0, -1, -1); + im = ImagingNew(mode, (ImagingNewParams){0, 0}); if (!im) { return -1; } @@ -697,7 +697,7 @@ _fill(PyObject *self, PyObject *args) { return NULL; } - im = ImagingNewDirty(mode, xsize, ysize); + im = ImagingNewDirty(mode, (ImagingNewParams){xsize, ysize}); if (!im) { return NULL; } @@ -724,7 +724,8 @@ _new(PyObject *self, PyObject *args) { return NULL; } - return PyImagingNew(ImagingNew(mode, xsize, ysize, depth, bands)); + return PyImagingNew( + ImagingNew(mode, (ImagingNewParams){xsize, ysize, depth, bands})); } static PyObject * @@ -940,7 +941,8 @@ _color_lut_3d(ImagingObject *self, PyObject *args) { return NULL; } - imOut = ImagingNewDirty(mode, self->image->xsize, self->image->ysize); + imOut = ImagingNewDirty( + mode, (ImagingNewParams){self->image->xsize, self->image->ysize}); if (!imOut) { free(prepared_table); return NULL; @@ -1127,7 +1129,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { } imIn = self->image; - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!imOut) { return NULL; } @@ -1747,8 +1749,8 @@ _quantize(ImagingObject *self, PyObject *args) { if (!self->image->xsize || !self->image->ysize) { /* no content; return an empty image */ - return PyImagingNew( - ImagingNew("P", self->image->xsize, self->image->ysize, -1, -1)); + return PyImagingNew(ImagingNew( + "P", (ImagingNewParams){self->image->xsize, self->image->ysize})); } return PyImagingNew(ImagingQuantize(self->image, colours, method, kmeans)); @@ -1955,7 +1957,7 @@ _resize(ImagingObject *self, PyObject *args) { a[2] = box[0]; a[5] = box[1]; - imOut = ImagingNewDirty(imIn->mode, xsize, ysize); + imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){xsize, ysize}); imOut = ImagingTransform( imOut, imIn, IMAGING_TRANSFORM_AFFINE, 0, 0, xsize, ysize, a, filter, 1); @@ -2140,13 +2142,15 @@ _transpose(ImagingObject *self, PyObject *args) { case 0: /* flip left right */ case 1: /* flip top bottom */ case 3: /* rotate 180 */ - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty( + imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); break; case 2: /* rotate 90 */ case 4: /* rotate 270 */ case 5: /* transpose */ case 6: /* transverse */ - imOut = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); + imOut = ImagingNewDirty( + imIn->mode, (ImagingNewParams){imIn->ysize, imIn->xsize}); break; default: PyErr_SetString(PyExc_ValueError, "No such transpose operation"); @@ -2195,7 +2199,7 @@ _unsharp_mask(ImagingObject *self, PyObject *args) { } imIn = self->image; - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!imOut) { return NULL; } @@ -2220,7 +2224,7 @@ _box_blur(ImagingObject *self, PyObject *args) { } imIn = self->image; - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!imOut) { return NULL; } @@ -2816,7 +2820,8 @@ _font_getmask(ImagingFontObject *self, PyObject *args) { return NULL; } - im = ImagingNew(self->bitmap->mode, textwidth(self, text), self->ysize, -1, -1); + im = ImagingNew( + self->bitmap->mode, (ImagingNewParams){textwidth(self, text), self->ysize}); if (!im) { free(text); return ImagingError_MemoryError(); diff --git a/src/libImaging/AlphaComposite.c b/src/libImaging/AlphaComposite.c index 6d728f9088b..b47441a6a48 100644 --- a/src/libImaging/AlphaComposite.c +++ b/src/libImaging/AlphaComposite.c @@ -36,7 +36,8 @@ ImagingAlphaComposite(Imaging imDst, Imaging imSrc) { return ImagingError_Mismatch(); } - imOut = ImagingNewDirty(imDst->mode, imDst->xsize, imDst->ysize); + imOut = + ImagingNewDirty(imDst->mode, (ImagingNewParams){imDst->xsize, imDst->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Bands.c b/src/libImaging/Bands.c index e1b16b34ac0..d9b31ce1dc0 100644 --- a/src/libImaging/Bands.c +++ b/src/libImaging/Bands.c @@ -41,7 +41,7 @@ ImagingGetBand(Imaging imIn, int band) { band = 3; } - imOut = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty("L", (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!imOut) { return NULL; } @@ -82,7 +82,7 @@ ImagingSplit(Imaging imIn, Imaging bands[4]) { } for (i = 0; i < imIn->bands; i++) { - bands[i] = ImagingNewDirty("L", imIn->xsize, imIn->ysize); + bands[i] = ImagingNewDirty("L", (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!bands[i]) { for (j = 0; j < i; ++j) { ImagingDelete(bands[j]); @@ -265,7 +265,8 @@ ImagingMerge(const char *mode, Imaging bands[4]) { } bandsCount = i; - imOut = ImagingNewDirty(mode, firstBand->xsize, firstBand->ysize); + imOut = + ImagingNewDirty(mode, (ImagingNewParams){firstBand->xsize, firstBand->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Blend.c b/src/libImaging/Blend.c index a53ae0fad53..444bf7f4074 100644 --- a/src/libImaging/Blend.c +++ b/src/libImaging/Blend.c @@ -41,7 +41,8 @@ ImagingBlend(Imaging imIn1, Imaging imIn2, float alpha) { return ImagingCopy(imIn2); } - imOut = ImagingNewDirty(imIn1->mode, imIn1->xsize, imIn1->ysize); + imOut = + ImagingNewDirty(imIn1->mode, (ImagingNewParams){imIn1->xsize, imIn1->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 4ea9c77178a..366e81ee6f2 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -269,7 +269,8 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) } } if (yradius != 0) { - imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); + imTransposed = + ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->ysize, imIn->xsize}); if (!imTransposed) { return NULL; } diff --git a/src/libImaging/Chops.c b/src/libImaging/Chops.c index f9c005efe3a..580568dc1e3 100644 --- a/src/libImaging/Chops.c +++ b/src/libImaging/Chops.c @@ -74,7 +74,7 @@ create(Imaging im1, Imaging im2, char *mode) { xsize = (im1->xsize < im2->xsize) ? im1->xsize : im2->xsize; ysize = (im1->ysize < im2->ysize) ? im1->ysize : im2->ysize; - return ImagingNewDirty(im1->mode, xsize, ysize); + return ImagingNewDirty(im1->mode, (ImagingNewParams){xsize, ysize}); } Imaging diff --git a/src/libImaging/Crop.c b/src/libImaging/Crop.c index 2425b4cd589..630028d0e16 100644 --- a/src/libImaging/Crop.c +++ b/src/libImaging/Crop.c @@ -37,7 +37,7 @@ ImagingCrop(Imaging imIn, int sx0, int sy0, int sx1, int sy1) { ysize = 0; } - imOut = ImagingNewDirty(imIn->mode, xsize, ysize); + imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){xsize, ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Effects.c b/src/libImaging/Effects.c index 93e7af0bce9..3d8a2e984f5 100644 --- a/src/libImaging/Effects.c +++ b/src/libImaging/Effects.c @@ -36,7 +36,7 @@ ImagingEffectMandelbrot(int xsize, int ysize, double extent[4], int quality) { return (Imaging)ImagingError_ValueError(NULL); } - im = ImagingNewDirty("L", xsize, ysize); + im = ImagingNewDirty("L", (ImagingNewParams){xsize, ysize}); if (!im) { return NULL; } @@ -80,7 +80,7 @@ ImagingEffectNoise(int xsize, int ysize, float sigma) { int nextok; double this, next; - imOut = ImagingNewDirty("L", xsize, ysize); + imOut = ImagingNewDirty("L", (ImagingNewParams){xsize, ysize}); if (!imOut) { return NULL; } @@ -120,7 +120,7 @@ ImagingEffectSpread(Imaging imIn, int distance) { Imaging imOut; int x, y; - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!imOut) { return NULL; diff --git a/src/libImaging/Fill.c b/src/libImaging/Fill.c index 5b6bfb89cd8..0090c23ab6a 100644 --- a/src/libImaging/Fill.c +++ b/src/libImaging/Fill.c @@ -76,7 +76,7 @@ ImagingFillLinearGradient(const char *mode) { return (Imaging)ImagingError_ModeError(); } - im = ImagingNewDirty(mode, 256, 256); + im = ImagingNewDirty(mode, (ImagingNewParams){256, 256}); if (!im) { return NULL; } @@ -111,7 +111,7 @@ ImagingFillRadialGradient(const char *mode) { return (Imaging)ImagingError_ModeError(); } - im = ImagingNewDirty(mode, 256, 256); + im = ImagingNewDirty(mode, (ImagingNewParams){256, 256}); if (!im) { return NULL; } diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 85de77fcbbc..87928acc8ee 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -59,7 +59,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin) { } imOut = ImagingNewDirty( - imIn->mode, imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin); + imIn->mode, + (ImagingNewParams){imIn->xsize + 2 * xmargin, imIn->ysize + 2 * ymargin}); if (!imOut) { return NULL; } @@ -393,7 +394,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o return (Imaging)ImagingError_ValueError("bad kernel size"); } - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + imOut = ImagingNewDirty(im->mode, (ImagingNewParams){im->xsize, im->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index ee2f6ea9d17..6dc14ee1c99 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -75,6 +75,13 @@ typedef struct ImagingPaletteInstance *ImagingPalette; #define IMAGING_MODE_MB "MB" /* multi-band format */ +typedef struct { + int xsize; + int ysize; + int depth; + int bands; +} ImagingNewParams; + typedef struct { char *ptr; int size; @@ -176,9 +183,9 @@ extern void ImagingMemoryClearCache(ImagingMemoryArena arena, int new_size); extern Imaging -ImagingNew(const char *mode, int xsize, int ysize, int depth, int bands); +ImagingNew(const char *mode, ImagingNewParams p); extern Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize); +ImagingNewDirty(const char *mode, ImagingNewParams p); extern Imaging ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn); extern void @@ -188,10 +195,9 @@ extern Imaging ImagingNewBlock(const char *mode, int xsize, int ysize); extern Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize, int depth, int bands); +ImagingNewPrologue(const char *mode, ImagingNewParams p); extern Imaging -ImagingNewPrologueSubtype( - const char *mode, int xsize, int ysize, int depth, int bands, int structure_size); +ImagingNewPrologueSubtype(const char *mode, ImagingNewParams p, int structure_size); extern void ImagingCopyPalette(Imaging destination, Imaging source); diff --git a/src/libImaging/Matrix.c b/src/libImaging/Matrix.c index ec7f4d93e06..45cc486473f 100644 --- a/src/libImaging/Matrix.c +++ b/src/libImaging/Matrix.c @@ -29,7 +29,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { } if (strcmp(mode, "L") == 0) { - imOut = ImagingNewDirty("L", im->xsize, im->ysize); + imOut = ImagingNewDirty("L", (ImagingNewParams){im->xsize, im->ysize}); if (!imOut) { return NULL; } @@ -48,7 +48,7 @@ ImagingConvertMatrix(Imaging im, const char *mode, float m[]) { ImagingSectionLeave(&cookie); } else if (strlen(mode) == 3) { - imOut = ImagingNewDirty(mode, im->xsize, im->ysize); + imOut = ImagingNewDirty(mode, (ImagingNewParams){im->xsize, im->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/ModeFilter.c b/src/libImaging/ModeFilter.c index 757cbc3fb86..0178fbef06e 100644 --- a/src/libImaging/ModeFilter.c +++ b/src/libImaging/ModeFilter.c @@ -28,7 +28,7 @@ ImagingModeFilter(Imaging im, int size) { return (Imaging)ImagingError_ModeError(); } - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + imOut = ImagingNewDirty(im->mode, (ImagingNewParams){im->xsize, im->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Negative.c b/src/libImaging/Negative.c index 70b96c39772..88755813079 100644 --- a/src/libImaging/Negative.c +++ b/src/libImaging/Negative.c @@ -27,7 +27,7 @@ ImagingNegative(Imaging im) { return (Imaging)ImagingError_ModeError(); } - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + imOut = ImagingNewDirty(im->mode, (ImagingNewParams){im->xsize, im->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Offset.c b/src/libImaging/Offset.c index 91ee91083cc..9bc74a4fed6 100644 --- a/src/libImaging/Offset.c +++ b/src/libImaging/Offset.c @@ -25,7 +25,7 @@ ImagingOffset(Imaging im, int xoffset, int yoffset) { return (Imaging)ImagingError_ModeError(); } - imOut = ImagingNewDirty(im->mode, im->xsize, im->ysize); + imOut = ImagingNewDirty(im->mode, (ImagingNewParams){im->xsize, im->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Point.c b/src/libImaging/Point.c index 3d54008cbc3..bf4b6c8efbd 100644 --- a/src/libImaging/Point.c +++ b/src/libImaging/Point.c @@ -152,7 +152,7 @@ ImagingPoint(Imaging imIn, const char *mode, const void *table) { goto mode_mismatch; } - imOut = ImagingNew(mode, imIn->xsize, imIn->ysize, -1, -1); + imOut = ImagingNew(mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!imOut) { return NULL; } @@ -214,7 +214,7 @@ ImagingPointTransform(Imaging imIn, double scale, double offset) { return (Imaging)ImagingError_ModeError(); } - imOut = ImagingNew(imIn->mode, imIn->xsize, imIn->ysize, -1, -1); + imOut = ImagingNew(imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index cdc614536da..8912bbd1be1 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1799,7 +1799,7 @@ ImagingQuantize(Imaging im, int colors, int mode, int kmeans) { ImagingSectionLeave(&cookie); if (result > 0) { - imOut = ImagingNewDirty("P", im->xsize, im->ysize); + imOut = ImagingNewDirty("P", (ImagingNewParams){im->xsize, im->ysize}); ImagingSectionEnter(&cookie); for (i = y = 0; y < im->ysize; y++) { diff --git a/src/libImaging/RankFilter.c b/src/libImaging/RankFilter.c index da40c892f11..9389a7bf562 100644 --- a/src/libImaging/RankFilter.c +++ b/src/libImaging/RankFilter.c @@ -84,8 +84,8 @@ MakeRankFunction(UINT8) MakeRankFunction(INT32) MakeRankFunction(FLOAT32) return (Imaging)ImagingError_ValueError("bad rank value"); } - imOut = - ImagingNew(im->mode, im->xsize - 2 * margin, im->ysize - 2 * margin, -1, -1); + imOut = ImagingNew( + im->mode, (ImagingNewParams){im->xsize - 2 * margin, im->ysize - 2 * margin}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Reduce.c b/src/libImaging/Reduce.c index 61566f0c506..bb1a86848c8 100644 --- a/src/libImaging/Reduce.c +++ b/src/libImaging/Reduce.c @@ -1427,7 +1427,9 @@ ImagingReduce(Imaging imIn, int xscale, int yscale, int box[4]) { } imOut = ImagingNewDirty( - imIn->mode, (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale); + imIn->mode, + (ImagingNewParams){ + (box[2] + xscale - 1) / xscale, (box[3] + yscale - 1) / yscale}); if (!imOut) { return NULL; } diff --git a/src/libImaging/Resample.c b/src/libImaging/Resample.c index 59c27b3f421..526c892fb82 100644 --- a/src/libImaging/Resample.c +++ b/src/libImaging/Resample.c @@ -659,7 +659,8 @@ ImagingResampleInner( bounds_vert[i * 2] -= ybox_first; } - imTemp = ImagingNewDirty(imIn->mode, xsize, ybox_last - ybox_first); + imTemp = ImagingNewDirty( + imIn->mode, (ImagingNewParams){xsize, ybox_last - ybox_first}); if (imTemp) { ResampleHorizontal( imTemp, imIn, ybox_first, ksize_horiz, bounds_horiz, kk_horiz); @@ -680,7 +681,7 @@ ImagingResampleInner( /* vertical pass */ if (need_vertical) { - imOut = ImagingNewDirty(imIn->mode, imIn->xsize, ysize); + imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){imIn->xsize, ysize}); if (imOut) { /* imIn can be the original image or horizontally resampled one */ ResampleVertical(imOut, imIn, 0, ksize_vert, bounds_vert, kk_vert); diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 382dbfa49be..788366161ab 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -42,12 +42,11 @@ */ Imaging -ImagingNewPrologueSubtype( - const char *mode, int xsize, int ysize, int depth, int bands, int size) { +ImagingNewPrologueSubtype(const char *mode, ImagingNewParams p, int size) { Imaging im; /* linesize overflow check, roughly the current largest space req'd */ - if (xsize > (INT_MAX / 4) - 1) { + if (p.xsize > (INT_MAX / 4) - 1) { return (Imaging)ImagingError_MemoryError(); } @@ -57,58 +56,58 @@ ImagingNewPrologueSubtype( } /* Setup image descriptor */ - im->xsize = xsize; - im->ysize = ysize; + im->xsize = p.xsize; + im->ysize = p.ysize; im->type = IMAGING_TYPE_UINT8; if (strcmp(mode, "1") == 0) { /* 1-bit images */ im->bands = im->pixelsize = 1; - im->linesize = xsize; + im->linesize = p.xsize; } else if (strcmp(mode, "P") == 0) { /* 8-bit palette mapped images */ im->bands = im->pixelsize = 1; - im->linesize = xsize; + im->linesize = p.xsize; im->palette = ImagingPaletteNew("RGB"); } else if (strcmp(mode, "PA") == 0) { /* 8-bit palette with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; im->palette = ImagingPaletteNew("RGB"); } else if (strcmp(mode, "L") == 0) { /* 8-bit grayscale (luminance) images */ im->bands = im->pixelsize = 1; - im->linesize = xsize; + im->linesize = p.xsize; } else if (strcmp(mode, "LA") == 0) { /* 8-bit grayscale (luminance) with alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "La") == 0) { /* 8-bit grayscale (luminance) with premultiplied alpha */ im->bands = 2; im->pixelsize = 4; /* store in image32 memory */ - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "F") == 0) { /* 32-bit floating point images */ im->bands = 1; im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; im->type = IMAGING_TYPE_FLOAT32; } else if (strcmp(mode, "I") == 0) { /* 32-bit integer images */ im->bands = 1; im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; im->type = IMAGING_TYPE_INT32; } else if ( @@ -118,21 +117,21 @@ ImagingNewPrologueSubtype( /* 16-bit raw integer images */ im->bands = 1; im->pixelsize = 2; - im->linesize = xsize * 2; + im->linesize = p.xsize * 2; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "RGB") == 0) { /* 24-bit true colour images */ im->bands = 3; im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ /* 15-bit reversed true colour */ im->bands = 3; im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; + im->linesize = (p.xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "BGR;16") == 0) { @@ -140,7 +139,7 @@ ImagingNewPrologueSubtype( /* 16-bit reversed true colour */ im->bands = 3; im->pixelsize = 2; - im->linesize = (xsize * 2 + 3) & -4; + im->linesize = (p.xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "BGR;24") == 0) { @@ -148,58 +147,58 @@ ImagingNewPrologueSubtype( /* 24-bit reversed true colour */ im->bands = 3; im->pixelsize = 3; - im->linesize = (xsize * 3 + 3) & -4; + im->linesize = (p.xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "RGBA") == 0) { /* 32-bit true colour images with alpha */ im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "RGBa") == 0) { /* 32-bit true colour images with premultiplied alpha */ im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "CMYK") == 0) { /* 32-bit colour separation */ im->bands = im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "YCbCr") == 0) { /* 24-bit video format */ im->bands = 3; im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "LAB") == 0) { /* 24-bit color, luminance, + 2 color channels */ /* L is uint8, a,b are int8 */ im->bands = 3; im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, "HSV") == 0) { /* 24-bit color, luminance, + 2 color channels */ /* L is uint8, a,b are int8 */ im->bands = 3; im->pixelsize = 4; - im->linesize = xsize * 4; + im->linesize = p.xsize * 4; } else if (strcmp(mode, IMAGING_MODE_MB) == 0) { - if (bands <= 0 || depth <= 0) { + if (p.bands <= 0 || p.depth <= 0) { return (Imaging)ImagingError_ValueError( "multi-band missing bands and depth"); } - im->bands = bands; - im->depth = depth; - im->pixelsize = depth / CHAR_BIT * bands; - im->linesize = xsize * im->pixelsize; + im->bands = p.bands; + im->depth = p.depth; + im->pixelsize = p.depth / CHAR_BIT * p.bands; + im->linesize = p.xsize * im->pixelsize; im->type = IMAGING_TYPE_MB; } else { @@ -212,7 +211,7 @@ ImagingNewPrologueSubtype( /* Pointer array (allocate at least one line, to avoid MemoryError exceptions on platforms where calloc(0, x) returns NULL) */ - im->image = (char **)calloc((ysize > 0) ? ysize : 1, sizeof(void *)); + im->image = (char **)calloc((p.ysize > 0) ? p.ysize : 1, sizeof(void *)); if (!im->image) { free(im); @@ -237,9 +236,8 @@ ImagingNewPrologueSubtype( } Imaging -ImagingNewPrologue(const char *mode, int xsize, int ysize, int depth, int bands) { - return ImagingNewPrologueSubtype( - mode, xsize, ysize, depth, bands, sizeof(struct ImagingMemoryInstance)); +ImagingNewPrologue(const char *mode, ImagingNewParams p) { + return ImagingNewPrologueSubtype(mode, p, sizeof(struct ImagingMemoryInstance)); } void @@ -498,15 +496,14 @@ ImagingAllocateBlock(Imaging im) { */ static Imaging -ImagingNewInternal( - const char *mode, int xsize, int ysize, int depth, int bands, int dirty) { +ImagingNewInternal(const char *mode, ImagingNewParams p, int dirty) { Imaging im; - if (xsize < 0 || ysize < 0) { + if (p.xsize < 0 || p.ysize < 0) { return (Imaging)ImagingError_ValueError("bad image size"); } - im = ImagingNewPrologue(mode, xsize, ysize, depth, bands); + im = ImagingNewPrologue(mode, p); if (!im) { return NULL; } @@ -527,13 +524,13 @@ ImagingNewInternal( } Imaging -ImagingNew(const char *mode, int xsize, int ysize, int depth, int bands) { - return ImagingNewInternal(mode, xsize, ysize, depth, bands, 0); +ImagingNew(const char *mode, ImagingNewParams p) { + return ImagingNewInternal(mode, p, 0); } Imaging -ImagingNewDirty(const char *mode, int xsize, int ysize) { - return ImagingNewInternal(mode, xsize, ysize, -1, -1, 1); +ImagingNewDirty(const char *mode, ImagingNewParams p) { + return ImagingNewInternal(mode, p, 1); } Imaging @@ -544,7 +541,7 @@ ImagingNewBlock(const char *mode, int xsize, int ysize) { return (Imaging)ImagingError_ValueError("bad image size"); } - im = ImagingNewPrologue(mode, xsize, ysize, -1, -1); + im = ImagingNewPrologue(mode, (ImagingNewParams){xsize, ysize}); if (!im) { return NULL; } @@ -564,12 +561,15 @@ ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { if (imOut) { /* make sure images match */ if (strcmp(imOut->mode, mode) != 0 || imOut->xsize != imIn->xsize || - imOut->ysize != imIn->ysize) { + imOut->ysize != imIn->ysize || imOut->depth != imIn->depth || + imOut->bands != imIn->bands) { return ImagingError_Mismatch(); } } else { /* create new image */ - imOut = ImagingNewDirty(mode, imIn->xsize, imIn->ysize); + imOut = ImagingNewDirty( + mode, + (ImagingNewParams){imIn->xsize, imIn->ysize, imIn->depth, imIn->bands}); if (!imOut) { return NULL; } diff --git a/src/map.c b/src/map.c index 46db0aedee2..acbac156222 100644 --- a/src/map.c +++ b/src/map.c @@ -125,7 +125,9 @@ PyImaging_MapBuffer(PyObject *self, PyObject *args) { } im = ImagingNewPrologueSubtype( - mode, xsize, ysize, depth, bands, sizeof(ImagingBufferInstance)); + mode, + (ImagingNewParams){xsize, ysize, depth, bands}, + sizeof(ImagingBufferInstance)); if (!im) { PyBuffer_Release(&view); return NULL; From bcd9a326582c4d54c76170b177c7ef6c1893ac82 Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sun, 2 Jun 2024 12:40:32 +0000 Subject: [PATCH 11/16] crop() with multi-band format --- Tests/test_file_tiff.py | 20 +++++++++++++++----- src/libImaging/Crop.c | 3 ++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a4366f7fbb7..370d598926f 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -895,11 +895,18 @@ def test_oom(self, test_file: str) -> None: def test_open_tiff_uint16_multiband(self): """Test opening multiband TIFFs and reading all channels.""" - def getpixel00(im: Image.Image): + def check_pixel(im: Image.Image, expected_pixel, pos: tuple[int, int]): actual_pixel = im.getpixel((0, 0)) if isinstance(actual_pixel, int): actual_pixel = (actual_pixel,) - return actual_pixel + assert actual_pixel == expected_pixel + + def check_image(im: Image.Image, width: int, height: int, expected_pixel): + assert im.width == width + assert im.height == height + for x in range(im.width): + for y in range(im.height): + check_pixel(im, expected_pixel, (x, y)) base_value = 4660 for i in range(1, 6): @@ -908,10 +915,13 @@ def getpixel00(im: Image.Image): im = Image.open(infile) im.load() - assert getpixel00(im) == pixel + check_image(im, 10, 10, pixel) + + im1 = im.copy() + check_image(im1, 10, 10, pixel) - im.copy() - assert getpixel00(im) == pixel + im2 = im.crop((2, 2, 7, 7)) + check_image(im2, 5, 5, pixel) @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/src/libImaging/Crop.c b/src/libImaging/Crop.c index 630028d0e16..52f9b0a431f 100644 --- a/src/libImaging/Crop.c +++ b/src/libImaging/Crop.c @@ -37,7 +37,8 @@ ImagingCrop(Imaging imIn, int sx0, int sy0, int sx1, int sy1) { ysize = 0; } - imOut = ImagingNewDirty(imIn->mode, (ImagingNewParams){xsize, ysize}); + imOut = ImagingNewDirty( + imIn->mode, (ImagingNewParams){xsize, ysize, imIn->depth, imIn->bands}); if (!imOut) { return NULL; } From 96087f14add9bfa97d3dfc9dc4c540b942fffc0d Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sun, 2 Jun 2024 13:07:58 +0000 Subject: [PATCH 12/16] ImagingNew2Dirty update mismatch condition --- src/libImaging/Storage.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 788366161ab..6aa49a9080f 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -561,8 +561,9 @@ ImagingNew2Dirty(const char *mode, Imaging imOut, Imaging imIn) { if (imOut) { /* make sure images match */ if (strcmp(imOut->mode, mode) != 0 || imOut->xsize != imIn->xsize || - imOut->ysize != imIn->ysize || imOut->depth != imIn->depth || - imOut->bands != imIn->bands) { + imOut->ysize != imIn->ysize || + (strcmp(mode, IMAGING_MODE_MB) == 0 && + (imOut->depth != imIn->depth || imOut->bands != imIn->bands))) { return ImagingError_Mismatch(); } } else { From 7c0eac14be21dd7369d4e0feb8fb063f73e74e86 Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sun, 2 Jun 2024 17:38:27 +0000 Subject: [PATCH 13/16] FLIP_LEFT_RIGHT with multi-band format --- Tests/test_file_tiff.py | 3 +++ src/_imaging.c | 6 ++++-- src/libImaging/Geometry.c | 30 ++++++++---------------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 370d598926f..b621460db30 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -923,6 +923,9 @@ def check_image(im: Image.Image, width: int, height: int, expected_pixel): im2 = im.crop((2, 2, 7, 7)) check_image(im2, 5, 5, pixel) + im3 = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + check_image(im3, 10, 10, pixel) + @pytest.mark.skipif(not is_win32(), reason="Windows only") class TestFileTiffW32: diff --git a/src/_imaging.c b/src/_imaging.c index b216f41f937..7313cb4cff3 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2143,14 +2143,16 @@ _transpose(ImagingObject *self, PyObject *args) { case 1: /* flip top bottom */ case 3: /* rotate 180 */ imOut = ImagingNewDirty( - imIn->mode, (ImagingNewParams){imIn->xsize, imIn->ysize}); + imIn->mode, + (ImagingNewParams){imIn->xsize, imIn->ysize, imIn->depth, imIn->bands}); break; case 2: /* rotate 90 */ case 4: /* rotate 270 */ case 5: /* transpose */ case 6: /* transverse */ imOut = ImagingNewDirty( - imIn->mode, (ImagingNewParams){imIn->ysize, imIn->xsize}); + imIn->mode, + (ImagingNewParams){imIn->ysize, imIn->xsize, imIn->depth, imIn->bands}); break; default: PyErr_SetString(PyExc_ValueError, "No such transpose operation"); diff --git a/src/libImaging/Geometry.c b/src/libImaging/Geometry.c index cf3bc997942..abc91fc1c11 100644 --- a/src/libImaging/Geometry.c +++ b/src/libImaging/Geometry.c @@ -17,7 +17,7 @@ Imaging ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingSectionCookie cookie; - int x, y, xr; + // int x, y, xr; if (!imOut || !imIn || strcmp(imIn->mode, imOut->mode) != 0) { return (Imaging)ImagingError_ModeError(); @@ -28,32 +28,18 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn) { ImagingCopyPalette(imOut, imIn); -#define FLIP_LEFT_RIGHT(INT, image) \ - for (y = 0; y < imIn->ysize; y++) { \ - INT *in = (INT *)imIn->image[y]; \ - INT *out = (INT *)imOut->image[y]; \ - xr = imIn->xsize - 1; \ - for (x = 0; x < imIn->xsize; x++, xr--) { \ - out[xr] = in[x]; \ - } \ - } - ImagingSectionEnter(&cookie); - - if (imIn->image8) { - if (strncmp(imIn->mode, "I;16", 4) == 0) { - FLIP_LEFT_RIGHT(UINT16, image8) - } else { - FLIP_LEFT_RIGHT(UINT8, image8) + for (int y = 0; y < imIn->ysize; ++y) { + char *in = imIn->image[y]; + char *out = imOut->image[y]; + int xr = imIn->linesize - imIn->pixelsize; + for (int x = 0; x < imIn->linesize; + x += imIn->pixelsize, xr -= imIn->pixelsize) { + memcpy(out + xr, in + x, imIn->pixelsize); } - } else { - FLIP_LEFT_RIGHT(INT32, image32) } - ImagingSectionLeave(&cookie); -#undef FLIP_LEFT_RIGHT - return imOut; } From 5bd64f096d8683bf5ce9ece73cda5d19f3ca0d7a Mon Sep 17 00:00:00 2001 From: Junxiao Shi Date: Sun, 2 Jun 2024 19:56:57 +0000 Subject: [PATCH 14/16] explain how mode=MB is stored --- src/libImaging/Imaging.h | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 6dc14ee1c99..976fe85e62f 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -42,11 +42,21 @@ extern "C" { * LA 4 L, -, -, A * PA 4 P, -, -, A * I;16 2 I (16-bit integer, native byte order) + * MB variable * * "P" is an 8-bit palette mode, which should be mapped through the * palette member to get an output image. Check palette->mode to * find the corresponding "real" mode. * + * "MB" is an experimental multi-band mode for multi-channel image where each sample is + * more than UINT8. In this mode, + * - im->depth is size of each sample in bits. Valid values are 8, 16, and 32. Currently + * each sample is assumed to be a uint; further refactoring will be needed to support + * int and float. + * - im->type is set to IMAGING_TYPE_MB. + * - Neither im->image8 nor im->image32 is set. All operators must access im->image + * directly. + * * For information on how to access Imaging objects from your own C * extensions, see http://www.effbot.org/zone/pil-extending.htm */ @@ -75,11 +85,12 @@ typedef struct ImagingPaletteInstance *ImagingPalette; #define IMAGING_MODE_MB "MB" /* multi-band format */ +/* Parameters of various ImagingNew* functions. */ typedef struct { int xsize; int ysize; - int depth; - int bands; + int depth; /** MB mode only. */ + int bands; /** MB mode only. */ } ImagingNewParams; typedef struct { From f3a195ee73652b82ac5d36d3a781c8f3e5336cfd Mon Sep 17 00:00:00 2001 From: "Jeffrey A. Clark (Alex)" Date: Thu, 20 Jun 2024 09:23:30 -0400 Subject: [PATCH 15/16] hack202406 - requested changes --- src/PIL/ImageFile.py | 6 +++--- src/PIL/TiffImagePlugin.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 700f3b106a2..a2f6af86f40 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -118,7 +118,7 @@ def __init__(self, fp=None, filename=None): self.readonly = 1 # until we know better - self.newconfig = () + self.mb_config = () self.decoderconfig = () self.decodermaxblock = MAXBLOCK @@ -229,7 +229,7 @@ def load(self): msg = "buffer is not large enough" raise OSError(msg) self.im = Image.core.map_buffer( - self.map, self.size, decoder_name, offset, args, *self.newconfig + self.map, self.size, decoder_name, offset, args, *self.mb_config ) readonly = 1 # After trashing self.im, @@ -318,7 +318,7 @@ def load(self): def load_prepare(self) -> None: # create image memory if necessary if not self.im or self.im.mode != self.mode or self.im.size != self.size: - self.im = Image.core.new(self.mode, self.size, *self.newconfig) + self.im = Image.core.new(self.mode, self.size, *self.mb_config) # create palette (optional) if self.mode == "P": Image.Image.load(self) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 9d3c3c0f86f..8f7898cbc83 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -60,7 +60,6 @@ from .TiffTags import TYPES logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) # XXX hack202406 # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False @@ -1483,7 +1482,7 @@ def _setup(self): logger.debug("- pil mode: %s", self.mode) if self.mode == "MB": assert max(bps_tuple) == min(bps_tuple) - self.newconfig = (max(bps_tuple), samples_per_pixel) + self.mb_config = (max(bps_tuple), samples_per_pixel) self.info["compression"] = self._compression From 77b7a4f82c1ce78106f5c1a367df316c55ab473d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 7 Jul 2024 00:27:00 +0000 Subject: [PATCH 16/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- PIL/TiffImagePlugin.py | 453 +++++++++++++++++++++++----------------- Tests/test_file_tiff.py | 7 +- 2 files changed, 264 insertions(+), 196 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 670962e689f..77a7bba0756 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -38,27 +38,21 @@ # # See the README file for information on usage and redistribution. # - -from __future__ import division, print_function - -from PIL import Image, ImageFile -from PIL import ImagePalette -from PIL import _binary -from PIL import TiffTags +from __future__ import annotations import collections -from fractions import Fraction -from numbers import Number, Rational - import io import itertools import os import struct import sys import warnings +from fractions import Fraction +from numbers import Number, Rational -from .TiffTags import TYPES +from PIL import Image, ImageFile, ImagePalette, TiffTags, _binary +from .TiffTags import TYPES __version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. @@ -145,7 +139,6 @@ (MM, 1, (1,), 1, (1,), ()): ("1", "1"), (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), - (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), @@ -154,7 +147,6 @@ (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), - (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), @@ -163,7 +155,6 @@ (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), - (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), @@ -172,18 +163,48 @@ (MM, 1, (1,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), - (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), - (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), - (II, 1, (1,), 1, (16,16), (0,)): ("I;16", "I;16"), - (II, 1, (1,), 1, (16,16,16), (0,0,)): ("I;16", "I;16"), - (II, 1, (1,), 1, (16,16,16,16), (0,0,0,)): ("I;16", "I;16"), - (II, 1, (1,), 1, (16,16,16,16,16), (0,0,0,0,)): ("I;16", "I;16"), + (II, 1, (1,), 1, (16, 16), (0,)): ("I;16", "I;16"), + ( + II, + 1, + (1,), + 1, + (16, 16, 16), + ( + 0, + 0, + ), + ): ("I;16", "I;16"), + ( + II, + 1, + (1,), + 1, + (16, 16, 16, 16), + ( + 0, + 0, + 0, + ), + ): ("I;16", "I;16"), + ( + II, + 1, + (1,), + 1, + (16, 16, 16, 16, 16), + ( + 0, + 0, + 0, + 0, + ), + ): ("I;16", "I;16"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), (II, 1, (2,), 1, (16,), ()): ("I;16S", "I;16S"), (MM, 1, (2,), 1, (16,), ()): ("I;16BS", "I;16BS"), - (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), @@ -191,10 +212,8 @@ (MM, 1, (2,), 1, (32,), ()): ("I;32BS", "I;32BS"), (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), - (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), - (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), @@ -209,7 +228,6 @@ (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 - (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), @@ -228,13 +246,10 @@ (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), - (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), - (II, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), - (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } @@ -251,6 +266,7 @@ def _limit_rational(val, max_val): n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d + ## # Wrapper for TIFF IFDs. @@ -259,7 +275,7 @@ def _limit_rational(val, max_val): class IFDRational(Rational): - """ Implements a rational class where 0/0 is a legal value to match + """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used @@ -270,7 +286,7 @@ class IFDRational(Rational): """ - __slots__ = ('_numerator', '_denominator', '_val') + __slots__ = ("_numerator", "_denominator", "_val") def __init__(self, value, denominator=1): """ @@ -294,7 +310,7 @@ def __init__(self, value, denominator=1): return if denominator == 0: - self._val = float('nan') + self._val = float("nan") return elif denominator == 1: @@ -339,6 +355,7 @@ def __eq__(self, other): def _delegate(op): def delegate(self, *args): return getattr(self._val, op)(*args) + return delegate """ a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul', @@ -349,34 +366,34 @@ def delegate(self, *args): print "\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a) """ - __add__ = _delegate('__add__') - __radd__ = _delegate('__radd__') - __sub__ = _delegate('__sub__') - __rsub__ = _delegate('__rsub__') - __div__ = _delegate('__div__') - __rdiv__ = _delegate('__rdiv__') - __mul__ = _delegate('__mul__') - __rmul__ = _delegate('__rmul__') - __truediv__ = _delegate('__truediv__') - __rtruediv__ = _delegate('__rtruediv__') - __floordiv__ = _delegate('__floordiv__') - __rfloordiv__ = _delegate('__rfloordiv__') - __mod__ = _delegate('__mod__') - __rmod__ = _delegate('__rmod__') - __pow__ = _delegate('__pow__') - __rpow__ = _delegate('__rpow__') - __pos__ = _delegate('__pos__') - __neg__ = _delegate('__neg__') - __abs__ = _delegate('__abs__') - __trunc__ = _delegate('__trunc__') - __lt__ = _delegate('__lt__') - __gt__ = _delegate('__gt__') - __le__ = _delegate('__le__') - __ge__ = _delegate('__ge__') - __nonzero__ = _delegate('__nonzero__') - __ceil__ = _delegate('__ceil__') - __floor__ = _delegate('__floor__') - __round__ = _delegate('__round__') + __add__ = _delegate("__add__") + __radd__ = _delegate("__radd__") + __sub__ = _delegate("__sub__") + __rsub__ = _delegate("__rsub__") + __div__ = _delegate("__div__") + __rdiv__ = _delegate("__rdiv__") + __mul__ = _delegate("__mul__") + __rmul__ = _delegate("__rmul__") + __truediv__ = _delegate("__truediv__") + __rtruediv__ = _delegate("__rtruediv__") + __floordiv__ = _delegate("__floordiv__") + __rfloordiv__ = _delegate("__rfloordiv__") + __mod__ = _delegate("__mod__") + __rmod__ = _delegate("__rmod__") + __pow__ = _delegate("__pow__") + __rpow__ = _delegate("__rpow__") + __pos__ = _delegate("__pos__") + __neg__ = _delegate("__neg__") + __abs__ = _delegate("__abs__") + __trunc__ = _delegate("__trunc__") + __lt__ = _delegate("__lt__") + __gt__ = _delegate("__gt__") + __le__ = _delegate("__le__") + __ge__ = _delegate("__ge__") + __nonzero__ = _delegate("__nonzero__") + __ceil__ = _delegate("__ceil__") + __floor__ = _delegate("__floor__") + __round__ = _delegate("__round__") class ImageFileDirectory_v2(collections.MutableMapping): @@ -409,6 +426,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): .. versionadded:: 3.0.0 """ + """ Documentation: @@ -453,7 +471,7 @@ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): else: raise SyntaxError("not a TIFF IFD") self.reset() - self.next, = self._unpack("L", ifh[4:]) + (self.next,) = self._unpack("L", ifh[4:]) self._legacy_api = False prefix = property(lambda self: self._prefix) @@ -468,7 +486,7 @@ def reset(self): self._tags_v1 = {} # will remain empty if legacy_api is false self._tags_v2 = {} # main tag storage self._tagdata = {} - self.tagtype = {} # added 2008-06-05 by Florian Hoech + self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None self._offset = None @@ -480,8 +498,10 @@ def as_dict(self): .. deprecated:: 3.0.0 """ - warnings.warn("as_dict() is deprecated. " + - "Please use dict(ifd) instead.", DeprecationWarning) + warnings.warn( + "as_dict() is deprecated. " + "Please use dict(ifd) instead.", + DeprecationWarning, + ) return dict(self) def named(self): @@ -490,8 +510,7 @@ def named(self): Returns the complete tag dictionary, with named tags where possible. """ - return dict((TiffTags.lookup(code).name, value) - for code, value in self.items()) + return dict((TiffTags.lookup(code).name, value) for code, value in self.items()) def __len__(self): return len(set(self._tagdata) | set(self._tags_v2)) @@ -504,13 +523,14 @@ def __getitem__(self, tag): self[tag] = handler(self, data, self.legacy_api) # check type val = self._tags_v2[tag] if self.legacy_api and not isinstance(val, (tuple, bytes)): - val = val, + val = (val,) return val def __contains__(self, tag): return tag in self._tags_v2 or tag in self._tagdata if bytes is str: + def has_key(self, tag): return tag in self @@ -520,7 +540,7 @@ def __setitem__(self, tag, value): def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) if bytes is str: - basetypes += unicode, + basetypes += (unicode,) info = TiffTags.lookup(tag) values = [value] if isinstance(value, basetypes) else value @@ -533,7 +553,7 @@ def _setitem(self, tag, value, legacy_api): if all(isinstance(v, IFDRational) for v in values): self.tagtype[tag] = 5 elif all(isinstance(v, int) for v in values): - if all(v < 2 ** 16 for v in values): + if all(v < 2**16 for v in values): self.tagtype[tag] = 3 else: self.tagtype[tag] = 4 @@ -548,8 +568,10 @@ def _setitem(self, tag, value, legacy_api): self.tagtype[tag] = 2 if self.tagtype[tag] == 7 and bytes is not str: - values = [value.encode("ascii", 'replace') if isinstance(value, str) else value - for value in values] + values = [ + value.encode("ascii", "replace") if isinstance(value, str) else value + for value in values + ] values = tuple(info.cvt_enum(value) for value in values) @@ -557,8 +579,8 @@ def _setitem(self, tag, value, legacy_api): if info.length == 1: if legacy_api and self.tagtype[tag] in [5, 10]: - values = values, - dest[tag], = values + values = (values,) + (dest[tag],) = values else: dest[tag] = values @@ -579,37 +601,52 @@ def _pack(self, fmt, *values): def _register_loader(idx, size): def decorator(func): from PIL.TiffTags import TYPES + if func.__name__.startswith("load_"): TYPES[idx] = func.__name__[5:].replace("_", " ") _load_dispatch[idx] = size, func return func + return decorator def _register_writer(idx): def decorator(func): _write_dispatch[idx] = func return func + return decorator def _register_basic(idx_fmt_name): from PIL.TiffTags import TYPES + idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( - self._unpack("{0}{1}".format(len(data) // size, fmt), data)) + self._unpack(f"{len(data) // size}{fmt}", data) + ) _write_dispatch[idx] = lambda self, *values: ( - b"".join(self._pack(fmt, value) for value in values)) - - list(map(_register_basic, - [(3, "H", "short"), (4, "L", "long"), - (6, "b", "signed byte"), (8, "h", "signed short"), - (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) + b"".join(self._pack(fmt, value) for value in values) + ) + + list( + map( + _register_basic, + [ + (3, "H", "short"), + (4, "L", "long"), + (6, "b", "signed byte"), + (8, "h", "signed short"), + (9, "l", "signed long"), + (11, "f", "float"), + (12, "d", "double"), + ], + ) + ) @_register_loader(1, 1) # Basic type, except for the legacy API. def load_byte(self, data, legacy_api=True): - return (data if legacy_api else - tuple(map(ord, data) if bytes is str else data)) + return data if legacy_api else tuple(map(ord, data) if bytes is str else data) @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): @@ -625,20 +662,20 @@ def load_string(self, data, legacy_api=True): def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 if sys.version_info[0] == 2: - value = value.decode('ascii', 'replace') - return b"" + value.encode('ascii', 'replace') + b"\0" + value = value.decode("ascii", "replace") + return b"" + value.encode("ascii", "replace") + b"\0" @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): - vals = self._unpack("{0}L".format(len(data) // 4), data) + vals = self._unpack(f"{len(data) // 4}L", data) combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) - return tuple(combine(num, denom) - for num, denom in zip(vals[::2], vals[1::2])) + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) def write_rational(self, *values): - return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31)) - for frac in values) + return b"".join( + self._pack("2L", *_limit_rational(frac, 2**31)) for frac in values + ) @_register_loader(7, 1) def load_undefined(self, data, legacy_api=True): @@ -650,22 +687,23 @@ def write_undefined(self, value): @_register_loader(10, 8) def load_signed_rational(self, data, legacy_api=True): - vals = self._unpack("{0}l".format(len(data) // 4), data) + vals = self._unpack(f"{len(data) // 4}l", data) combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b) - return tuple(combine(num, denom) - for num, denom in zip(vals[::2], vals[1::2])) + return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) def write_signed_rational(self, *values): - return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30)) - for frac in values) + return b"".join( + self._pack("2L", *_limit_rational(frac, 2**30)) for frac in values + ) def _ensure_read(self, fp, size): ret = fp.read(size) if len(ret) != size: - raise IOError("Corrupt EXIF data. " + - "Expecting to read %d bytes but only got %d. " % - (size, len(ret))) + raise OSError( + "Corrupt EXIF data. " + + "Expecting to read %d bytes but only got %d. " % (size, len(ret)) + ) return ret def load(self, fp): @@ -679,8 +717,10 @@ def load(self, fp): if DEBUG: tagname = TiffTags.lookup(tag).name typname = TYPES.get(typ, "unknown") - print("tag: %s (%d) - type: %s (%d)" % - (tagname, tag, typname, typ), end=" ") + print( + "tag: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), + end=" ", + ) try: unit_size, handler = self._load_dispatch[typ] @@ -691,10 +731,12 @@ def load(self, fp): size = count * unit_size if size > 4: here = fp.tell() - offset, = self._unpack("L", data) + (offset,) = self._unpack("L", data) if DEBUG: - print("Tag Location: %s - Data Location: %s" % - (here, offset), end=" ") + print( + "Tag Location: %s - Data Location: %s" % (here, offset), + end=" ", + ) fp.seek(offset) data = ImageFile._safe_read(fp, size) fp.seek(here) @@ -702,9 +744,11 @@ def load(self, fp): data = data[:size] if len(data) != size: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d. " - "Skipping tag %s" % (size, len(data), tag)) + warnings.warn( + "Possibly corrupt EXIF data. " + "Expecting to read %d bytes but only got %d. " + "Skipping tag %s" % (size, len(data), tag) + ) continue self._tagdata[tag] = data @@ -716,8 +760,8 @@ def load(self, fp): else: print("- value:", self[tag]) - self.next, = self._unpack("L", self._ensure_read(fp, 4)) - except IOError as msg: + (self.next,) = self._unpack("L", self._ensure_read(fp, 4)) + except OSError as msg: warnings.warn(str(msg)) return @@ -747,8 +791,10 @@ def save(self, fp): if DEBUG: tagname = TiffTags.lookup(tag).name typname = TYPES.get(typ, "unknown") - print("save: %s (%d) - type: %s (%d)" % - (tagname, tag, typname, typ), end=" ") + print( + "save: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), + end=" ", + ) if len(data) >= 16: print("- value: " % len(data)) else: @@ -767,8 +813,7 @@ def save(self, fp): if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: - raise NotImplementedError( - "multistrip support not yet implemented") + raise NotImplementedError("multistrip support not yet implemented") value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data @@ -789,6 +834,7 @@ def save(self, fp): return offset + ImageFileDirectory_v2._load_dispatch = _load_dispatch ImageFileDirectory_v2._write_dispatch = _write_dispatch for idx, name in TYPES.items(): @@ -817,6 +863,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): .. deprecated:: 3.0.0 """ + def __init__(self, *args, **kwargs): ImageFileDirectory_v2.__init__(self, *args, **kwargs) self._legacy_api = True @@ -826,7 +873,7 @@ def __init__(self, *args, **kwargs): @classmethod def from_v2(cls, original): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` @@ -843,7 +890,7 @@ def from_v2(cls, original): return ifd def to_v2(self): - """ Returns an + """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` @@ -881,7 +928,7 @@ def __getitem__(self, tag): self._setitem(tag, handler(self, data, legacy), legacy) val = self._tags_v1[tag] if not isinstance(val, (tuple, bytes)): - val = val, + val = (val,) return val @@ -892,6 +939,7 @@ def __getitem__(self, tag): ## # Image plugin for TIFF files. + class TiffImageFile(ImageFile.ImageFile): format = "TIFF" @@ -966,9 +1014,11 @@ def _seek(self, frame): if not self.__next: raise EOFError("no more images in TIFF file") if DEBUG: - print("Seeking to frame %s, on frame %s, " - "__next %s, location: %s" % - (frame, self.__frame, self.__next, self.fp.tell())) + print( + "Seeking to frame %s, on frame %s, " + "__next %s, location: %s" + % (frame, self.__frame, self.__next, self.fp.tell()) + ) # reset python3 buffered io handle in case fp # was passed to libtiff, invalidating the buffer self.fp.tell() @@ -1004,7 +1054,7 @@ def _decoder(self, rawmode, layer, tile=None): if JPEGTABLES in self.tag_v2: # Hack to handle abbreviated JPEG headers # FIXME This will fail with more than one value - self.tile_prefix, = self.tag_v2[JPEGTABLES] + (self.tile_prefix,) = self.tag_v2[JPEGTABLES] elif compression == "packbits": args = rawmode elif compression == "tiff_lzw": @@ -1014,7 +1064,7 @@ def _decoder(self, rawmode, layer, tile=None): self.decoderconfig = (self.tag_v2[PREDICTOR],) if ICCPROFILE in self.tag_v2: - self.info['icc_profile'] = self.tag_v2[ICCPROFILE] + self.info["icc_profile"] = self.tag_v2[ICCPROFILE] return args @@ -1024,31 +1074,30 @@ def load(self): return super(TiffImageFile, self).load() def _load_libtiff(self): - """ Overload method triggered when we detect a compressed tiff - Calls out to libtiff """ + """Overload method triggered when we detect a compressed tiff + Calls out to libtiff""" pixel = Image.Image.load(self) if self.tile is None: - raise IOError("cannot load this image") + raise OSError("cannot load this image") if not self.tile: return pixel self.load_prepare() if not len(self.tile) == 1: - raise IOError("Not exactly one tile") + raise OSError("Not exactly one tile") # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] args = self.tile[0][3] + (self.tag_v2.offset,) - decoder = Image._getdecoder(self.mode, 'libtiff', args, - self.decoderconfig) + decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig) try: decoder.setimage(self.im, extents) except ValueError: - raise IOError("Couldn't set the image") + raise OSError("Couldn't set the image") if hasattr(self.fp, "getvalue"): # We've got a stringio like thing passed in. Yay for all in memory. @@ -1080,13 +1129,13 @@ def _load_libtiff(self): self.tile = [] self.readonly = 0 # libtiff closed the fp in a, we need to close self.fp, if possible - if hasattr(self.fp, 'close'): + if hasattr(self.fp, "close"): if not self.__next: self.fp.close() self.fp = None # might be shared if err < 0: - raise IOError(err) + raise OSError(err) self.load_end() @@ -1096,7 +1145,7 @@ def _setup(self): "Setup this image object based on current tags" if 0xBC01 in self.tag_v2: - raise IOError("Windows Media Photo files not yet supported") + raise OSError("Windows Media Photo files not yet supported") # extract relevant tags self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] @@ -1134,10 +1183,13 @@ def _setup(self): # mode: check photometric interpretation and bits per pixel key = ( - self.tag_v2.prefix, photo, format, fillorder, + self.tag_v2.prefix, + photo, + format, + fillorder, self.tag_v2.get(BITSPERSAMPLE, (1,)), - self.tag_v2.get(EXTRASAMPLES, ()) - ) + self.tag_v2.get(EXTRASAMPLES, ()), + ) if DEBUG: print("format key:", key) try: @@ -1174,14 +1226,18 @@ def _setup(self): offsets = self.tag_v2[STRIPOFFSETS] h = self.tag_v2.get(ROWSPERSTRIP, ysize) w = self.size[0] - if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", - "group4", "tiff_jpeg", - "tiff_adobe_deflate", - "tiff_thunderscan", - "tiff_deflate", - "tiff_sgilog", - "tiff_sgilog24", - "tiff_raw_16"]: + if READ_LIBTIFF or self._compression in [ + "tiff_ccitt", + "group3", + "group4", + "tiff_jpeg", + "tiff_adobe_deflate", + "tiff_thunderscan", + "tiff_deflate", + "tiff_sgilog", + "tiff_sgilog24", + "tiff_raw_16", + ]: # if DEBUG: # print "Activating g4 compression for whole file" @@ -1201,14 +1257,13 @@ def _setup(self): # libtiff closes the file descriptor, so pass in a dup. try: - fp = hasattr(self.fp, "fileno") and \ - os.dup(self.fp.fileno()) + fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) # flush the file descriptor, prevents error on pypy 2.4+ # should also eliminate the need for fp.tell for py3 # in _seek if hasattr(self.fp, "flush"): self.fp.flush() - except IOError: + except OSError: # io.BytesIO have a fileno, but returns an IOError if # it doesn't use a file descriptor. fp = False @@ -1219,10 +1274,13 @@ def _setup(self): # https://github.com/python-pillow/Pillow/issues/279 if fillorder == 2: key = ( - self.tag_v2.prefix, photo, format, 1, + self.tag_v2.prefix, + photo, + format, + 1, self.tag_v2.get(BITSPERSAMPLE, (1,)), - self.tag_v2.get(EXTRASAMPLES, ()) - ) + self.tag_v2.get(EXTRASAMPLES, ()), + ) if DEBUG: print("format key:", key) # this should always work, since all the @@ -1233,25 +1291,26 @@ def _setup(self): # we're expecting image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if self.mode in ('I;16B', 'I;16') and 'I;16' in rawmode: - rawmode = 'I;16N' + if self.mode in ("I;16B", "I;16") and "I;16" in rawmode: + rawmode = "I;16N" # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds a = (rawmode, self._compression, fp) - self.tile.append( - (self._compression, - (0, 0, w, ysize), - 0, a)) + self.tile.append((self._compression, (0, 0, w, ysize), 0, a)) a = None else: for i in range(len(offsets)): a = self._decoder(rawmode, l, i) self.tile.append( - (self._compression, - (0, min(y, ysize), w, min(y+h, ysize)), - offsets[i], a)) + ( + self._compression, + (0, min(y, ysize), w, min(y + h, ysize)), + offsets[i], + a, + ) + ) if DEBUG: print("tiles: ", self.tile) y = y + h @@ -1269,10 +1328,7 @@ def _setup(self): a = self._decoder(rawmode, l) # FIXME: this doesn't work if the image size # is not a multiple of the tile size... - self.tile.append( - (self._compression, - (x, y, x+w, y+h), - o, a)) + self.tile.append((self._compression, (x, y, x + w, y + h), o, a)) x = x + w if x >= self.size[0]: x, y = 0, y + h @@ -1290,6 +1346,8 @@ def _setup(self): if self.mode == "P": palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) + + # # -------------------------------------------------------------------- # Write TIFF files @@ -1315,7 +1373,6 @@ def _setup(self): "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), - "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), @@ -1328,17 +1385,16 @@ def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError: - raise IOError("cannot write mode %s as TIFF" % im.mode) + raise OSError("cannot write mode %s as TIFF" % im.mode) ifd = ImageFileDirectory_v2(prefix=prefix) - compression = im.encoderinfo.get('compression', - im.info.get('compression', 'raw')) + compression = im.encoderinfo.get("compression", im.info.get("compression", "raw")) - libtiff = WRITE_LIBTIFF or compression != 'raw' + libtiff = WRITE_LIBTIFF or compression != "raw" # required for color libtiff images - ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1) + ifd[PLANAR_CONFIGURATION] = getattr(im, "_planar_configuration", 1) ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] @@ -1358,10 +1414,16 @@ def _save(im, fp, filename): # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, 'tag_v2'): + if hasattr(im, "tag_v2"): # preserve tags from original TIFF image file - for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, - IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): + for key in ( + RESOLUTION_UNIT, + X_RESOLUTION, + Y_RESOLUTION, + IPTC_NAA_CHUNK, + PHOTOSHOP_CHUNK, + XMP, + ): if key in im.tag_v2: ifd[key] = im.tag_v2[key] ifd.tagtype[key] = im.tag_v2.tagtype.get(key, None) @@ -1371,20 +1433,24 @@ def _save(im, fp, filename): if "icc_profile" in im.info: ifd[ICCPROFILE] = im.info["icc_profile"] - for key, name in [(IMAGEDESCRIPTION, "description"), - (X_RESOLUTION, "resolution"), - (Y_RESOLUTION, "resolution"), - (X_RESOLUTION, "x_resolution"), - (Y_RESOLUTION, "y_resolution"), - (RESOLUTION_UNIT, "resolution_unit"), - (SOFTWARE, "software"), - (DATE_TIME, "date_time"), - (ARTIST, "artist"), - (COPYRIGHT, "copyright")]: + for key, name in [ + (IMAGEDESCRIPTION, "description"), + (X_RESOLUTION, "resolution"), + (Y_RESOLUTION, "resolution"), + (X_RESOLUTION, "x_resolution"), + (Y_RESOLUTION, "y_resolution"), + (RESOLUTION_UNIT, "resolution_unit"), + (SOFTWARE, "software"), + (DATE_TIME, "date_time"), + (ARTIST, "artist"), + (COPYRIGHT, "copyright"), + ]: name_with_spaces = name.replace("_", " ") if "_" in name and name_with_spaces in im.encoderinfo: - warnings.warn("%r is deprecated; use %r instead" % - (name_with_spaces, name), DeprecationWarning) + warnings.warn( + "%r is deprecated; use %r instead" % (name_with_spaces, name), + DeprecationWarning, + ) ifd[key] = im.encoderinfo[name.replace("_", " ")] if name in im.encoderinfo: ifd[key] = im.encoderinfo[name] @@ -1411,7 +1477,7 @@ def _save(im, fp, filename): ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut) # data orientation - stride = len(bits) * ((im.size[0]*bits[0]+7)//8) + stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) ifd[ROWSPERSTRIP] = im.size[1] ifd[STRIPBYTECOUNTS] = stride * im.size[1] ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer @@ -1440,11 +1506,11 @@ def _save(im, fp, filename): # the original file, e.g x,y resolution so that we can # save(load('')) == original file. legacy_ifd = {} - if hasattr(im, 'tag'): + if hasattr(im, "tag"): legacy_ifd = im.tag.to_v2() - for tag, value in itertools.chain(ifd.items(), - getattr(im, 'tag_v2', {}).items(), - legacy_ifd.items()): + for tag, value in itertools.chain( + ifd.items(), getattr(im, "tag_v2", {}).items(), legacy_ifd.items() + ): # Libtiff can only process certain core items without adding # them to the custom dictionary. It will segfault if it attempts # to add a custom tag without the dictionary entry @@ -1454,7 +1520,7 @@ def _save(im, fp, filename): continue if tag not in atts and tag not in blocklist: if isinstance(value, unicode if bytes is str else str): - atts[tag] = value.encode('ascii', 'replace') + b"\0" + atts[tag] = value.encode("ascii", "replace") + b"\0" elif isinstance(value, IFDRational): atts[tag] = float(value) else: @@ -1467,35 +1533,36 @@ def _save(im, fp, filename): # we're storing image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. - if im.mode in ('I;16B', 'I;16'): - rawmode = 'I;16N' + if im.mode in ("I;16B", "I;16"): + rawmode = "I;16N" a = (rawmode, compression, _fp, filename, atts) # print(im.mode, compression, a, im.encoderconfig) - e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig) - e.setimage(im.im, (0, 0)+im.size) + e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig) + e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16*1024) + l, s, d = e.encode(16 * 1024) if not _fp: fp.write(d) if s: break if s < 0: - raise IOError("encoder error %d when writing image file" % s) + raise OSError("encoder error %d when writing image file" % s) else: offset = ifd.save(fp) - ImageFile._save(im, fp, [ - ("raw", (0, 0)+im.size, offset, (rawmode, stride, 1)) - ]) + ImageFile._save( + im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + ) # -- helper for multi-page save -- if "_debug_multipage" in im.encoderinfo: # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd + # # -------------------------------------------------------------------- # Register diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index b621460db30..d5352559e1e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -964,14 +964,15 @@ def test_open_tiff_uint16_multiband(self): """Test opening multiband TIFFs and reading all channels.""" base_value = 4660 for i in range(2, 6): - infile = "Tests/images/uint16_{}_{}.tif".format(i, base_value) + infile = f"Tests/images/uint16_{i}_{base_value}.tif" im = Image.open(infile) im.load() pixel = [base_value + j for j in range(0, i)] - actual_pixel = im.getpixel((0,0)) + actual_pixel = im.getpixel((0, 0)) if type(actual_pixel) is int: actual_pixel = [actual_pixel] self.assertEqual(actual_pixel, pixel) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main()