Skip to content

APNG save_all merges or crops I;16 frames that differ only above 255 #10329

Closed

Description

@brettdavies

What did you do?

I'm building an image-optimization tool that prepares screenshots and photos for PDFs: it picks the output format, carries the DPI so pages place at the right size, and checks that lossless steps keep every pixel. Its test suite generates each input type Pillow opens and runs it through the tool, which is where this came up. While generating 16-bit grayscale APNG inputs with save_all, a three-frame animation came back as one frame.

import io
from PIL import Image

def roundtrip(frames, **kwargs):
    buf = io.BytesIO()
    frames[0].save(buf, "PNG", save_all=True, append_images=frames[1:], **kwargs)
    with Image.open(buf) as im:
        values = []
        for i in range(getattr(im, "n_frames", 1)):
            im.seek(i)
            values.append((im.getpixel((0, 0)), im.getpixel((7, 7))))
        return values

# Three distinct frames are written as one
frames = [Image.new("I;16", (8, 8), v) for v in (1000, 2000, 3000)]
print(roundtrip(frames, duration=100))

# The second frame's changed region is cropped to the pixels that differ below 256
first = Image.new("I;16", (8, 8), 1000)
second = Image.new("I;16", (8, 8), 2000)
second.putpixel((0, 0), 50)
print(roundtrip([first, second]))

What did you expect to happen?

Three frames, [(1000, 1000), (2000, 2000), (3000, 3000)], and in the second case a second frame of (50, 2000).

What actually happened?

[(1000, 1000)]
[(1000, 1000), (50, 1000)]

The frames are merged, and where a frame differs both below and above 255 only the region that differs below 256 is written. _write_multiple_frames compares im_frame.convert("RGBA") with base_im.convert("RGBA"), and converting I;16 to RGBA clips everything above 255, so those differences disappear. This may be close to what #10273 is changing.

Related: #10330 (16-bit tRNS key in convert("RGBA")).

What are your OS, Python and Pillow versions?

  • OS: Ubuntu 24.04 (Linux x86_64)
  • Python: 3.13.13
  • Pillow: 13.0.0 (also reproduces on 12.3.0)
--------------------------------------------------------------------
Pillow 13.0.0
Python 3.13.13 (main, May 10 2026, 19:26:54) [Clang 22.1.3 ]
--------------------------------------------------------------------
Python executable is ~/.venv/bin/python
Environment Python files loaded from ~/.venv
System Python files loaded from ~/.local/share/uv/python/cpython-3.13.13-linux-x86_64-gnu
--------------------------------------------------------------------
Python Pillow modules loaded from ~/.venv/lib/python3.13/site-packages/PIL
Binary Pillow modules loaded from ~/.venv/lib/python3.13/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 13.0.0
--- TKINTER support ok, loaded 9.0
--- FREETYPE2 support ok, loaded 2.14.3
--- LITTLECMS2 support ok, loaded 2.19
--- WEBP support ok, loaded 1.6.0
--- AVIF support ok, loaded 1.4.2
--- JPEG support ok, compiled for libjpeg-turbo 3.2.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.4
--- ZLIB (PNG/ZIP) support ok, loaded 1.3, compiled for zlib-ng 2.3.3
--- LIBTIFF support ok, loaded 4.7.2
--- RAQM (Bidirectional Text) support ok, loaded 0.11.0, fribidi 1.0.13, harfbuzz 14.5.0
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions