From 2dab52b8a8cc4e139a29ca78762079e573d32c31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 5 Aug 2021 23:27:08 +1000 Subject: [PATCH 1/2] Allow saving 1 mode TIFF with PhotometricInterpretation 0 --- Tests/test_file_tiff.py | 8 ++++++++ src/PIL/TiffImagePlugin.py | 32 +++++++++++++++++++++----------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 57f45bd09b2..a475c55b98e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -447,6 +447,14 @@ def test_exif_frames(self): im.seek(1) assert im.getexif()[273] == (1408, 1907) + def test_photometric(self, tmp_path): + filename = str(tmp_path / "temp.tif") + im = hopper("1") + im.save(filename, tiffinfo={262: 0}) + with Image.open(filename) as reloaded: + assert reloaded.tag_v2[262] == 0 + assert_image_equal(im, reloaded) + def test_seek(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6df4c361af6..2858694f859 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1487,7 +1487,9 @@ def _save(im, fp, filename): ifd = ImageFileDirectory_v2(prefix=prefix) - compression = im.encoderinfo.get("compression", im.info.get("compression")) + encoderinfo = im.encoderinfo + encoderconfig = im.encoderconfig + compression = encoderinfo.get("compression", im.info.get("compression")) if compression is None: compression = "raw" elif compression == "tiff_jpeg": @@ -1505,7 +1507,7 @@ def _save(im, fp, filename): ifd[IMAGELENGTH] = im.size[1] # write any arbitrary tags passed in as an ImageFileDirectory - info = im.encoderinfo.get("tiffinfo", {}) + info = encoderinfo.get("tiffinfo", {}) logger.debug("Tiffinfo Keys: %s" % list(info)) if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() @@ -1534,7 +1536,7 @@ def _save(im, fp, filename): # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech - icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) + icc = encoderinfo.get("icc_profile", im.info.get("icc_profile")) if icc: ifd[ICCPROFILE] = icc @@ -1550,10 +1552,10 @@ def _save(im, fp, filename): (ARTIST, "artist"), (COPYRIGHT, "copyright"), ]: - if name in im.encoderinfo: - ifd[key] = im.encoderinfo[name] + if name in encoderinfo: + ifd[key] = encoderinfo[name] - dpi = im.encoderinfo.get("dpi") + dpi = encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 ifd[X_RESOLUTION] = dpi[0] @@ -1568,7 +1570,15 @@ def _save(im, fp, filename): if format != 1: ifd[SAMPLEFORMAT] = format - ifd[PHOTOMETRIC_INTERPRETATION] = photo + if PHOTOMETRIC_INTERPRETATION not in ifd: + ifd[PHOTOMETRIC_INTERPRETATION] = photo + elif im.mode == "1" and ifd[PHOTOMETRIC_INTERPRETATION] == 0: + inverted_im = im.copy() + px = inverted_im.load() + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im if im.mode in ["P", "PA"]: lut = im.im.getpalette("RGB", "RGB;L") @@ -1605,8 +1615,8 @@ def _save(im, fp, filename): ifd.setdefault(tag, value) if libtiff: - if "quality" in im.encoderinfo: - quality = im.encoderinfo["quality"] + if "quality" in encoderinfo: + quality = encoderinfo["quality"] if not isinstance(quality, int) or quality < 0 or quality > 100: raise ValueError("Invalid quality setting") if compression != "jpeg": @@ -1695,7 +1705,7 @@ def _save(im, fp, filename): tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) - e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig) + e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: @@ -1715,7 +1725,7 @@ def _save(im, fp, filename): ) # -- helper for multi-page save -- - if "_debug_multipage" in im.encoderinfo: + if "_debug_multipage" in encoderinfo: # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd From 9bf7dae03d90fa48f61409d439d7ea7668f7e497 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Aug 2021 23:50:52 +1000 Subject: [PATCH 2/2] Allow saving L mode TIFF with PhotometricInterpretation 0 --- Tests/test_file_tiff.py | 5 +++-- src/PIL/TiffImagePlugin.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a475c55b98e..d5dda5799a6 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -447,9 +447,10 @@ def test_exif_frames(self): im.seek(1) assert im.getexif()[273] == (1408, 1907) - def test_photometric(self, tmp_path): + @pytest.mark.parametrize("mode", ("1", "L")) + def test_photometric(self, mode, tmp_path): filename = str(tmp_path / "temp.tif") - im = hopper("1") + im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: assert reloaded.tag_v2[262] == 0 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2858694f859..15678948c4a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -48,7 +48,7 @@ from fractions import Fraction from numbers import Number, Rational -from . import Image, ImageFile, ImagePalette, TiffTags +from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import o8 from .TiffTags import TYPES @@ -1572,13 +1572,16 @@ def _save(im, fp, filename): if PHOTOMETRIC_INTERPRETATION not in ifd: ifd[PHOTOMETRIC_INTERPRETATION] = photo - elif im.mode == "1" and ifd[PHOTOMETRIC_INTERPRETATION] == 0: - inverted_im = im.copy() - px = inverted_im.load() - for y in range(inverted_im.height): - for x in range(inverted_im.width): - px[x, y] = 0 if px[x, y] == 255 else 255 - im = inverted_im + elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0: + if im.mode == "1": + inverted_im = im.copy() + px = inverted_im.load() + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im + else: + im = ImageOps.invert(im) if im.mode in ["P", "PA"]: lut = im.im.getpalette("RGB", "RGB;L")