Skip to content

Reduce oversized images while decoding them - #563

Merged
Velli20 merged 1 commit into
mainfrom
reduce-oversized-images
Oct 11, 2026
Merged

Velli20 merged 1 commit into
mainfrom
reduce-oversized-images

Conversation

@Velli20

@Velli20 Velli20 commented Oct 11, 2026

Copy link
Copy Markdown
Owner

Summary

A page whose image XObject is 19522×27655 px (/Indexed /DeviceRGB, Flate + RunLength) failed with
failed to create skia raster image from data (19522x27655). Before that, it built every buffer at full resolution:

Stage Resident
Eager stream decode at document open 540 MB
Palette indices → RGB +1.6 GB
RGB → RGBA +2.2 GB
Skia copy +2.2 GB

The 2,159,620,440-byte RGBA buffer exceeds Skia's 2 GiB raster limit. A 19522 px side is also wider than GPU texture and browser canvas limits.

Images beyond 2^25 pixels, or 16384 px on a side, are now reduced while they are decoded, by the smallest whole factor that fits. Each output pixel averages its block of source pixels.

  • Filtered samples are decoded a band of factor rows at a time through the existing DecodedSamples conversion. No full-resolution component or RGBA buffer is built.
  • Gray and RGB samples are averaged before their RGBA conversion, so that conversion only runs on the reduced image. Other components are converted to RGBA per band first, and RGBA is averaged with alpha weighting.
  • JPEG 2000 images, and samples shorter than their declared rows, are decoded whole and then reduced.
  • The soft-mask combination size (combined_size) reuses the same PixelBudget search.

Results

The page renders from an 86 MB 3905×5531 image. Image decode time:

Build Before After
Optimized 1.47 s, then render error 1.54 s, renders
Debug 35 s, then render error 35 s, renders

Follow-ups (not in this PR)

  • Image streams are still decoded eagerly in ObjectCollection::insert: 540 MB resident for this file, and 3.8 s of RunLength decoding in debug builds.
  • Indexed images could be reduced directly from palette indices, skipping the RGB expansion.
  • Debug builds could optimize the image-decode crates, as is already done for pdf-jpeg2000.

Test plan

  • cargo test
  • cargo clippy --workspace -- -D warnings
  • cargo fmt --check
  • cargo conformance render on the file: the page renders and was checked by eye. There was no PDFium reference available locally, so it is not compared against PDFium.

🤖 Generated with Claude Code

A 19522x27655 Indexed image was expanded to full-resolution RGB and then
RGBA, about 6.5 GB at peak, before Skia refused the 2.16 GB pixel buffer
because it exceeds its 2 GiB raster limit, so the page failed to render.

An image beyond 2^25 pixels or 16384 pixels on a side is now reduced by
the smallest whole factor that fits, averaging each block of source
pixels. Filtered samples are decoded a band of rows at a time through
the existing conversion, and gray and RGB samples are averaged before
their RGBA conversion, so no full-resolution intermediate is built.
JPEG 2000 images, and samples shorter than their declared rows, are
decoded whole and then reduced. The soft-mask combination reuses the
same budget search.

The page now renders from an 86 MB 3905x5531 image. Decoding takes about
as long as before: 1.5 s optimized, 35 s in debug builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Conformance reads

Documents that Safe-PDF reads differently from the base commit, compared with PDFium. A read
regresses when Safe-PDF fails to open a document it opened before, crashes or times out, or
counts a different number of pages than PDFium.

pdf.js corpus

1008 documents compared with a02dc06: 1 read worse, 0 read better.

Regressions

Document Base This pull request
issue19517 read, 1 page timeout: time limit exceeded (stage safe-read)

840 documents of the PDFium corpus read as on a02dc06.

Details are in the job summaries of the workflow run. Rerun a case locally with
cargo conformance run --corpus <corpus> --read-only --case <id>.

@Velli20
Velli20 merged commit 0d1303f into main Oct 11, 2026
10 checks passed
@Velli20
Velli20 deleted the reduce-oversized-images branch October 11, 2026 20:03
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