diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index acb2f66bf0f..2d7c5bd38ba 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -399,21 +399,23 @@ def test_non_bytes( "tiffinfo", ( {TiffImagePlugin.IMAGEWIDTH: 1 << 27}, + {TiffImagePlugin.IMAGELENGTH: 1 << 27}, {TiffImagePlugin.SAMPLESPERPIXEL: 3}, + {TiffImagePlugin.PLANAR_CONFIGURATION: 2}, ), ) def test_geometry_tags( self, tiffinfo: dict[int, int], monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - # libtiff reads a scanline of the size described by the IMAGEWIDTH, - # SAMPLESPERPIXEL and BITSPERSAMPLE tags from a buffer that only holds one - # row of the image, so a tag describing a larger row must be rejected. + # The geometry of the image is used, not the geometry from tiffinfo. + # Otherwise libtiff would read past the end of the row buffer. monkeypatch.setattr(TiffImagePlugin, "WRITE_LIBTIFF", True) out = tmp_path / "temp.tif" im = hopper("L") - with pytest.raises(OSError, match="encoder error -8"): - im.save(out, tiffinfo=tiffinfo) + im.save(out, tiffinfo=tiffinfo) + + assert_image_equal_tofile(im, out) @pytest.mark.parametrize( "tag, value", diff --git a/docs/releasenotes/13.1.0.rst b/docs/releasenotes/13.1.0.rst index 7782570bba8..43999d2d20f 100644 --- a/docs/releasenotes/13.1.0.rst +++ b/docs/releasenotes/13.1.0.rst @@ -59,6 +59,15 @@ use, now copy an image before writing to it if its memory is read-only, as memory-mapped image files, or modify the buffer behind an image created by :py:func:`~PIL.Image.frombuffer` or :py:func:`~PIL.Image.fromarrow`. +Saving TIFF images with libtiff geometry tags +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a TIFF image with libtiff, the ImageWidth, ImageLength, +SamplesPerPixel and PlanarConfiguration tags are now always taken from the +image, rather than from ``tiffinfo``, ``exif`` or the IFD of the original file. +Previously, values that described a larger row than the image raised an +:py:exc:`OSError`, and other values could produce an invalid file. + Resizing I;16B and I;16N images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6dd34ea80fb..b821edb6474 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1825,9 +1825,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: libtiff = WRITE_LIBTIFF or compression != "raw" - # required for color libtiff images - ifd[PLANAR_CONFIGURATION] = 1 - ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] @@ -1846,6 +1843,8 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: + if libtiff and key in {IMAGEWIDTH, IMAGELENGTH}: + continue if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS: ifd[key] = info.get_ifd(key) else: @@ -1855,6 +1854,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: except Exception: pass # might not be an IFD. Might not have populated type + # required for color libtiff images + ifd[PLANAR_CONFIGURATION] = 1 + legacy_ifd = {} if hasattr(im, "tag"): legacy_ifd = im.tag.to_v2() @@ -1926,8 +1928,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if bits != (1,): ifd[BITSPERSAMPLE] = bits - if len(bits) != 1: - ifd[SAMPLESPERPIXEL] = len(bits) + ifd[SAMPLESPERPIXEL] = len(bits) if extra is not None: ifd[EXTRASAMPLES] = extra if format != 1: