Skip to content

fix(render): budget decoder intermediates on top of the decoded image - #14

Merged
lzwind merged 1 commit into
linuxdeepin:mainfrom
YangtseSu:fix/decoded-image-alloc-budget
Oct 19, 2026
Merged

lzwind merged 1 commit into
linuxdeepin:mainfrom
YangtseSu:fix/decoded-image-alloc-budget

Conversation

@YangtseSu

@YangtseSu YangtseSu commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Both image decode sites handed the image crate a single budget derived from the finished
RGBA8 raster, while Limits::max_alloc accounts for every allocation the decoder makes —
its own sample buffer, palette and row buffers included. A picture whose raster fits the
limits therefore failed with Memory limit exceeded before those limits were consulted.

Site 1 — ImageDecoder::decode (crates/rofd-render/src/images.rs)

crates/rofd-render/tests/images.rs::decoded_limits_have_exact_and_one_over_boundaries is red
on main (no CI job runs the suite today):

thread 'decoded_limits_have_exact_and_one_over_boundaries' panicked at crates/rofd-render/tests/images.rs:222:5:
assertion failed: decoder.decode(&resource, &exact).is_ok()
Err(ImageDecode { resource_id: 10, path: "Doc_0/image.bin", message: "Memory limit exceeded" })

max_decoded_image_bytes bounds the finished RGBA8 image, which validate_dimensions already
checks before decoding; passing that size as the allocator budget rejects an image that is
exactly at its limit.

Site 2 — decode_seal_raster (crates/rofd-render/src/cairo_renderer.rs)

Stamp annotations (electronic seals) rasterize their picture through the same crate with the
default limits, min(max_decoded_image_bytes, max_raster_bytes) as the budget. Consequences,
measured with the real decoder on 16-bit RGBA PNGs:

seal decoded RGBA8 decoder buffers old budget with this change
6000×6000 137 MiB 274 MiB Err — seal silently dropped decodes
8192×8192 RGBA8 256 MiB 256 MiB Err — dropped decodes, then RasterBudgetExceeded
10240×10240 400 MiB 400 MiB Err "Memory limit exceeded" decodes, then RasterBudgetExceeded
14336×14336 784 MiB 784 MiB Err Err (still bounded)

Both limits are documented to bound the finished image, so a seal inside them must render;
today it is dropped without a diagnostic, and a seal outside them reports a decode failure
instead of the designed RasterBudgetExceeded.

Change

Budget the decoder with three times the finished raster, and leave the raster checks as the
authority (the pre-decode dimension limit and the post-decode RasterBudgetExceeded comparison
are unchanged): a decoder holds its own buffer (up to 8 bytes per pixel for 16-bit channels) and
the RGBA8 conversion (4 bytes per pixel) at the same time, so the exact boundary of the caller's
budget decodes while the wall stays proportional to it.

Verification

  • cargo test -p rofd-render --test images → 14/14 (1/14 fails at 140b936)
  • cargo test -p rofd-render → green
  • cargo fmt --all -- --check, cargo clippy -p rofd-render --all-targets -- -D warnings → clean
  • New regression test
    crates/rofd-render/tests/cairo_stamp.rs::a_large_seal_raster_within_the_document_limits_is_not_called_over_budget
    uses a generated 6000×6000 16-bit RGBA seal (420 KiB, ~11 s): it fails before the change
    (the stamp pixel stays page-white) and passes after. The fixture is generated by the few
    lines recorded in crates/rofd-render/tests/fixtures/images/PROVENANCE.md.
  • Throwaway probes reproduced the table above (and confirmed the 16-bit path is over the old
    budget while the RGBA8 path only is on compressible input).

@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Adjusts decoder budgeting so its intermediate allocations are accounted for separately from the validated final RGBA8 image, allowing exact-limit images to decode without allowing images beyond the caller's dimensions or decoded-size limits.

Sequence diagram for decoder memory budgeting

sequenceDiagram
    participant Caller
    participant Decoder as ImageDecoder
    participant Limits as DimensionAndSizeValidation
    participant Buffers as DecoderBuffers
    participant RGBA as RGBA8Image

    Caller->>Decoder: decode_pixels()
    Decoder->>Limits: validate_dimensions(width, height)
    Limits-->>Decoder: dimensions within max_decoded_image_bytes
    Decoder->>Decoder: max_alloc = max_decoded_image_bytes.saturating_mul(3)
    Decoder->>Buffers: decode intermediate samples, palette, and rows
    Decoder->>RGBA: convert to finished RGBA8 buffer
    RGBA-->>Caller: decoded image
Loading

Flow diagram for exact decoded-image budget boundaries

flowchart LR
    Input[Image resource] --> Validate[Validate dimensions and final RGBA8 size]
    Validate -->|within caller budget| Budget[Set decoder max_alloc to 3x final size]
    Budget --> Decode[Decode intermediate buffers and RGBA8 output]
    Decode --> Exact[Exact-limit image succeeds]
    Validate -->|exceeds caller budget| Reject[Reject before decoding]
    Decode -->|decoder budget exceeded| RejectDecode[Reject with ImageLimitExceeded]
Loading

File-Level Changes

Change Details Files
Increase the image decoder's allocation budget to account for intermediate decoder buffers alongside the final RGBA8 output.
  • Keep the existing pre-decode width, height, and decoded-image-size validation unchanged.
  • Set the decoder's running allocation limit to three times the caller's decoded RGBA8 byte budget, using saturating arithmetic.
  • Document the memory rationale, including simultaneous 16-bit source buffers and RGBA8 conversion storage.
crates/rofd-render/src/images.rs
Verify exact-boundary decoding and preserve rejection of over-limit images across rendering and related workspace checks.
  • Confirm the existing image boundary test passes at the exact limit and fails one byte below.
  • Exercise 16-bit RGBA and full-palette PNG cases, plus formatting, clippy, and workspace tests.
crates/rofd-render/tests/images.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@deepin-ci-robot

Copy link
Copy Markdown

Hi @YangtseSu. Thanks for your PR.

I'm waiting for a linuxdeepin member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

YangtseSu added a commit to YangtseSu/rofd that referenced this pull request Oct 15, 2026
The first split only covered the region pixel diff; the decoded image limit boundary
test is red on upstream main everywhere (it is what PR linuxdeepin#14 fixes), so the gating suite
failed on every branch. Both now run as their own visible, non-gating steps with the
reason and the fixing PR inline; the gate is everything else, so branch regressions
still fail the run.
Both image decode sites handed the image crate a single budget derived from the
*finished* RGBA8 raster, while `Limits::max_alloc` accounts for every allocation the
decoder makes - its own sample buffer, palette and row buffers included. A picture
whose raster fits the limits therefore still failed with "Memory limit exceeded"
before those limits were consulted:

- `ImageDecoder::decode` rejected every image whose decode needs more than one
  raster's worth of memory, which is why
  `crates/rofd-render/tests/images.rs::decoded_limits_have_exact_and_one_over_boundaries`
  is red on main (`decoded_limits_have_exact_and_one_over_boundaries`).
- `decode_seal_raster` (stamp annotations, i.e. electronic seals) reported a decode
  error instead of `RasterBudgetExceeded`, and silently dropped seals that are inside
  the limits. Measured with the real decoder on 16-bit RGBA PNGs: a 6000x6000 seal
  (137 MiB raster, 274 MiB decoder buffers, both well inside
  `ResourceLimits::max_decoded_image_bytes` = 400 MiB) failed to decode at the old
  budget and decodes now; a 14336x14336 one is still rejected.

Budget the decoder with three times the finished raster and keep the raster checks as
the authority (the pre-decode dimension limit and the post-decode
`RasterBudgetExceeded` comparison are unchanged). A decoder holds its own buffer (up
to 8 bytes per pixel for 16-bit channels) and the RGBA8 conversion at the same time,
so the exact boundary of the caller's budget decodes and the wall stays proportional
to it.

Tests: `cargo test -p rofd-render --test images` 14/14 (1/14 at 140b936), the new
`cairo_stamp.rs::a_large_seal_raster_within_the_document_limits_is_not_called_over_budget`
(6000x6000 16-bit seal, generated fixture, 420 KiB, ~11 s) fails before and passes
after, and `cargo test -p rofd-render` is green. Verified with `cargo fmt --all --
--check` and `cargo clippy -p rofd-render --all-targets -- -D warnings`.
@YangtseSu
YangtseSu force-pushed the fix/decoded-image-alloc-budget branch from 90b76a0 to 2983cf6 Compare October 15, 2026 07:16
@YangtseSu YangtseSu changed the title fix(render): budget the decoder's own buffers, not only the RGBA image fix(render): budget decoder intermediates on top of the decoded image Oct 15, 2026
archlinux-github pushed a commit to archlinux/aur that referenced this pull request Oct 15, 2026
The image crate allocates its own intermediate buffers on top of the decoded RGBA8
raster, so budgeting the decoder with the raster size alone dropped seals (stamp
annotations) that are inside the document limits, and reported a decode error where a
raster-budget error is designed. Upstream PR linuxdeepin/rofd#14 carries the fix; this
package applies its patch. Binary diffs do not survive format-patch + patch, so the
regression test fixture ships as a checked source and prepare() installs it.

Rebuilt from the pinned 140b936 tree: prepare() applies the three patches cleanly, the
package exports the same 78 symbols (rofd_document_get_page_size_mm included), and the
papers-ofd suite passes 12/12 against the packaged library.
YangtseSu added a commit to YangtseSu/rofd that referenced this pull request Oct 15, 2026
Merging the workflow onto the release branch left its trigger on
ci/fork-checks, so nothing ran on the branch that actually ships (0 runs for
release/0.4.3-fork.1). Point push at release/* and make a release ref run
strict: no --skip and no continue-on-error steps, because a release branch
carries the fixes (PRs linuxdeepin#14, linuxdeepin#15) that make both upstream-red tests green, so
they gate like everything else. Non-release refs, reachable only through
workflow_dispatch, keep the loose mode: the two tests run as visible,
non-gating steps so a branch's own regressions still fail the run.
@YangtseSu

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA.

@lzwind
lzwind merged commit bef8a3e into linuxdeepin:main Oct 19, 2026
12 of 13 checks passed
@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: lzwind, YangtseSu

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

YangtseSu added a commit to YangtseSu/rofd that referenced this pull request Oct 19, 2026
…merged

Upstream `main` carries the decoder budget and the region tolerance (PRs linuxdeepin#14 and linuxdeepin#15), so neither previously red test is red anywhere any more and the dev-only distinction between strict and loose checking has no purpose left. The ref matrix loses its `strict` field, the two `continue-on-error` steps go, and FORK.md describes the single mode.
YangtseSu added a commit to YangtseSu/rofd that referenced this pull request Oct 19, 2026
…merged

Upstream `main` carries the decoder budget and the region tolerance (PRs linuxdeepin#14 and linuxdeepin#15), so neither previously red test is red anywhere any more and the dev-only distinction between strict and loose checking has no purpose left. The ref matrix loses its `strict` field, the two `continue-on-error` steps go, and FORK.md describes the single mode.
@YangtseSu
YangtseSu deleted the fix/decoded-image-alloc-budget branch October 19, 2026 09:01
YangtseSu added a commit to YangtseSu/rofd that referenced this pull request Oct 20, 2026
…merged

Upstream `main` carries the decoder budget and the region tolerance (PRs linuxdeepin#14 and linuxdeepin#15), so neither previously red test is red anywhere any more and the dev-only distinction between strict and loose checking has no purpose left. The ref matrix loses its `strict` field, the two `continue-on-error` steps go, and FORK.md describes the single mode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants