Skip to content

16-bit PNG tRNS key: convert("RGBA") misses the keyed pixel and makes another transparent #10330

Open

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. The tool reads transparency through convert("RGBA"), and on a 16-bit RGB PNG with a tRNS key the keyed pixels stayed opaque.

import io, struct, zlib
from PIL import Image

def png(width, depth, colour_type, row, trns):
    def chunk(kind, data):
        return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
    ihdr = struct.pack(">IIBBBBB", width, 1, depth, colour_type, 0, 0, 0)
    return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"tRNS", trns)
            + chunk(b"IDAT", zlib.compress(b"\x00" + row)) + chunk(b"IEND", b""))

# 16-bit RGB: pixel 0 is the tRNS colour, pixel 1 is not
key = (0x1234, 0x5678, 0x9ABC)
row = struct.pack(">6H", *key, 0x3434, 0x7878, 0xBCBC)
with Image.open(io.BytesIO(png(2, 16, 2, row, struct.pack(">3H", *key)))) as im:
    print(im.mode, im.info["transparency"], [im.getpixel((x, 0)) for x in range(2)])
    print([im.convert("RGBA").getpixel((x, 0))[3] for x in range(2)])

# 16-bit greyscale: pixel 0 is the tRNS grey level, pixel 1 is not
row = struct.pack(">2H", 0x1234, 0x0034)
with Image.open(io.BytesIO(png(2, 16, 0, row, struct.pack(">H", 0x1234)))) as im:
    print(im.mode, im.info["transparency"], [im.getpixel((x, 0)) for x in range(2)])
    print([im.convert("RGBA").getpixel((x, 0))[3] for x in range(2)])

What did you expect to happen?

Alpha [0, 255] in both cases: the pixel equal to the tRNS value is transparent and the other is opaque, as for 8-bit files.

What actually happened?

RGB (4660, 22136, 39612) [(18, 86, 154), (52, 120, 188)]
[255, 0]
I;16 4660 [4660, 52]
[255, 0]

The keyed pixel stays opaque, and the pixel whose 8-bit value equals the key's low byte (0x34, 0x78, 0xBC) becomes transparent. 16-bit RGB opens as 8-bit RGB while info["transparency"] keeps the 16-bit tuple. #10320 covers the 2- and 4-bit gray case; this is the 16-bit one, and I'm not asking for 16-bit RGB support (#1888).

Related: #10329 (16-bit APNG frames merged on save).

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

  1. hugosmoreira commented on Oct 27, 2026

    @hugosmoreira

    I reproduced both examples on current main and added local regression tests for distinct 16-bit pixels that share the same high bytes. Shifting the RGB transparency key fixes the original RGB example but makes both pixels transparent in that counterexample.

    Would you accept decoding 16-bit RGB PNGs with a tRNS key directly to RGBA, matching the full key before reducing precision? This would change their public mode from RGB to RGBA and represent transparency in alpha rather than info["transparency"]. The grayscale case could retain I;16 and compare the original sample before conversion. I'd like to confirm this compatibility choice before implementing it.

  2. radarhere commented on Oct 28, 2026

    @radarhere
    Member

    The tool reads transparency through convert("RGBA")

    #10320 covers the 2- and 4-bit gray case

    Do you actually want to convert to RGBA, or are you just saying that because it is what was happening within PngImagePlugin? If it is the latter, then #10336 may have resolved this, as PngImagePlugin does not necessarily convert to RGBA anymore.

  3. hugosmoreira commented on Oct 28, 2026

    @hugosmoreira

    I reran the standalone #10330 example on current main (cb77e1bfaa) with a source-built Pillow on Linux/Python 3.13.16. Both direct convert("RGBA") cases still produce alpha [255, 0], rather than [0, 255]; that example does not save an APNG.

    Both original APNG examples from #10329 now produce the expected frames and pixels after #10336.

    The RGB precision case also remains: (0x1234, 0x5678, 0x9ABC) and (0x1235, 0x5678, 0x9ABC) decode to the same RGB tuple but require different alpha. Current conversion gives [255, 255]; shifting the transparency key gives [0, 0].

    I see #10338 already covers grayscale, so I'll avoid duplicating that work. The reporter can confirm their application's requirements; the remaining behavior I reproduced is direct RGB conversion, independently of APNG saving.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions