Skip to content

otf-pixels-codec-jpeg@0.1.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 10:52
· 41 commits to main since this release

Fixed

  • Clippy 1.99, which CI runs, rejected two new lints (byte_char_slices in
    the JPEG decoder, manual slice filling in the AV1 tile decoder). Both are
    rewritten; behaviour is unchanged.
  • The JPEG codec built without its progressive feature warned about three
    unused stream-replay methods, which CI's -D warnings turned into a
    failure of every feature combination but the defaults. They are now
    compiled only with the feature.
  • The crates' repository link pointed at github.com/opentf/pixels, which
    is not where the code lives; it is now
    github.com/Open-Tech-Foundation/Pixels.
  • Output no longer refuses a pixel format its encoder cannot hold. A
    16-bit PNG, TIFF or 10/12-bit AVIF written as JPEG, WebP, AVIF or GIF
    failed with Unsupported, and float pixels could not be written at all.
    output() now narrows to what the format holds: 8 bits for those four,
    16 bits for PNG and TIFF.
  • The last column of an odd-width 4:2:0 or 4:2:2 AVIF was filtered toward
    its left chroma neighbour, where libavif (through libyuv) copies its own
    chroma sample. On sharp chroma this differed by up to 35. It now matches.
  • An AVIF whose coded frame repeated its header in an
    OBU_REDUNDANT_FRAME_HEADER after the tile data lost that tile data. A
    redundant copy now only stands in for a header that never arrived.
  • AVIF imir was documented, though not yet used, with its two modes swapped.
    Mode 0 exchanges top and bottom and mode 1 left and right, per ISO/IEC
    23008-12:2022 and libavif, which writes most AVIFs.
  • A PNG with tRNS reported an opaque pixel format until its first row was
    read. The chunks before IDAT are now read at construction, so the format
    is final from the start.
  • Any streaming decode resized to more than one tile column failed outright
    with "source cannot rewind" — 512x512 to 300x300 on PNG or JPEG alike, while
    512x512 to 32x32 worked, the boundary being one 128px tile. Consecutive
    requests to a forward-only source overlap rather than abut, because each
    output row of a resize needs input rows either side of it, and the retained
    band was discarded wholesale instead of carrying those rows over. The
    scheduler and the M1 reference evaluator now agree byte for byte across the
    shapes that used to fail.
  • DecodeCapability::Regions no longer claims to cover scaled-decode JPEG. A
    reduced-scale decode still emits rows in order and still entropy-decodes
    every coefficient; it is a decoder configuration, not random access, and the
    scheduler would have acted on the claim.
  • The MSRV job now checks without dev-dependencies. rust-version is a
    promise about consuming the library; the benchmark's fast_image_resize
    needs 1.87 and no user of otf-pixels ever compiles it.
  • The declared MSRV of 1.85 was not actually met: three let chains need
    1.88. Rewritten as nested if let, and CI now checks 1.85 so the promise
    stays true rather than aspirational.
  • A build without the raw feature now compiles. The exit-criteria suites and
    the crate-level example are expressed over the raw codec, so they compile out
    with it rather than failing to build.

Changed

  • CI's fuzz job installs a prebuilt cargo-fuzz instead of compiling it on
    every run.
  • The reference-manifest check (tsr references) is a local task, run after
    regenerating fixtures, and no longer a CI step. It re-encodes every
    fixture with whatever encoders the runner's distribution ships, so it could
    fail CI with no change in this repository. tsr interop stays in CI.
  • tsr test (and CI's test job) runs cargo test without the benchmark
    targets, which --all-targets used to run in full in a debug build. The
    benchmarks are a local task, tsr bench, and no longer a CI job.
  • CI runs through the tsr task runner, as ES-Runtime does. Every command
    CI runs is a task in tasks.toml, so tsr ci locally is what a pull request
    must pass, and tsr --list shows the rest (msrv, interop,
    references, bench, fuzz). RUSTFLAGS=-D warnings moves from the
    workflow into the tasks, so it applies locally too. The interop task also
    runs check-avif-interop.sh, which CI never ran before.
  • Published crates leave out their integration tests and fixtures, which
    only run from the repository, so dependents no longer download them: the
    AVIF crate drops from 3.2 MiB to 279 KiB compressed and the WebP crate
    from 2.6 MiB to 66 KiB. The unit tests that ship pass from the unpacked
    packages, on Rust 1.85 as declared. The facade's own tests need 1.87, for
    a benchmark-only dev-dependency.
  • Every published crate carries the Apache-2.0 LICENSE and the NOTICE,
    as the licence requires of redistributions.
  • Version 0.1.0, the first release candidate. All crates package and verify
    with cargo package --workspace.
  • Fit, Blend, Filter, Conversion and Unconvertible are
    #[non_exhaustive], so later modes can be added without breaking callers.
  • WebP output is now lossy by default, at EncodeOptions::quality
    (default 80), as with cwebp and sharp; set EncodeOptions::lossless for
    the lossless output that was previously the only kind.
  • otf-pixels-codec-avif: StillPicture records every tile group
    (tile_groups, tile_group_data) in place of one tile_data_offset/
    tile_data_len range, and decode_still takes the tile groups' bytes.
  • PngDecoder::new now reads through to the first IDAT header rather than
    stopping after IHDR, so that eXIf and tRNS are known at open. It still
    reads no image data.
  • JpegDecoder::orientation is now the Decoder trait method and returns
    Orientation instead of a raw Option<u8>.
  • SPEC's JPEG fast-path paragraph now records that it describes intent rather
    than facade behaviour, and what stands in the way: the planner analyses an
    immutable graph where selecting a reduced source would require rewriting it,
    and ops carrying pixel-valued parameters (crop, composite, convolve)
    do not mean the same thing against a reduced source. The decoder half is
    implemented and usable directly.
  • otf-pixels now evaluates pipelines with the tile scheduler instead of the
    M1 whole-image evaluator. Rows still reach an encoder top to bottom.
  • DecodedSource now streams instead of materializing the whole image on
    first pull. It advances a forward-only decoder to the requested band and
    retains just that band as a rolling window, which is what makes constant
    memory real rather than promised. A request behind the window is a reported
    error, never a silent rewind.
  • Flip now declares AccessPattern::Sequential, not Spatial.
    AccessPattern describes tile shape; a vertical mirror reads one input
    row per output row and wants full-width strips. Its row reversal is tile
    order, which input_regions already expresses and the scheduler resolves
    at the seam (ADR-0009).
  • SPEC §Guarantees 1 now states the constant-memory condition as "where the
    format and pipeline order allow", naming the reverse-order-over-sequential
    -source exception rather than leaving the guarantee quietly overstated.

Fixed

  • An AVIF file with an alpha plane, or with a colour matrix other than
    identity/BT.601/709/2020 (YCgCo, say), is now refused as Unsupported; an
    alpha file previously failed with a misleading "raster is short" error,
    because the descriptor promised RGBA while the decode produced RGB.
  • The AVIF decoder dropped the samples a transform block writes past the
    decoded area's right or bottom edge; the spec keeps them, and a later
    chroma-from-luma prediction reads that luma back. Planes now cover whole
    superblocks, with the decoded area tracked separately for intra edges.
  • A palette block overhanging the frame edge had its whole colour map decoded;
    only the on-screen part is coded, the rest replicating its last column and
    row, so such a block desynchronised the tile.
  • The AVIF decoder silently produced a wrong image — no error — for frames
    coded with per-superblock quantizer deltas (delta_q) or quantizer matrices,
    both of which libaom emits under common tuning (deltaq-mode=3,
    enable-qm=1); a 10-bit frame failed with a misleading "raster is short"
    malformed error. The tile decoder reads none of these tools, so it now
    refuses them — along with segmentation, delta_lf and monochrome — as
    Unsupported before decoding, keeping the promise that an unimplemented tool
    fails cleanly rather than decoding wrong. A new tests/unsupported.rs holds
    one real avifenc encode per refused tool and asserts each is refused for its
    own tool
    ; verified by removing the check, which let delta_q and qmatrix
    "decode".
  • The AVIF decoder read angle_delta and palette_mode_info only for blocks at
    least 8 samples in both dimensions. The spec's gate is MiSize >= BLOCK_8X8 in block-size enum order, where 4x16 and 16x4 sort after 8x8, so
    those two shapes do code both. A directional 4x16/16x4 block skipped its
    angle_delta symbol and a DC one skipped its palette flag, desynchronising
    the rest of the tile. The encoder only reaches for these shapes at slower
    speeds, which is why every fixture encoded at speed 6 decoded correctly; the
    filters-off lossy fixtures are now encoded at speed 4, and textured_nofilter
    joins them. Found by diffing our symbol stream value for value against an
    instrumented libaom decoder.
  • The AVIF decoder judged chroma-from-luma allowed only for a 4x4 block, which
    is the lossless 4:4:4 rule; for a lossy frame CfL is allowed for any block
    up to 32x32. An 8x8-or-larger lossy block therefore read uv_mode against the
    wrong CDF — the CfL-allowed alphabet carries an extra symbol the not-allowed
    one lacks — and desynchronised the whole tile, so every lossy frame with a
    block above 4x4 decoded to garbage while the lossless path (always 4x4-block
    CfL) stayed correct. is_cfl_allowed now follows the lossless flag, and lossy
    frames decode bit-exact against libavif at every transform size. Found by
    diffing our symbol stream against a libaom accounting trace. Three filters-off
    lossy fixtures (gradient_nofilter, blocks_nofilter, gradient_odd_nofilter)
    join the reference corpus.
  • The AVIF reconstruction decoded transform blocks whose top-left corner lies
    past the frame's right or bottom edge, which a partial superblock produces.
    The spec codes no such block, so the extra symbol reads desynchronised
    everything after them — surfacing as wrong chroma in the last region of a
    frame with odd dimensions while the luma before it stayed correct. The
    transform-block loop now skips them, and gradient_odd_lossless (37x29)
    decodes bit-exact.
  • The AV1 CDF table generator dropped the multiplication in spec entries of the
    form { 128 * 125, 32768, 0 }, splitting 128 * 125 into two separate values
    and lengthening every affected binary CDF by one bogus entry. The tokenizer
    now evaluates the product. This corrupted dc_sign and a dozen other binary
    CDFs, which silently desynchronised coefficient decoding — the kind of bug the
    generated-and-diffed approach exists to make visible, once something actually
    read the tables.

Changed

  • otf-pixels-codec-avif generalizes the AV1 tile reconstruct to arbitrary
    transform sizes and non-zero quantisers. The residual loop now reads a
    per-block luma transform size (read_block_tx_size, §5.11.16) and steps the
    transform blocks by it, chroma derives its own size, and the coefficient
    contexts (all_zero, dc_sign, level spread), the prediction edge/neighbour
    assembly, chroma-from-luma, and the flip-aware residual add all run at the
    block's real size — with real Dc_Qlookup/Ac_Qlookup steps per plane. The
    lossless intra path flows through the same general code and stays bit-exact
    against libavif, and lossy frames now decode bit-exact at every transform size
    when they code every in-loop post-filter off (filters_disabled): deblock,
    CDEF, loop restoration, super-resolution and film grain. A lossy frame with
    any post-filter active is still refused, since the reconstruct applies none.

Added

  • Output::with_scheduler: run on a Scheduler shared across pipelines.
    Without it every run spawns and joins a thread per core, which a server
    or runtime pays on every request. Many pipelines may share one scheduler
    concurrently. Scheduler is re-exported from the facade.
  • docs/EMBEDDING.md: putting the engine behind a runtime or server API
    (threading, per-request limits, error codes, the sharp option mapping,
    animation, memory). The crate docs now describe the v1 scope, with a
    serving example.
  • Image::to_pixel_format and the ConvertFormat op: depth (8-bit,
    16-bit, float) and layout (grey, grey with alpha, RGB, RGBA) conversion.
    PixelFormat::from_parts names a format by layout and depth.
  • All five of sharp's fit modes. Fit::Cover (scale to cover, crop the
    overflow centred), Fit::Contain (scale inside, pad centred with
    ResizeOptions::background, opaque black by default) and Fit::Outside
    join Fill and Inside. The crop and the padding happen inside the resize
    op itself, so cover and contain thumbnails keep the JPEG shrink-on-load
    path and match the reference evaluator however they are tiled.
  • Animation metadata. Image::animation (and Decoder::animation) report
    an animated GIF's or WebP's frame count, loop count and per-frame
    durations, matching Pillow and the timing libwebp wrote. The pipeline still
    processes the first frame. OpenOptions::with_animated(true) reserves the
    multi-frame API: until it is implemented, an animated input opened with it
    is refused as Unsupported instead of being flattened. GIF now buffers its
    compressed stream (at most 512 MiB) to count frames at open.
  • OpenOptions::with_limits: decode limits per open, so a runtime can bound
    untrusted input per request. Previously the facade always used the default
    268-megapixel limit.
  • ICC colour conversion to sRGB (ADR-0015). Opening an image converts
    matrix/TRC RGB and grey profiles (Display P3, Adobe RGB, ProPhoto,
    BT.2020, gamma, curv tables, every para curve) to sRGB and drops the
    profile. OpenOptions::with_to_srgb(false) keeps pixels as stored, and
    Image::to_srgb converts later. Profiles it cannot convert (LUT-based,
    CMYK, Lab PCS) stay with the pixels. Results are within one step of
    lcms2's on generated fixtures and on every RGB and grey profile colord and
    Ghostscript ship. ToSrgb is also usable as an op.
  • ICC profiles are preserved. Every decoder that can find one reports it
    (Decoder::icc_profile): PNG iCCP, JPEG APP2 (joined across
    segments, baseline and progressive), WebP ICCP, TIFF tag 34675 and AVIF
    colr. The facade carries it through the pipeline (Image::icc_profile,
    Image::with_icc_profile) and hands it to the encoder
    (Encoder::set_icc_profile), which writes it wherever the output format
    has a place for one. Pillow and libavif read every written profile back
    intact, including one large enough to span several JPEG segments.
  • AVIF decoding of YCgCo colour (matrix coefficients 8) and of
    premultiplied alpha (a prem reference), both previously refused as
    Unsupported. Premultiplied colour is returned straight, as the API
    carries it, rounded as libavif rounds it. Both are checked against
    libavif's output.
  • AVIF encoding: Format::Avif output through AvifEncoder, at
    EncodeOptions::quality. Colour is coded as 8-bit 4:2:0 BT.601 full-range
    YUV (monochrome for grey input), and transparency as an auxiliary alpha
    item; an opaque alpha channel is dropped. Lossless AVIF is refused as
    Unsupported. scripts/check-avif-interop.sh checks that libaom, dav1d and
    libgav1 (through avifdec) decode every emitted file to our pixels. On a
    768x512 photo the files are about 15-20% larger than avifenc -s 6 at
    equal PSNR.
  • An AV1 key-frame encoder for AVIF stills (8-bit, 4:2:0 or monochrome),
    built as the tile decoder driven by decisions: every symbol site asks a
    TileCoder, which either reads the stream or decides and writes it, so the
    encoder predicts, reconstructs and adapts its probabilities with the very
    code that decodes. Variance-driven partitioning down to 8x8, the closest of
    the 13 intra modes per block, forward transforms derived from the normative
    inverses, dead-zone quantization, deblocking and CDEF. libaom decodes every
    test stream to exactly what ours does. The AVIF container and the
    Format::Avif encoder that use it follow.
  • AVIF encoding groundwork: the AV1 multi-symbol arithmetic encoder (libaom's
    od_ec_enc, sharing the decoder's CDF adaptation, round-tripped symbol for
    symbol through our decoder) and writers for the reduced still-picture
    sequence header, the key-frame header and OBUs, each parsed back by our own
    parsers in its tests.
  • otf-pixels-codec-webp is now owned outright and image-webp is gone
    (ADR-0014 complete). An owned VP8L encoder writes lossless WebP — colour
    indexing with pixel bundling for up to 256 colours, otherwise subtract-green
    and a per-block predictor transform, then LZ77, a colour cache sized by an
    entropy estimate, and length-limited canonical prefix codes — and also
    compresses the ALPH chunk of lossy files. libwebp reads back every
    lossless file with exact pixels. On the lossless test corpus the files are
    about 3% larger than libwebp's at its slower settings, with the
    cross-colour transform, meta prefix codes (blocks clustered into groups
    with their own codes) and predictor modes chosen by coded cost; palette
    images come out equal or smaller.
  • Lossy WebP output. An owned VP8 encoder writes WebP at
    EncodeOptions::quality, mapped to the quantizer as libwebp maps it,
    choosing 16x16 or 4x4 prediction and chroma modes per macroblock by
    rate-distortion cost and rewriting the coefficient probabilities for what
    it chose. Alpha goes in an ALPH chunk, losslessly. Every file is checked
    by libwebp, which must decode it to exactly what our decoder does; size and
    PSNR are on par with libwebp's own encoder at equal quality on the test
    images. Encoder options (token partitions, simple filter, sharpness,
    segments, filter deltas) also put the decoder's remaining paths under
    libwebp's check.
  • EncodeOptions::lossless (and with_lossless): for a format with both
    modes, choose lossless.
  • otf-pixels-codec-webp: animated files decode their first frame through
    the owned decoders, placed on the canvas as libwebp's animation decoder
    does (a transparent-black canvas, the key frame written unblended), so
    decoding no longer uses image-webp at all. Checked against libwebp on
    Pillow-written animations and on hand-assembled ones whose first frame
    sits at an offset on a larger canvas.
  • otf-pixels-codec-webp: an owned VP8 (lossy) key-frame decoder, ALPH
    alpha decoding, and libwebp's YUV-to-RGB conversion (its "fancy" chroma
    upsampler and fixed-point BT.601 matrix), so lossy stills now decode
    through owned code and match libwebp byte for byte rather than within a
    tolerance. The VP8 probability and quantizer tables are generated from the
    reference decoder attached to RFC 6386 (scripts/generate-vp8-tables.py)
    and checked in CI. A 19-file lossy corpus — qualities 0 to 100, every
    segment map, 4x4 and 16x16 prediction, loop-filter levels 0 to 63,
    quantized and lossless alpha, 1x1 and one-pixel-wide images — decodes
    exactly as libwebp does. Only animations still go through image-webp.
  • ADR-0014: own WebP, lossy and lossless, decode and encode, and remove
    image-webp. The wrapped encoder can only write lossless WebP, and lossy
    WebP output with quality control is one of the things an image API is
    called for most.
  • otf-pixels-codec-webp: an owned RIFF container parser and VP8L
    (lossless) decoder, which lossless stills now decode through. All four
    transforms, colour indexing at every pixel-bundling width, the colour
    cache, LZ77 with the short-distance neighbourhood map, and meta prefix
    codes; prefix-code tables are built only for groups the image uses, so a
    hostile file cannot make memory outgrow its own size. A 20-file corpus
    written by libwebp at settings chosen to reach each of those tools decodes
    exactly as libwebp does. Lossy and animated images still go through
    image-webp for now.
  • AVIF segmentation, which rav1e (the encoder behind ravif and the Rust
    image crate) uses to give parts of the frame their own quantizer. Those
    files were refused as Unsupported. Each block's segment_id is read,
    predicted from its neighbours within the tile, and drives that block's
    quantizer (SEG_LVL_ALT_Q), lossless flag, quantizer-matrix level and
    transform-type coding; the forced-skip feature (SEG_LVL_SKIP) and the
    per-segment loop-filter offsets (SEG_LVL_ALT_LF_*) are applied too.
    Four rav1e plane fixtures (two speeds, 10-bit 4:4:4, tiled) and an SVT-AV1
    file match libaom exactly, and a rav1e photo matches libavif's RGB through
    the facade. No available encoder writes the skip or loop-filter segment
    features, so those are covered by unit tests only.
  • AVIF quantizer matrices (using_qmatrix), which libaom's enable-qm
    turns on. Those files were refused as Unsupported. Each 2D transform's
    coefficients are now dequantised with their position's matrix weight, and
    identity transforms are left alone, as §7.12.3 says. The 100,320-entry
    Quantizer_Matrix table is generated from a vendored spec extract by
    scripts/generate-av1-qm-tables.py. The generator checks every matrix size
    against the spec's own derivation from the fundamental matrices, and CI
    checks that the table is current. Four plane fixtures (levels 0, 3, 6 and 8;
    4:2:0, 10-bit 4:4:4, screen content) match libaom exactly, and a photo at
    libaom's default level matches libavif's RGB through the facade.
  • otf-pixels-codec-avif: dequantize_with_matrix and quantizer_matrix.
  • AVIF multi-tile decode. Encoders split large images into tiles (libavif's
    --autotiling, --tilecolslog2/--tilerowslog2), and those files were
    refused as Unsupported. Each tile now decodes from fresh CDFs and cleared
    above contexts, restarts CurrentQIndex, DeltaLF and the restoration
    references, and treats neighbours across its edge as unavailable; the
    in-loop filters then run over the whole frame. Tile groups split across
    several OBUs are joined in order, and a missing, repeated or overrunning
    tile is Malformed. Seven plane fixtures, among them uneven grids, 128x128
    superblocks, palettes at tile edges and multiple tile groups, match libaom
    exactly, and a tiled photo matches libavif's RGB through the facade.
  • AVIF per-superblock quantizer and loop-filter deltas (delta_q,
    delta_lf). libaom's key-frame visual-quality mode (deltaq-mode=3) sets
    them, so files that were refused as Unsupported now decode. CurrentQIndex
    drives dequantisation block by block, and each block's DeltaLF moves its
    deblocking strength, singly or per filter level with delta_lf_multi. Four
    plane fixtures (4:2:0, 10-bit 4:4:4, monochrome) match libaom exactly, and a
    4:2:0 photo matches libavif's RGB through the facade.
  • Auto-orientation, on by default as SPEC always said it was. Image::open
    and Image::from_stream turn an image upright as its metadata declares
    before any op runs: EXIF Orientation in JPEG, TIFF, PNG (eXIf) and WebP,
    and irot/imir in AVIF. Previously every phone photograph came out
    sideways. OpenOptions::with_auto_orient(false) with Image::open_with /
    Image::from_stream_with leaves pixels as stored, and Image::orient
    applies an orientation by hand. Rotation and mirroring rescale, so an
    oriented JPEG thumbnail still takes the shrink-on-load fast path. Checked
    against Pillow's exif_transpose for all eight EXIF values in every format
    and against libavif's --irot/--imir for AVIF
    (scripts/regenerate-orientation-reference.py).
  • otf-pixels-core: Orientation, the eight EXIF orientations with their
    rotate-then-mirror decomposition and a shared EXIF parser, and
    Decoder::orientation, which decoders report and never apply.
  • AVIF alpha and monochrome decode. A straight alpha plane — stored as a
    second, usually monochrome, AV1 image — becomes the alpha channel (Rgba8,
    Rgba16, GrayA8), expanded to full range if coded at studio range; a
    monochrome picture decodes to Gray8/Gray16. Monochrome reconstruction
    needed only the refusal lifted (two 4:0:0 plane fixtures match libaom), and
    seven reference fixtures cover RGBA at 8/10 bits, 4:2:0 with alpha, grey at
    8/10 bits and studio range, and grey + alpha. Premultiplied alpha (a prem
    reference, colour item to alpha item) is refused as Unsupported, since
    the API boundary is straight alpha. Note for the studio-range grey fixture:
    avifdec writes an 8-bit monochrome picture as a grey PNG of raw Y samples,
    skipping the range expansion, so that reference is taken from its 16-bit
    output instead — against which our decode is exact.
  • 10- and 12-bit AVIF now decodes, to Rgb16 over its full 0..=65535 range
    (the conversion computes RGB at 16 bits directly, as libavif does, rather
    than leaving samples in 10-bit units — the old doc comment claiming they
    "keep their original range" contradicted Rgb16's contract and is fixed).
    The AV1 reconstruction needed no change beyond lifting the refusal: bit
    depth was already threaded through every stage, and three new 10/12-bit
    plane fixtures match libaom exactly. Four 16-bit RGB reference fixtures
    (4:2:0, studio range, 12-bit 4:4:4, identity) agree with libavif — which
    converts wide samples in floating point — on 99% of samples and within one
    16-bit step on the rest. The regeneration script reads avifdec's 16-bit
    PNGs itself, since Pillow cannot hold 16-bit RGB.
  • AVIF files with real YUV now decode to RGB: 4:2:0, 4:2:2 and 4:4:4 with the
    BT.601, BT.709 or BT.2020 matrix at full or studio range — which covers what
    avifenc writes by default and what most AVIF in the wild uses. Chroma is
    upsampled with the triangle filter (9:3:3:1 in 4:2:0) and converted in
    16-bit fixed point, matching exact arithmetic to within one step (unit
    tested against a floating-point reference); the matrix comes from the
    container's nclx, else the sequence header. libavif converts through
    libyuv's coarser matrices, so seven new photo_* reference fixtures hold
    our raster to libavif's within the measured 1-5 step gap, and the
    reference suite now honours each fixture's manifest tolerance. It also no
    longer skips a fixture that reports Unsupported: refusals are
    tests/unsupported.rs's job, so here every fixture must decode. A facade
    test opens a 4:2:0 file through Image::open and crops it.
  • otf-pixels-codec-avif reconstructs 4:2:0 and 4:2:2 frames — the chroma
    formats nearly every real AVIF uses. Each chroma plane is decoded in its own
    subsampled grid: a block one unit wide or high shares its chroma with its
    neighbour, the odd one of the pair coding it (HasChroma) with its own
    neighbour availability; residual, transform sizes, coefficient contexts,
    prediction edges, the smooth-filter neighbour lookup, palette maps and
    chroma-from-luma's 2x2 luma averaging all follow the plane's subsampling, as
    do deblocking's edge walk and CDEF/restoration. Verified sample for sample
    against libaom's decoded planes by a new plane-level suite
    (tests/planes.rs, ten fixtures: 4:2:0 with filters off and on, odd sizes,
    4:2:2, palettes, 128x128 superblocks) — before any colour conversion, which
    is still identity-only, so these files decode to planes but not yet to RGB.
  • otf-pixels-codec-avif walks a block wider or taller than 64 in 64x64
    residual chunks, as the spec requires; libaom codes 128x128 blocks in
    128x128 superblocks, which it uses for larger images.
  • otf-pixels-codec-avif decodes super-resolution frames (§7.16). Such a frame
    is coded at a reduced width (8 / SuperresDenom of the display width) and
    reconstructed, deblocked and CDEF-filtered at that width; between CDEF and
    loop restoration both the CDEF output and the pre-CDEF frame are stretched
    back to UpscaledWidth with the normative 8-tap, 64-phase upscale filter, and
    restoration runs on the upscaled pair. read_lr scales a superblock's coded
    columns by SuperresDenom / 8 to find the restoration units it carries. The
    source column clamps to the decoded MiCols * 4 area, as the spec says, and
    libaom agrees on widths that are not a multiple of 8. Lossy frames are now
    refused only for film grain. libavif does not expose aom's superres settings,
    so the regeneration script encodes these fixtures with aomenc and wraps the
    key frame into a minimal AVIF container itself (CI installs aom-tools). Five
    fixtures — denominators 9 to 16, odd widths, and the full deblock + CDEF +
    restoration pipeline across two restoration-unit columns — decode byte-exact
    against libavif.
  • otf-pixels-codec-avif applies loop restoration (§7.17), the last in-loop
    filter, after CDEF. Each restoration unit's type and coefficients are read
    during tile decode (read_lr/read_lr_unit, §5.11.57–58): a Wiener, a
    self-guided or — for a switchable frame — a restoration_type symbol, with
    the coefficients sub-exponentially coded against a running per-plane
    reference. The filter then runs in 64-row stripes: the separable 7-tap
    (luma) / 5-tap (chroma) Wiener filter (§7.17.4), or the self-guided filter
    (§7.17.2–3) blending two box-filter passes through the coded projection
    weights. Samples within a stripe come from the CDEF output, and the rows just
    beyond it from the pre-CDEF frame (§7.17.6). Lossy frames are now refused only
    for super-resolution and film grain. Five restoration fixtures join the
    reference corpus and decode byte-exact against libavif: restoration alone
    (textured_restore, textured_odd_restore, mixed_restore) and the whole
    deblock + CDEF + restoration pipeline (textured_restore_full,
    mixed_restore_full). Between them they cover Wiener and self-guided units, a
    switchable frame type on luma and on chroma, and multi-unit grids.
  • otf-pixels-codec-avif applies the constrained directional enhancement filter
    (CDEF, §7.15) after deblocking: the per-64x64 cdef_idx is read during tile
    decode (read_cdef, right after read_skip), and each 8x8 block is deringed
    by the direction search (§7.15.2) — projecting the block onto eight oriented
    lines and picking the highest-energy one — followed by the directional filter
    (§7.15.3) with its primary/secondary taps and the constrain cap that leaves
    genuine edges intact. The still-image intra subset takes the skip test straight
    from the block Skips, and single-tile makes is_inside_filter_region the
    frame bounds. The lossy gate no longer requires CDEF off. Three cdef fixtures
    (gradient_cdef, blocks_cdef, gradient_odd_cdef), encoded with CDEF on and
    deblocking off to isolate it, join the reference corpus and decode byte-exact
    against libavif.
  • otf-pixels-codec-avif applies the deblocking loop filter (§7.14) after
    reconstruction: both passes (all vertical then all horizontal boundaries), the
    transform-edge and adaptive-strength derivation, and the narrow, 8-/14-tap
    luma and 6-/8-tap chroma wide filters. The still-image intra subset collapses
    applyFilter to the transform-edge test and the strength to the frame
    loop_filter level plus the intra reference delta, with libaom's frame-level
    guard that skips filtering when both luma levels are zero. The gate on lossy
    frames now only requires the unimplemented post-filters off (CDEF, loop
    restoration, super-resolution, film grain); deblocking may be on. Three
    deblock fixtures (gradient_deblock, blocks_deblock, gradient_odd_deblock)
    join the reference corpus and decode byte-exact against libavif.
  • otf-pixels-codec-avif reconstructs a transform block of any size with the
    flipUD/flipLR the transform type calls for. add_residual (§7.12.3 step 3)
    adds the pre-flip Residual to the prediction at the flipped output position
    and clips; add_residual_4x4 is now a thin DCT_DCT wrapper the lossless tile
    drives. This completes the reconstruct primitive for the FLIPADST/V_FLIPADST
    /H_FLIPADST transform types ahead of wiring real sizes into the tile.
  • otf-pixels-codec-avif generalizes the recursive filter-intra predictor
    (§7.11.2.3) to any transform size: predict_filter_intra walks the 4x2
    sub-blocks over a w x h block, and predict_filter_intra_4x4 is now a thin
    wrapper. With this every intra predictor — DC, Paeth, Smooth, directional and
    filter-intra — runs at any size; the lossless 4x4 tile drives them all and
    stays bit-exact against libavif.
  • otf-pixels-codec-avif generalizes the slanted directional intra predictor to
    any transform size. predict_directional runs the §7.11.2.4 projection — edge
    filter, upsample, and the per-angle interpolation — over a w x h block, and
    predict_directional_4x4 is now a thin wrapper over it. The Edge scratch
    buffer grew to span a 64x64 transform's reach. With the non-directional
    predictors already generalized, every intra mode now predicts at any size; the
    lossless 4x4 tile drives the general paths and stays bit-exact against libavif.
  • otf-pixels-codec-avif generalizes the non-edge-filtered intra predictors to
    any transform size. predict_intra_block runs DC (§7.11.2.5), Paeth
    (§7.11.2.2), the three Smooth variants (§7.11.2.6, with the full
    Sm_Weights_Tx_4x4..64x64 tables selected per side length), and the
    axis-aligned V/H copies over a w x h block, and predict_intra_4x4 is now a
    thin wrapper over it. The lossless 4x4 tile drives the general path and stays
    bit-exact against libavif on every fixture. The slanted directional modes
    still need the edge-filter machinery and report unsupported.
  • otf-pixels-codec-avif gains the intra transform-size decode: read_tx_size
    (§5.11.15) resolves a coding block's luma transform size — lossless forces
    TX_4X4, otherwise the size starts at Max_Tx_Size_Rect and, under
    TX_MODE_SELECT, a tx_depth symbol splits it down through Split_Tx_Size.
    The tx_depth context (§8.3.2) counts the above/left neighbour transforms
    that are at least as large as the block's maximum, and the four
    Default_Tx_*_Cdf tables select the alphabet by maximum depth. Exposed as a
    tested unit on the crate's AV1 surface; the tile still drives a TX_4X4-only
    reconstruct, so the lossless fixtures are unaffected.
  • otf-pixels-codec-avif reads the transform type inside the coefficient decode
    at its spec position. decode_coeffs now resolves each coded block's
    PlaneTxType after all_zero and before eob_pt (§5.11.39): luma reads the
    intra_tx_type symbol against its size-and-direction contexts, chroma derives
    its type from the prediction mode, and the resolved type rides out on the
    block to drive the inverse transform. The tile driver threads in the frame's
    transform set and quantiser state. Lossless is qindex 0, so no symbol is read
    and every block stays DCT_DCT — the fixtures remain bit-exact against
    libavif — while the machinery is now in place for the lossy path.
  • otf-pixels-codec-avif gains the intra transform-type decode: get_tx_set
    (which of DCT_DCT-only, the seven-type set 1, or the five-type set 2 a
    transform of a given size uses), the intra_tx_type symbol read with its
    size-and-direction CDFs mapped back through the inversion tables, and the
    chroma derivation (Mode_To_Txfm gated by set membership, compute_tx_type).
    A large or lossless transform stays DCT_DCT without consuming a symbol, so
    the lossless path is unaffected. Exposed as a tested unit on the crate's AV1
    surface ahead of wiring it into the tile coefficient path.
  • otf-pixels-codec-avif generalizes the coefficient decoder to every transform
    size and type, not just the 4x4 lossless corner. decode_coeffs now selects
    the eob_pt alphabet by size, walks the size-and-type-specific scan order
    (get_scan over the generated Default/Mrow/Mcol scan tables), and
    derives the coeff_base/coeff_br contexts from the coded block's bwl and
    transform class (2D vs. the 1D row/column identities). The scan tables are
    generated from a vendored spec extract by scripts/generate-av1-scan-tables.py
    and validated to be exact permutations, with CI re-running the generator and
    diffing — the same discipline as the CDF tables. The lossless path is the
    TX_4X4/DCT_DCT case of the general decoder and stays bit-exact against
    libavif on every fixture.
  • otf-pixels-codec-avif gains the full lossy inverse-transform machinery, the
    foundation of the lossy decode path. inverse_transform_2d runs the §7.13
    butterfly network — the inverse DCT (4..64), ADST (4/8/16) and identity
    transforms, with the flip and rectangular-scaling cases — over any of the 19
    transform sizes, and dequantize applies the real Dc_Qlookup/Ac_Qlookup
    quantiser steps (generated from the spec). The lossless reconstruct now flows
    through the same pair — qindex 0 is just its dc == ac == 4, dqDenom == 1
    corner where the Walsh–Hadamard transform divides the dequantiser back out —
    so every lossless fixture stays bit-exact against libavif while the lossy
    transforms come online.
  • otf-pixels-codec-avif decodes the screen-content lossless path: palette and
    chroma-from-luma prediction. Palette blocks decode their colour set (merging
    the neighbour palettes into the prediction cache, then cache hits, a base
    colour, and clipped deltas) and the per-sample colour-index map (the wavefront
    scan with its colour-order context model), and predict each sample as the
    colour its index selects. Chroma-from-luma reads its signed alphas and adds the
    scaled, DC-removed reconstructed luma to the DC chroma prediction. With these,
    every lossless AVIF fixture — including the block/screen-content one — decodes
    bit-exact against libavif; only intra block copy remains unimplemented in the
    lossless path.
  • otf-pixels-codec-avif decodes real pixels end to end: AvifDecoder::read_row
    reconstructs a lossless 4:4:4 AVIF still, converts the identity-matrix planes
    to RGB, and serves the rows — bit-exact against libavif on every lossless
    fixture (tests/reference.rs). The tile driver walks the partition tree
    (including the T-shaped and 4-way splits), reads each block's intra mode, and
    for every 4x4 transform block predicts, decodes coefficients, inverts the
    Walsh–Hadamard transform, and writes the samples back — threading the
    neighbour level, DC-sign, mode, and block-decoded context arrays that keep the
    entropy contexts aligned with the encoder. All the intra modes a natural image
    reaches are implemented: DC, Paeth, the smooth family, the slanted directional
    modes with their edge-filter and upsample machinery, and recursive
    filter-intra. Intra block copy, subsampled chroma, lossy transforms, and grids
    report Unsupported rather than decode wrong.
  • otf-pixels-codec-avif gains the 4x4 coefficient decoder — the syntax that
    turns a transform block's entropy-coded symbols into a signed level array.
    It reads all_zero, the end-of-block position, each coefficient's base level
    walking the scan backwards, the coeff_br and Golomb magnitude extension for
    large levels, and the signs walking forwards, each against its
    context-selected CDF. The context derivations (get_coeff_base_ctx, the
    coeff_br and end-of-block buckets) are transcribed from the spec and unit
    tested against its worked contexts. Scoped to TX_4X4, which is every block
    the lossless path codes; the wider transform sizes arrive with the lossy path.
  • otf-pixels-codec-avif gains 4x4 intra prediction for the non-directional
    modes. DC, Paeth, and the three Smooth variants are complete, as are the two
    axis-aligned directional modes (V and H, which at exactly 90 and 180 degrees
    are plain copies with no edge filtering). The predictors are pure functions
    over the assembled AboveRow/LeftCol neighbour arrays, so they are tested
    directly against the spec's formulas; building those arrays from the plane is
    the tile driver's job. The slanted directional modes, which need the
    edge-filter and upsample machinery, report Unsupported until that lands.
  • otf-pixels-codec-avif gains the first reconstruction primitives: a bounds-safe
    sample Plane (edge-replicating clamped reads for intra prediction, the
    workspace's slice-indexing ban concentrated in one file) and the lossless 4x4
    inverse transform. The transform is the Walsh–Hadamard pair the lossless path
    uses: dequantize each level by the qindex-0 step of 4, run the row WHT whose
    shift = 2 divides that step straight back out, then the column WHT — which
    is why a lossless decode is bit-exact. The 4x4 blocks are handled by array
    destructuring rather than indexing, keeping the hot path within the lint.
  • otf-pixels-codec-avif gains its reference-fixture harness ahead of pixel
    reconstruction, because our own decoder cannot validate itself: a shared
    misreading of the spec round-trips perfectly and is still wrong.
    scripts/regenerate-avif-reference.py encodes fixtures with libavif's
    avifenc and takes the expected rasters from libavif's avifdec — an
    independent AV1 implementation. The first fixtures are lossless, the strictest
    target there is: lossless AVIF is CodedLossless, so every post-filter is off
    and only the 4x4 Walsh-Hadamard transform is used, and the raster must equal
    the source exactly. CI installs libavif, re-runs the generator and diffs the
    manifest, and the lossy fixtures with their tolerances join as the DCT/ADST
    paths and post-filters land.
  • otf-pixels-codec-avif gains the AV1 default CDF tables, generated rather than
    hand-transcribed. scripts/generate-av1-cdf-tables.py emits av1/cdf.rs from
    a vendored extract of the specification's section-10 default-CDF blocks
    (scripts/data/av1-default-cdf-tables.txt), and CI re-runs the generator and
    git diff --exit-codes the result, exactly as it does the REFERENCE
    manifests — so a stale table or a hand edit is caught. All 96 tables land in
    the exact shape SymbolDecoder::read_symbol consumes: cumulative frequencies
    ending in 1 << 15, then a 0 adaptation counter.
  • otf-pixels-codec-avif gains the AV1 multi-symbol arithmetic decoder — the
    entropy engine every tile symbol passes through. It is a range decoder over
    15-bit inverse CDFs with a per-symbol floor probability, plus the CDF
    adaptation that lets the distributions track the stream. The transcription
    follows the AV1 specification's symbol-decoding process verbatim and was
    cross-checked against libaom's entdec.c; the two agree on the inverse-CDF
    arithmetic and the renormalization. Reads are fallible and the SymbolMaxBits
    accounting means the implicit zero-padding past the real bytes is entered
    deliberately rather than by reading off the end. Numeric conformance against
    real streams is deferred to the libaom differential harness, the only
    trustworthy check for an entropy coder, which lands with reconstruction.
  • otf-pixels-codec-avif gains the AV1 front end: the bitstream layers that
    read a still picture's headers down to — and stopping at — the tile-group
    data. A most-significant-bit-first BitReader with the AV1 primitives
    (f, uvlc, leb128, le, su, ns, byte alignment), the OBU framing
    layer, the full sequence header (color_config and all), and the frame
    (uncompressed) header restricted to the intra key-frame path — size,
    quantizer, segmentation, loop-filter/CDEF/restoration parameters, transform
    mode, tile layout and film-grain parameters, each sub-block walked in order
    so CodedLossless is known before the filter parameters that depend on it.
    A StillPicture driver ties configuration OBUs and the coded frame together
    the way an AVIF still is laid out and returns both parsed headers plus a
    locator for the tile data. Every primitive is fallible: a bitstream that ends
    mid-value, an OBU size past the buffer, or a non-zero alignment pad is a
    returned error, never a panic. The front end is exposed as public API and
    fully tested against hand-assembled streams; it is wired into pixel
    reconstruction in the phase that follows. Still no pixels: read_row still
    reports Unsupported.
  • otf-pixels-codec-avif: the first slice of an AVIF codec owned from scratch,
    container and (eventually) AV1 bitstream both — ADR-0013, reversing ADR-0004's
    decision to wrap the dav1d/rav1e family, because the wrapped route pulled a
    non-Rust build dependency the rest of the workspace does not have. This
    release lands the ISOBMFF/HEIF container: the box reader (64-bit sizes and
    parent-bound checks that reject the classic infinite-loop and
    run-past-the-end malformations), the item model (meta/pitm/iinf/iloc/
    iref/idat), and item properties (iprp/ipco/ipma with
    ispe/av1C/pixi/colr/irot/imir/auxC). AvifDecoder reports
    correct metadata() — dimensions, pixel format, alpha, grid — for any AVIF,
    and AvifCodec/probe sniff the file by its ftyp brand. Wired into the
    facade behind an avif feature, on by default. Pixel decode currently reports
    Unsupported: the AV1 bitstream reconstruction lands in the phases that
    follow. Still images only; image sequences (the avis brand) are out of scope
    for v1.
  • otf-pixels-codec-webp: WebP decode (lossy and lossless, alpha detected
    from the container) and lossless encode, wrapping image-webp per ADR-0004.
    Wired into the facade behind a webp feature, on by default. Pure Rust; its
    only transitive dependencies are byteorder-lite and quick-error.
  • WebP encoding is lossless only, because the wrapped encoder has no
    quality control at all — EncodeOptions::quality is accepted and ignored.
    For a photograph that means a much larger file than the format's headline
    feature would give, and it is recorded here rather than left to be
    discovered from a file size. It is the strongest argument for revisiting
    WebP ownership.
  • Greyscale has no native WebP mode, so one channel in comes back as three.
    Asserted rather than glossed, since it differs from PNG and JPEG.
  • libwebp reads all 24 emitted WebP files back to the exact pixels put in
    (scripts/check-webp-interop.sh). Exact, not tolerant: the encoder is
    lossless, so there is nowhere for a bug to hide.
  • Progressive JPEG decode, wrapped rather than owned (ADR-0004), behind the
    jpeg-progressive feature of otf-pixels — on by default. jpeg-decoder
    with default features off is the only external dependency in the default
    build and brings no transitive ones; default-features = false drops it and
    progressive files then report Unsupported naming the feature.
  • The handover is the part worth testing, and is tested: our header parser
    consumes bytes from a forward-only stream before it learns the frame is
    progressive, so the reader records what it has read and replays it to the
    wrapped decoder. A short, long or misordered replay would produce a wrong
    picture or a rejected file, so both progressive fixtures are compared
    against libjpeg to within 4 per sample. The tape is dropped the moment a
    baseline frame is confirmed, so an ordinary decode buffers nothing.
  • JpegDecoder::is_progressive reports which decoder ran. Progressive frames
    are internally buffered (SPEC §Formats) and offer no reduced-scale decode,
    so shrink-on-load correctly declines them rather than promising a fast path
    the wrapped decoder cannot provide.
  • Shrink-on-load: shrink_on_load rewrites a graph over a reduced source when
    the whole pipeline permits it, so a JPEG thumbnail decodes at 1/8 rather
    than at full size and then discards the pixels. The decision needs the
    complete graph — from_stream runs before .resize(200, 150) is ever
    called — so it is a rewrite pass rather than a decoder option, and it runs
    above the choice of evaluator so the scheduler and the reference evaluator
    keep agreeing.
  • Op::rescaled replaces the pair of decisions an op would otherwise have to
    get right separately: whether it means the same thing against a smaller
    input, and whether the instance it hands back carries state bound to the
    old one. resize memoizes filter tables keyed to the shape it first saw, so
    reusing the instance was a resample against the wrong scale waiting to
    happen. crop, convolve and composite decline outright — coordinates
    and kernel sizes are in input pixels.
  • Output::write_with_stats returns RunStats, including the reduction
    shrink-on-load applied, so a pipeline that expected the fast path and did
    not get it is diagnosable rather than merely slow.
  • otf-pixels-codec-jpeg: JpegDecoder, baseline JPEG from scratch — Huffman
    entropy decode, a fixed-point IDCT, every chroma subsampling, restart
    intervals, greyscale and RGB-labelled files, and EXIF orientation read (but
    not applied: auto_orient is a pipeline decision, and a decoder that
    rotated its own output would leave no way to turn it off). Decode is
    streaming at one MCU row, so peak memory is a band and not the image.
    Progressive, arithmetic-coded, 12-bit and CMYK files are reported
    Unsupported rather than Malformed: they are valid JPEGs this codec does
    not own, and a host binding routes on that difference.
  • otf-pixels-codec-jpeg fixtures are compared against libjpeg-turbo with a
    tolerance rather than a hash, because JPEG defines the IDCT only to an
    accuracy bound and a hash would fail a conforming decoder. Chroma is
    upsampled nearest-neighbour where libjpeg interpolates, so subsampled
    fixtures are compared across their flat interiors, where the two filters
    must agree, and left alone at chroma edges, where they legitimately differ.
  • otf-pixels-codec-jpeg: JpegEncoder, writing baseline JPEG with a
    fixed-point forward DCT, the Annex K quantization and Huffman tables scaled
    by the IJG quality mapping, and 4:4:4/4:2:2/4:2:0 chroma subsampling
    (4:4:4 from quality 90 up, where subsampling rather than quantization would
    otherwise become the dominant loss). Encoding streams at one MCU row, so
    bytes reach the sink before the last row arrives. Alpha is composited
    against black rather than dropped, as the GIF encoder already does.
    Optimal Huffman tables are deliberately not derived: that needs a counting
    pass over every coefficient before the first byte can be written, which
    trades ADR-0005's streaming contract for a few percent.
  • libjpeg reads all 140 emitted JPEGs — seven sizes, three subsamplings, five
    qualities, greyscale and RGB — and decodes them to within 1.6x of the loss
    libjpeg's own encoder produces on the same input
    (scripts/check-jpeg-interop.sh). The comparison is against the reference
    encoder rather than a fixed tolerance because a fixed one is wrong at both
    ends: a steep gradient in a 7x3 image loses far more to 4:2:0 than the same
    gradient across 64x48 does.
  • otf-pixels-codec-jpeg: JpegDecoder::with_scale, decoding at 1/8, 1/4 or
    1/2 resolution straight from the coefficients (SPEC §Core ops, "JPEG fast
    path"). Scale::fitting picks the coarsest scale still at least the target
    size, never below it. The win is downstream: at 1/8 every later op sees one
    sixty-fourth of the pixels, and the full-resolution image is never
    materialized.
  • The reduced transform is defined as the exact box average of the full
    one — each basis entry sums the cosines of the samples that output replaces,
    so all sixty-four coefficients contribute. The first implementation instead
    inverse-transformed the top-left MxM corner, which is the obvious reading
    and is measurably wrong: against libjpeg's scaled decode on a noise fixture
    at 1/2, truncation was ten times further from the true downsample (15.4 mean
    error against 0.57). With the box basis we match libjpeg to three decimal
    places on every fixture that has no chroma subsampling.
  • otf-pixels: JPEG wired into sniffing, decode and encode behind a jpeg
    feature, on by default. Image::from_stream recognises a JPEG by its magic
    bytes and output(Format::Jpeg, options) writes one, so a decode/resize/
    re-encode pipeline works end to end.
  • A jpeg_decode fuzz target and an in-tree mutation harness, both asserting
    only that no input panics, plus a jpeg_roundtrip target asserting that
    every stream this encoder produces is one this decoder accepts at the
    declared shape.
  • M5 exit-criterion tests and benches/thumbnail.rs, the giant-tiled-TIFF
    thumbnail benchmark against libvips. It skips cleanly when vips is not
    installed rather than omitting the row or inventing a number.
  • ADR-0012 (extract otf-pixels-compress), executing the "move on a third
    consumer" clause ADR-0010 wrote for exactly this moment.
  • otf-pixels-codec-tiff: TiffDecoder covering baseline TIFF 6.0 — both
    byte orders, strip and tile layouts, none/LZW/Deflate/PackBits compression,
    greyscale at 1/8/16 bits, RGB, and palette. Exotic tags are skipped, not
    errors. A tiled file reports DecodeCapability::Regions, so producing a
    region decompresses only the tiles it touches; a strip file reports
    Sequential, because claiming otherwise would be a lie the scheduler acts on.
  • otf-pixels-codec-tiff: TiffEncoder, writing baseline TIFF in strips or
    tiles, uncompressed or Deflate. libtiff reads all 240 emitted files — six
    sizes, five formats, four layouts, two compressions — back to the pixels we
    put in (scripts/check-tiff-interop.sh). Tiled output is what lets a
    pipeline store an intermediate it will re-read with random access.
  • otf-pixels-core: DecodedSource now asks a region-capable decoder for
    regions directly instead of driving it row by row. Without that a tiled TIFF
    was declared random-access and then read sequentially anyway.
  • otf-pixels: GIF and TIFF wired into sniffing, decode and (for GIF) encode,
    behind gif and tiff features.
  • otf-pixels-codec-gif: GifDecoder covering the whole format — all frames,
    both interlace layouts, transparency and every disposal method — and
    GifEncoder, single-frame with median-cut quantization and Floyd-Steinberg
    dithering, which is SPEC §Formats' stated v1 scope. Decoder yields the
    first frame so ordinary pipelines work unchanged; next_frame walks the
    rest, keeping "animation pipelines are v2" honest.
  • otf-pixels-compress: inflate, deflate, zlib, Crc32 and Adler32 moved
    from otf-pixels-codec-png (which re-exports them, so nothing downstream
    changes), plus LZW in both the GIF and TIFF dialects — one implementation
    parameterized by bit order, since the two specifications differ in framing
    and not in substance.
  • ADR-0011 (SIMD by autovectorization; fixed-point arithmetic at 8 bits).
  • otf-pixels: resize, thumbnail, rotate, modulate, convolve,
    blur, sharpen, extract_channel, flatten and composite on the
    chainable facade. composite joins two lazy branches of a graph.
  • benches/ops.rs: the M4 comparative benchmark against image and
    fast_image_resize, both dev-dependencies only. Numbers published in the
    README, including the part that does not flatter us.
  • M4 exit-criterion tests: every pipeline is byte-identical run to run, across
    thread counts, across tile shapes, and against the M1 oracle.
  • otf-pixels-ops: the rest of the v1 op set (SPEC §Core ops) — Rotate
    (quarter turns), Modulate (brightness/saturation/hue), Convolve with
    blur, Gaussian and sharpen presets, Composite (Porter-Duff source-over,
    the first two-input op), ExtractChannel and Flatten. Every op with a
    non-trivial demand mapping asserts that its output does not depend on how
    the image is tiled.
  • otf-pixels-ops: Resize, separable resampling with seven filters
    (nearest, box, bilinear, Catmull-Rom, Mitchell, Lanczos2, Lanczos3), Fit
    modes and without_enlargement. Weight tables are built per image, not
    per tile, so the output does not depend on how the image is cut up — which
    is asserted directly rather than assumed.
  • ADR-0008 (crossbeam-deque for work stealing), ADR-0009 (scheduler-inserted
    materialization at order-incompatible seams) and ADR-0010 (own inflate and
    deflate).
  • otf-pixels-codec-png: Crc32, Adler32, and a from-scratch DEFLATE
    decompressor (inflate_to, zlib_decompress) per ADR-0010. Bounded output
    makes decompression bombs a malformed-input error rather than an allocation.
  • otf-pixels-codec-png: a from-scratch DEFLATE compressor (deflate,
    zlib_compress, Level) with levels 0-9. Verified in both directions
    against reference zlib — we decode its streams, and it decodes ours
    (scripts/check-deflate-interop.sh).
  • otf-pixels-codec-png: PngDecoder, covering every v1 PNG feature — bit
    depths 1/2/4/8/16, colour types 0/2/3/4/6, PLTE, tRNS, Adam7 interlace
    and all five filter types. Ancillary chunks are skipped, not honoured.
    Non-interlaced images stream: peak memory is two scanlines, the 32 KiB
    inflate window and a read buffer, none of which grow with image height.
    Interlaced images buffer, exactly as SPEC §Formats already said.
  • otf-pixels-codec-png: Inflater and ZlibStream, the incremental
    decompressors the streaming decoder is built on, plus ChunkStream, which
    walks PNG chunks from a forward-only source a piece at a time.
  • CI (.github/workflows/ci.yml): test, fmt, clippy, docs, feature
    combinations, MSRV, reference interop and fuzzing. The interop job also
    regenerates the PngSuite manifest and fails on a diff, so a stale reference
    cannot quietly start agreeing with a broken decoder.
  • fuzz/: cargo fuzz targets for PNG decode, inflate and encode/decode
    round-trip, plus tests/fuzz.rs, an in-tree deterministic mutation harness
    that runs the same no-panic property on stable in seconds.
  • M3 exit-criterion tests: PNG round-trips through the engine for every
    format it can represent, ops compose over a decoded PNG exactly as over raw
    pixels, the scheduler agrees with the M1 oracle over a PNG source, and
    decoding a tall PNG reads a fraction of the file before the first row.
  • otf-pixels: Image::open and Image::from_stream, which identify a format
    from its magic bytes and never from a file extension (SPEC §Formats).
    Format::Png now resolves to a real encoder behind the png feature.
  • otf-pixels-core: Prefixed, a source that replays a buffered prefix before
    delegating. It is what lets sniffing look at the magic bytes and still hand
    the whole stream onward, without asking any source to seek (ADR-0005).
  • otf-pixels-codec-png: PngCodec, the sniffing registry entry for PNG.
  • otf-pixels-codec-png: PngEncoder, writing non-interlaced PNG at DEFLATE
    levels 0-9 with per-row adaptive filter selection. Verified in the encode
    direction too: libpng reads all 140 emitted files — seven pixel formats,
    five sizes, four levels — back to the pixels we put in
    (scripts/check-png-interop.sh). EncodeOptions::quality is read as
    compression effort, since PNG is lossless and has no fidelity to trade.
  • otf-pixels-codec-png: the PngSuite conformance corpus (100 files, ~62 KB,
    vendored with its licence) checked against reference decodings from libpng
    rather than against ourselves. All 14 corruption modes are rejected, and no
    truncation or single-byte mutation of any fixture panics.
  • otf-pixels-core: TileCache, a byte-budgeted LRU of graph intermediates
    keyed by (NodeId, Region). Eviction bounds what the cache retains, never
    what a caller holds alive, so tiles need no pinning.
  • otf-pixels-core: ThreadPool, a work-stealing pool over crossbeam-deque
    (ADR-0008). Panicking tasks are contained and reported as errors; a batch
    reports its lowest-indexed failure so errors stay deterministic.
  • otf-pixels-core: Plan, the pre-execution graph analysis. Negotiates tile
    shapes per segment (ADR-0003) and marks materialization points where
    non-forward demand meets a forward-only source (ADR-0009). Pure analysis —
    it reads no pixels.
  • Producer::capability, the upstream half of ADR-0009's seam analysis.
    BufferSource reports Regions; DecodedSource delegates to its decoder.
  • otf-pixels-core: Scheduler, the demand-driven parallel tile evaluator,
    plus evaluate_tiled and RunStats. Output tiles are evaluated in parallel
    batches and delivered to the sink in order.
  • NodePlan::cacheable: only nodes demanded more than once (a shared prefix,
    or one feeding a spatial op) are retained in the tile cache.
  • Output::bytes_via_reference, running a pipeline through the M1 evaluator so
    the scheduler can be differentially tested against it.
  • Output::threads / Output::scheduler_options for tuning a run.
  • Builder setters on Limits, PlanOptions and SchedulerOptions, which are
    #[non_exhaustive] and were otherwise unconfigurable downstream.
  • M2 exit-criterion tests and benches/scaling.rs, a std-only benchmark
    reporting speedup and parallel efficiency across thread counts.
  • Project documentation: README, ARCHITECTURE, SPEC, ROADMAP, ADR-0001..0007.
  • Cargo workspace: otf-pixels, otf-pixels-core, otf-pixels-ops,
    otf-pixels-codec-raw (ADR-0006). No external dependencies.
  • otf-pixels-core: PixelsError with stable ErrorCodes, PixelFormat and
    the Sample dispatch seam (ADR-0002), Region/ImageDescriptor/Limits
    with max_pixels enforced before allocation, strided Tile/TileMut/
    TileBuf views, streaming Source/Sink traits (ADR-0005), Decoder/
    Encoder/Codec traits, the Op trait and lazy Image op graph, and the
    naive whole-image evaluator that M2 will be diffed against.
  • otf-pixels-codec-raw: RawDecoder/RawEncoder with caller-supplied
    dimensions, pixel format and stride, streaming a row at a time in both
    directions. Truncated streams are malformed-input errors, never panics.
  • otf-pixels-ops: Crop, Flip and Flop geometry ops, each declaring its
    demand mapping and access pattern for M2's scheduler.
  • otf-pixels: the chainable facade — Image::from_raw, from_raw_stream,
    crop/flip/flop, and the output(format, options) terminal with
    write(sink) and bytes(). Errors raised mid-chain are captured and
    surfaced at the terminal, so pipelines read as one expression.
  • M1 exit-criterion test suite: raw → crop/flip → raw round-trips, graph
    laziness (zero source bytes read before a terminal), malformed-input and
    limit handling, determinism, and concurrent evaluation.