Repository navigation
otf-pixels-codec-jpeg@0.1.0
·
41 commits
to main
since this release
Fixed
- Clippy 1.99, which CI runs, rejected two new lints (
byte_char_slicesin
the JPEG decoder, manual slice filling in the AV1 tile decoder). Both are
rewritten; behaviour is unchanged. - The JPEG codec built without its
progressivefeature warned about three
unused stream-replay methods, which CI's-D warningsturned into a
failure of every feature combination but the defaults. They are now
compiled only with the feature. - The crates'
repositorylink pointed atgithub.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 withUnsupported, 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_HEADERafter the tile data lost that tile data. A
redundant copy now only stands in for a header that never arrived. - AVIF
imirwas 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
tRNSreported an opaque pixel format until its first row was
read. The chunks beforeIDATare 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::Regionsno 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-versionis a
promise about consuming the library; the benchmark'sfast_image_resize
needs 1.87 and no user ofotf-pixelsever compiles it. - The declared MSRV of 1.85 was not actually met: three
letchains need
1.88. Rewritten as nestedif let, and CI now checks 1.85 so the promise
stays true rather than aspirational. - A build without the
rawfeature 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-fuzzinstead 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 interopstays in CI. tsr test(and CI's test job) runscargo testwithout the benchmark
targets, which--all-targetsused 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
tsrtask runner, as ES-Runtime does. Every command
CI runs is a task intasks.toml, sotsr cilocally is what a pull request
must pass, andtsr --listshows the rest (msrv,interop,
references,bench,fuzz).RUSTFLAGS=-D warningsmoves from the
workflow into the tasks, so it applies locally too. Theinteroptask also
runscheck-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
LICENSEand theNOTICE,
as the licence requires of redistributions. - Version 0.1.0, the first release candidate. All crates package and verify
withcargo package --workspace. Fit,Blend,Filter,ConversionandUnconvertibleare
#[non_exhaustive], so later modes can be added without breaking callers.- WebP output is now lossy by default, at
EncodeOptions::quality
(default 80), as withcwebpand sharp; setEncodeOptions::losslessfor
the lossless output that was previously the only kind. otf-pixels-codec-avif:StillPicturerecords every tile group
(tile_groups,tile_group_data) in place of onetile_data_offset/
tile_data_lenrange, anddecode_stilltakes the tile groups' bytes.PngDecoder::newnow reads through to the firstIDATheader rather than
stopping afterIHDR, so thateXIfandtRNSare known at open. It still
reads no image data.JpegDecoder::orientationis now theDecodertrait method and returns
Orientationinstead of a rawOption<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-pixelsnow evaluates pipelines with the tile scheduler instead of the
M1 whole-image evaluator. Rows still reach an encoder top to bottom.DecodedSourcenow 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.Flipnow declaresAccessPattern::Sequential, notSpatial.
AccessPatterndescribes tile shape; a vertical mirror reads one input
row per output row and wants full-width strips. Its row reversal is tile
order, whichinput_regionsalready 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 asUnsupported; 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_lfand monochrome — as
Unsupportedbefore decoding, keeping the promise that an unimplemented tool
fails cleanly rather than decoding wrong. A newtests/unsupported.rsholds
one real avifenc encode per refused tool and asserts each is refused for its
own tool; verified by removing the check, which letdelta_qandqmatrix
"decode". - The AVIF decoder read
angle_deltaandpalette_mode_infoonly for blocks at
least 8 samples in both dimensions. The spec's gate isMiSize >= BLOCK_8X8in 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_deltasymbol 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, andtextured_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 readuv_modeagainst 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_allowednow 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, andgradient_odd_lossless(37x29)
decodes bit-exact. - The AV1 CDF table generator dropped the multiplication in spec entries of the
form{ 128 * 125, 32768, 0 }, splitting128 * 125into two separate values
and lengthening every affected binary CDF by one bogus entry. The tokenizer
now evaluates the product. This corrupteddc_signand 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-avifgeneralizes 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 realDc_Qlookup/Ac_Qlookupsteps 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 aSchedulershared 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.Scheduleris 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_formatand theConvertFormatop: depth (8-bit,
16-bit, float) and layout (grey, grey with alpha, RGB, RGBA) conversion.
PixelFormat::from_partsnames 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) andFit::Outside
joinFillandInside. 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(andDecoder::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 asUnsupportedinstead 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,curvtables, everyparacurve) to sRGB and drops the
profile.OpenOptions::with_to_srgb(false)keeps pixels as stored, and
Image::to_srgbconverts 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.ToSrgbis also usable as an op. - ICC profiles are preserved. Every decoder that can find one reports it
(Decoder::icc_profile): PNGiCCP, JPEGAPP2(joined across
segments, baseline and progressive), WebPICCP, 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 (apremreference), 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::Avifoutput throughAvifEncoder, 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.shchecks that libaom, dav1d and
libgav1 (throughavifdec) decode every emitted file to our pixels. On a
768x512 photo the files are about 15-20% larger thanavifenc -s 6at
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::Avifencoder 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-webpis now owned outright andimage-webpis 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 theALPHchunk 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 anALPHchunk, 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(andwith_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 usesimage-webpat 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 throughimage-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-webpfor now.- AVIF segmentation, which rav1e (the encoder behind
ravifand the Rust
imagecrate) uses to give parts of the frame their own quantizer. Those
files were refused asUnsupported. Each block'ssegment_idis 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'senable-qm
turns on. Those files were refused asUnsupported. 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_Matrixtable 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_matrixandquantizer_matrix.- AVIF multi-tile decode. Encoders split large images into tiles (libavif's
--autotiling,--tilecolslog2/--tilerowslog2), and those files were
refused asUnsupported. Each tile now decodes from fresh CDFs and cleared
above contexts, restartsCurrentQIndex,DeltaLFand 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 isMalformed. 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 asUnsupportednow decode.CurrentQIndex
drives dequantisation block by block, and each block'sDeltaLFmoves its
deblocking strength, singly or per filter level withdelta_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
andImage::from_streamturn an image upright as its metadata declares
before any op runs: EXIFOrientationin JPEG, TIFF, PNG (eXIf) and WebP,
andirot/imirin AVIF. Previously every phone photograph came out
sideways.OpenOptions::with_auto_orient(false)withImage::open_with/
Image::from_stream_withleaves pixels as stored, andImage::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'sexif_transposefor all eight EXIF values in every format
and against libavif's--irot/--imirfor 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 toGray8/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 (aprem
reference, colour item to alpha item) is refused asUnsupported, 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
Rgb16over 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" contradictedRgb16'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'snclx, else the sequence header. libavif converts through
libyuv's coarser matrices, so seven newphoto_*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 reportsUnsupported: refusals are
tests/unsupported.rs's job, so here every fixture must decode. A facade
test opens a 4:2:0 file throughImage::openand crops it. otf-pixels-codec-avifreconstructs 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-avifwalks 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-avifdecodes super-resolution frames (§7.16). Such a frame
is coded at a reduced width (8 / SuperresDenomof 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 toUpscaledWidthwith the normative 8-tap, 64-phase upscale filter, and
restoration runs on the upscaled pair.read_lrscales a superblock's coded
columns bySuperresDenom / 8to find the restoration units it carries. The
source column clamps to the decodedMiCols * 4area, 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 withaomencand wraps the
key frame into a minimal AVIF container itself (CI installsaom-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-avifapplies 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 — arestoration_typesymbol, 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-avifapplies the constrained directional enhancement filter
(CDEF, §7.15) after deblocking: the per-64x64cdef_idxis read during tile
decode (read_cdef, right afterread_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 theconstraincap that leaves
genuine edges intact. The still-image intra subset takes the skip test straight
from the blockSkips, and single-tile makesis_inside_filter_regionthe
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-avifapplies 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
applyFilterto the transform-edge test and the strength to the frame
loop_filterlevel 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-avifreconstructs a transform block of any size with the
flipUD/flipLRthe transform type calls for.add_residual(§7.12.3 step 3)
adds the pre-flipResidualto the prediction at the flipped output position
and clips;add_residual_4x4is now a thinDCT_DCTwrapper the lossless tile
drives. This completes the reconstruct primitive for theFLIPADST/V_FLIPADST
/H_FLIPADSTtransform types ahead of wiring real sizes into the tile.otf-pixels-codec-avifgeneralizes the recursive filter-intra predictor
(§7.11.2.3) to any transform size:predict_filter_intrawalks the 4x2
sub-blocks over aw x hblock, andpredict_filter_intra_4x4is 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-avifgeneralizes the slanted directional intra predictor to
any transform size.predict_directionalruns the §7.11.2.4 projection — edge
filter, upsample, and the per-angle interpolation — over aw x hblock, and
predict_directional_4x4is now a thin wrapper over it. TheEdgescratch
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-avifgeneralizes the non-edge-filtered intra predictors to
any transform size.predict_intra_blockruns 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..64x64tables selected per side length), and the
axis-aligned V/H copies over aw x hblock, andpredict_intra_4x4is 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 reportunsupported.otf-pixels-codec-avifgains 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 atMax_Tx_Size_Rectand, under
TX_MODE_SELECT, atx_depthsymbol splits it down throughSplit_Tx_Size.
Thetx_depthcontext (§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_*_Cdftables select the alphabet by maximum depth. Exposed as a
tested unit on the crate's AV1 surface; the tile still drives aTX_4X4-only
reconstruct, so the lossless fixtures are unaffected.otf-pixels-codec-avifreads the transform type inside the coefficient decode
at its spec position.decode_coeffsnow resolves each coded block's
PlaneTxTypeafterall_zeroand beforeeob_pt(§5.11.39): luma reads the
intra_tx_typesymbol 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 staysDCT_DCT— the fixtures remain bit-exact against
libavif — while the machinery is now in place for the lossy path.otf-pixels-codec-avifgains the intra transform-type decode:get_tx_set
(which ofDCT_DCT-only, the seven-type set 1, or the five-type set 2 a
transform of a given size uses), theintra_tx_typesymbol read with its
size-and-direction CDFs mapped back through the inversion tables, and the
chroma derivation (Mode_To_Txfmgated by set membership,compute_tx_type).
A large or lossless transform staysDCT_DCTwithout 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-avifgeneralizes the coefficient decoder to every transform
size and type, not just the 4x4 lossless corner.decode_coeffsnow selects
theeob_ptalphabet by size, walks the size-and-type-specific scan order
(get_scanover the generatedDefault/Mrow/Mcolscan tables), and
derives thecoeff_base/coeff_brcontexts from the coded block'sbwland
transform class (2D vs. the 1D row/column identities). The scan tables are
generated from a vendored spec extract byscripts/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_DCTcase of the general decoder and stays bit-exact against
libavif on every fixture.otf-pixels-codec-avifgains the full lossy inverse-transform machinery, the
foundation of the lossy decode path.inverse_transform_2druns 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, anddequantizeapplies the realDc_Qlookup/Ac_Qlookup
quantiser steps (generated from the spec). The lossless reconstruct now flows
through the same pair — qindex 0 is just itsdc == 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-avifdecodes 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-avifdecodes 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
reportUnsupportedrather than decode wrong.otf-pixels-codec-avifgains the 4x4 coefficient decoder — the syntax that
turns a transform block's entropy-coded symbols into a signed level array.
It readsall_zero, the end-of-block position, each coefficient's base level
walking the scan backwards, thecoeff_brand 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_brand end-of-block buckets) are transcribed from the spec and unit
tested against its worked contexts. Scoped toTX_4X4, which is every block
the lossless path codes; the wider transform sizes arrive with the lossy path.otf-pixels-codec-avifgains 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 assembledAboveRow/LeftColneighbour 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, reportUnsupporteduntil that lands.otf-pixels-codec-avifgains the first reconstruction primitives: a bounds-safe
samplePlane(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 = 2divides 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-avifgains 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.pyencodes fixtures with libavif's
avifencand takes the expected rasters from libavif'savifdec— an
independent AV1 implementation. The first fixtures are lossless, the strictest
target there is: lossless AVIF isCodedLossless, 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-avifgains the AV1 default CDF tables, generated rather than
hand-transcribed.scripts/generate-av1-cdf-tables.pyemitsav1/cdf.rsfrom
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 shapeSymbolDecoder::read_symbolconsumes: cumulative frequencies
ending in1 << 15, then a0adaptation counter.otf-pixels-codec-avifgains 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'sentdec.c; the two agree on the inverse-CDF
arithmetic and the renormalization. Reads are fallible and theSymbolMaxBits
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-avifgains 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-firstBitReaderwith the AV1 primitives
(f,uvlc,leb128,le,su,ns, byte alignment), the OBU framing
layer, the full sequence header (color_configand 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
soCodedLosslessis known before the filter parameters that depend on it.
AStillPicturedriver 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_rowstill
reportsUnsupported.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/ipmawith
ispe/av1C/pixi/colr/irot/imir/auxC).AvifDecoderreports
correctmetadata()— dimensions, pixel format, alpha, grid — for any AVIF,
andAvifCodec/probesniff the file by itsftypbrand. Wired into the
facade behind anaviffeature, on by default. Pixel decode currently reports
Unsupported: the AV1 bitstream reconstruction lands in the phases that
follow. Still images only; image sequences (theavisbrand) are out of scope
for v1.otf-pixels-codec-webp: WebP decode (lossy and lossless, alpha detected
from the container) and lossless encode, wrappingimage-webpper ADR-0004.
Wired into the facade behind awebpfeature, on by default. Pure Rust; its
only transitive dependencies arebyteorder-liteandquick-error.- WebP encoding is lossless only, because the wrapped encoder has no
quality control at all —EncodeOptions::qualityis 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-progressivefeature ofotf-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 = falsedrops it and
progressive files then reportUnsupportednaming 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_progressivereports 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_loadrewrites 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_streamruns 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::rescaledreplaces 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.resizememoizes filter tables keyed to the shape it first saw, so
reusing the instance was a resample against the wrong scale waiting to
happen.crop,convolveandcompositedecline outright — coordinates
and kernel sizes are in input pixels.Output::write_with_statsreturnsRunStats, 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_orientis 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
Unsupportedrather thanMalformed: they are valid JPEGs this codec does
not own, and a host binding routes on that difference.otf-pixels-codec-jpegfixtures 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::fittingpicks 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-leftMxMcorner, 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 ajpeg
feature, on by default.Image::from_streamrecognises a JPEG by its magic
bytes andoutput(Format::Jpeg, options)writes one, so a decode/resize/
re-encode pipeline works end to end.- A
jpeg_decodefuzz target and an in-tree mutation harness, both asserting
only that no input panics, plus ajpeg_roundtriptarget 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 whenvipsis 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:TiffDecodercovering 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 reportsDecodeCapability::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:DecodedSourcenow 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,
behindgifandtifffeatures.otf-pixels-codec-gif:GifDecodercovering 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.Decoderyields the
first frame so ordinary pipelines work unchanged;next_framewalks the
rest, keeping "animation pipelines are v2" honest.otf-pixels-compress: inflate, deflate, zlib,Crc32andAdler32moved
fromotf-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,flattenandcompositeon the
chainable facade.compositejoins two lazy branches of a graph.benches/ops.rs: the M4 comparative benchmark againstimageand
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),Convolvewith
blur, Gaussian and sharpen presets,Composite(Porter-Duff source-over,
the first two-input op),ExtractChannelandFlatten. 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 andwithout_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:InflaterandZlibStream, the incremental
decompressors the streaming decoder is built on, plusChunkStream, 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 fuzztargets for PNG decode, inflate and encode/decode
round-trip, plustests/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::openandImage::from_stream, which identify a format
from its magic bytes and never from a file extension (SPEC §Formats).
Format::Pngnow resolves to a real encoder behind thepngfeature.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::qualityis 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 overcrossbeam-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.
BufferSourcereportsRegions;DecodedSourcedelegates to its decoder.otf-pixels-core:Scheduler, the demand-driven parallel tile evaluator,
plusevaluate_tiledandRunStats. 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_optionsfor tuning a run.- Builder setters on
Limits,PlanOptionsandSchedulerOptions, 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:PixelsErrorwith stableErrorCodes,PixelFormatand
theSampledispatch seam (ADR-0002),Region/ImageDescriptor/Limits
withmax_pixelsenforced before allocation, stridedTile/TileMut/
TileBufviews, streamingSource/Sinktraits (ADR-0005),Decoder/
Encoder/Codectraits, theOptrait and lazyImageop graph, and the
naive whole-image evaluator that M2 will be diffed against.otf-pixels-codec-raw:RawDecoder/RawEncoderwith 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,FlipandFlopgeometry 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 theoutput(format, options)terminal with
write(sink)andbytes(). 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.