Pillow icon indicating copy to clipboard operation
Pillow copied to clipboard

TIFF save options are not applied to appended images

Open jvanderneutstulen opened this issue 7 months ago • 6 comments

What did you do?

Save a multiframe tiff image with group3 compression and PhotometricInterpretation WhiteIsZero.

What did you expect to happen?

Settings applied to all frames.

pageaaa.tif: TIFF image data, little-endian, direntries=9, height=2291, bps=1, compression=bi-level group 3, PhotometricInterpretation=WhiteIsZero, width=1686
pageaab.tif: TIFF image data, little-endian, direntries=9, height=2291, bps=1, compression=bi-level group 3, PhotometricInterpretation=WhiteIsZero, width=1686
pageaac.tif: TIFF image data, little-endian, direntries=9, height=2291, bps=1, compression=bi-level group 3, PhotometricInterpretation=WhiteIsZero, width=1686
pageaad.tif: TIFF image data, little-endian, direntries=9, height=2291, bps=1, compression=bi-level group 3, PhotometricInterpretation=WhiteIsZero, width=1686

What actually happened?

Settings apply only to the first frame.

Pageaaa.tif: TIFF image data, little-endian, direntries=9, height=2291, bps=1, compression=bi-level group 3, PhotometricInterpretation=WhiteIsZero, width=1686
pageaab.tif: TIFF image data, little-endian, direntries=8, height=2291, compression=none, PhotometricInterpretation=BlackIsZero, width=1686
pageaac.tif: TIFF image data, little-endian, direntries=8, height=2291, compression=none, PhotometricInterpretation=BlackIsZero, width=1686
pageaad.tif: TIFF image data, little-endian, direntries=8, height=2291, compression=none, PhotometricInterpretation=BlackIsZero, width=1686

What are your OS, Python and Pillow versions?

  • OS: Ubuntu 24.04 LTS
  • Python: 3.12.3
  • Pillow: 11.2.1

Pillow 11.1.0 yields the expected result.

git bisect results:

5c93145061953d8633397bb79ace396ab1e71eb5 is the first bad commit
commit 5c93145061953d8633397bb79ace396ab1e71eb5
Author: Andrew Murray <[email protected]>
Date:   Fri Feb 28 22:16:52 2025 +1100

    Allow encoderconfig and encoderinfo to be set for appended TIFF images

 Tests/test_file_tiff.py              | 12 ++++++++++++
 docs/handbook/image-file-formats.rst |  4 +---
 src/PIL/TiffImagePlugin.py           | 15 ++++++---------
 3 files changed, 19 insertions(+), 12 deletions(-)

Example code:

from PIL import Image

fax_images = []

for page in ["img/1.png", "img/2.png", "img/3.png", "img/4.png"]:

    image = Image.open(page)
    image = image.convert(mode="1")
    fax_images.append(image)


# TIFF settings (0 is white)
tiffinfo = {262: 0}

first = fax_images.pop(0)
first.save(
    "fax-document.tif",
    "TIFF",
    append_images=fax_images,
    save_all=True,
    compression="group3",
    tiffinfo=tiffinfo,
)

Test commands:

python test-case.py
tiffsplit fax-document.tif page
file page*tif

jvanderneutstulen avatar May 21 '25 10:05 jvanderneutstulen

Hi. This change was made to allow users to save different information to different frames, see #8775.

Instead, you can specify the arguments for each subsequent frame with image.encoderinfo = {"tiffinfo": {262: 0}, "compression": "group3"}.

from PIL import Image

fax_images = []

for page in ["img/1.png", "img/2.png", "img/3.png", "img/4.png"]:

    image = Image.open(page)
    image = image.convert(mode="1")
    image.encoderinfo = {"tiffinfo": {262: 0}, "compression": "group3"}
    fax_images.append(image)


# TIFF settings (0 is white)
tiffinfo = {262: 0}

first = fax_images.pop(0)
first.save(
    "fax-document.tif",
    "TIFF",
    append_images=fax_images,
    save_all=True,
    compression="group3",
    tiffinfo=tiffinfo,
)

radarhere avatar May 21 '25 11:05 radarhere

Yes, that will work. But I would expect that the save options apply by default to all frames, as the old behavior was, unless you specify encoderinfo properties explicitly for a frame.

  • 11.1.0
  1. Save options apply to all frames
  2. Image encoderinfo ignored (encoderinfo of first frame applies to all)
  • 11.2.1
  1. Save options apply to first frame only
  2. Subsequent frames use only encoderinfo of the current frame
  • proposal:
  1. Save options apply to first frame, and all subsequent frames by default
  2. Subsequent frames will use default, unless explicit encoderinfo has been specified

This will restore the old behavior, and allow for different information in different frames.

I can make a PR for this.

jvanderneutstulen avatar May 21 '25 11:05 jvanderneutstulen

@anntzer, as the creator of #8775, do you have any thoughts on this?

radarhere avatar May 21 '25 12:05 radarhere

Sure, the proposed behavior seems reasonable (in fact, likely better). I guess the current strategy in Image.save (stash the params into encoderinfo and later let the plugin's _save_all parse that) will need to be refined to distinguish between globally set parameters and parameters originally set on the first frame's encoderinfo but not to be applied to all frames, though.

anntzer avatar May 23 '25 08:05 anntzer

I've created https://github.com/python-pillow/Pillow/pull/9001 to resolve this.

radarhere avatar Jun 07 '25 05:06 radarhere

This PR works for me and seems more generic than #8974, so please merge.

jvanderneutstulen avatar Jun 30 '25 08:06 jvanderneutstulen