Skip to content

Fix the soft mask of PNG images with packed palette indices - #1829

Open
wleeaf wants to merge 1 commit into
foliojs:masterfrom
wleeaf:png-packed-palette-transparency
Open

wleeaf wants to merge 1 commit into
foliojs:masterfrom
wleeaf:png-packed-palette-transparency

Conversation

@wleeaf

@wleeaf wleeaf commented Oct 26, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

Bug fix. A PNG image with a palette of 1, 2 or 4 bits per index and a tRNS chunk comes out mostly transparent. tests/images/pallete-transparent-white-1bit.png is an example: drawn with doc.image(), only about 4,300 of its 30,400 dark pixels show up when the page is rendered at the image's size, and the rest of the image is blank.

loadIndexedAlphaChannel reads the pixels png-js decodes as one palette index per byte. For fewer than 8 bits per index, png-js leaves several indices packed in each byte, so each pixel gets the alpha of a byte made up of several indices. The alpha channel also runs out of data after width * height * bits / 8 values, and the pixels after that stay transparent. The image data itself is fine, because it goes into the PDF as packed PNG rows with a PNG predictor.

png-js's decodePixels can't simply be reused for these images: it unfilters rows in steps of bits / 8 bytes, a fraction of a byte, whereas PNG filters them a byte at a time. So for non-interlaced images with fewer than 8 bits per index, this change inflates and unfilters the rows itself and unpacks the indices. It adds inflateSync to the zlib wrappers for that, using unzlibSync from fflate in the browser build. Images with 8 bits per index, and interlaced ones, go through png-js as before.

The rendering of the fixture above now matches its decoding by Pillow. With PNGs I made with 4 and 2 bits per index and one transparent entry, the mean difference per channel drops from 117 and 77 to 0.1 and 0.4.

Checklist:

  • Unit Tests: Pallete indexed transparency 1bit alpha channel in tests/unit/png.spec.js runs the real loadIndexedAlphaChannel on the 1-bit fixture. It checks that there is one alpha value per pixel and that 6,900 pixels are transparent and 7,600 opaque, as Pillow decodes the image. On master it finds 12,737 transparent pixels.
  • Documentation N/A
  • Update CHANGELOG.md
  • Ready to be merged

npx vitest run tests/unit (525 tests) and tests/visual (53 tests) pass, and eslint and prettier are clean on the changed files.

@wleeaf
wleeaf force-pushed the png-packed-palette-transparency branch from c095e7b to e16f8e3 Compare October 27, 2026 00:12

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant