Skip to content

Add low-memory JPEG decode path#1

Draft
mariusandra wants to merge 9 commits into
emojisfrom
embedded
Draft

Add low-memory JPEG decode path#1
mariusandra wants to merge 9 commits into
emojisfrom
embedded

Conversation

@mariusandra

Copy link
Copy Markdown
Collaborator

Needed this to be able to load JPEGs on an ESP32...

mariusandra and others added 9 commits June 14, 2026 12:08
- pixie/decodebudget: runtime per-decode memory budget (replaces the
  compile-time frameosEmbedded consts); decoders raise catchable
  PixieErrors instead of exhausting memory
- jpeg: decode plan checked against the budget before any image-sized
  allocation; progressive JPEGs now use target-sized channel masks;
  sampling resolution clamps itself to the budget, trading sharpness
  for a successful decode
- jpeg: streaming decode (decodeJpegStreamScaled/Into) pulls the
  compressed input through a 32K sliding window so files never need to
  be fully buffered; bit-identical output across the test suite
- jpeg: decodeJpegInfo probe + jpegDecodeIntermediateBytes for
  pre-decode budget planning
- jpeg/png: ScaledDecodeFit (stretch/cover/contain) on all scaled
  decode paths, enabling aspect-correct decode-into-canvas; cover
  inflates mask sampling density for the cropped region within budget
- png: budget check at IHDR; inflated scanlines released before the
  pixel seq allocation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
seekEntropyMarker could set state.pos behind the sliding window start on
a long 0xFF run in damaged entropy data; clamp to windowStart so recovery
matches the buffered decoder instead of failing the whole decode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unfiltering allocated a second scanline-sized buffer, putting the decode
plan for a canvas-sized RGBA PNG at pixels + 2x scanlines (4.5MB for
480x800). Compacting the [filter byte][row] stride in place drops the
plan to pixels + scanlines (3.0MB), which fits the decode budget on
ESP32-class devices. Interlaced images keep the per-pass copy and the
old plan formula.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Non-interlaced PNGs now decode row by row through zippy's streaming
inflate (pinned to the FrameOS fork): scanlines are unfiltered against
a single previous-row buffer and written straight into the pixel data,
so the whole-image inflate buffer disappears. Full decodes plan
pixels + a fixed ~64KB; decodePngScaled/Into sample rows on the fly and
never allocate the full-size pixel buffer at all, so a huge PNG can
scale into display bounds like a streamed JPEG.

Interlaced and 16-bit-scaled decodes keep the buffered path. Verified
pixel-identical to the previous decoder across the whole pngsuite
corpus, plus differential streamed-vs-buffered scaled decode tests and
tightened budget assertions (480x800 RGBA: full decode within 2MB,
scaled-into within 256KB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A transitive URL requirement breaks nimble's CI resolution (two zippy
sources for one package name); frameos.nimble pins the FrameOS zippy
fork at the root instead, the same proven pattern used for pixie.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI's nimble cannot reliably resolve a second forked package (transitive
or root URL requirements both produced incomplete nimble.paths), so the
streaming inflate now lives inside pixie as a self-contained module
(huffman machinery vendored from zippy 0.10.16, MIT) raising PixieError
directly. The dependency graph returns to the CI-proven shape: one
forked package (pixie), stock guzba zippy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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