* craft-jpeg: decode sequential 8-bit JPEG a band of rows at a time
Bands reads the headers of a sequential Huffman-coded (SOF0, SOF1) 8-bit file whose first scan
holds every component, and a Reader decodes it one line of MCUs at a time: Huffman decoding,
dequantization, inverse transform, upsampling and colour conversion, holding at most three MCU
lines of samples. Nothing the size of the image is allocated, so a frame up to the format's
65,535 px sides can be decoded straight into the caller's storage.
The samples equal decode()'s for the same rows: both go through the same block transform
(idct_block, factored out of reconstruct), the same upsampling maps and the same colour
conversion (to_color, to_u8). A component subsampled down reads the MCU line above and below
its band, as the whole-image upsampling does.
A band that begins a restart interval is a seek point once find_seek_points has found every
restart marker, in sequence; readers on other threads can decode from there. Damaged data
leaves the rest of its restart interval at zero and decoding resumes at the next marker, so the
result is the same however the bands were read. Bits::restart now also clears the exhausted
flag: decode() never restarts after running out of data, the band decoder does.
parse_frame is split into parse_header (validation and geometry) and the whole-image
allocation; parse_scan takes the component identifiers. decode() is unchanged.
Tests: bands against decode() sample for sample over jpeg-encoder streams (gray, YCbCr, CMYK,
YCCK; 4:4:4, 4:2:2, 4:2:0, 4:4:0; four restart intervals) and a test-only encoder's interleaved
layouts with factors of 3 and 4, in order and by seeking; zune-jpeg within the existing
tolerance; damage with and without restarts; limits; unsupported files; the libjpeg test
images; the mutation run with a seek-equals-in-order check; and a `bands` fuzz target.
Co-Authored-By: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
* craft-jpeg: faster inverse transform, rounding, Huffman codes and band rows
Each change keeps every sample as it was: decode() and the bands give bit-identical output
before and after on the test images, 12-bit and arithmetic-coded files, 48 generated streams
and a 48,000 × 34,200 file decoded in bands.
- idct_block works on rows of eight: each sum keeps its order (over u, then over v), eight
values of x at a time, and rows of zero coefficients are skipped (a zero term changes only the
sign of a zero, which the level shift removes). The transposed basis is built once.
- to_u8 rounds half away from zero as truncation plus one from .5 up, exact on 0..=255, instead
of the `round` library call targets without SSE4.1 make.
- Huffman::decode resolves codes of up to 9 bits from a table of the next 9 bits, built by
running the bit-by-bit procedure on each, so both give the same symbol.
- The band rows are colour converted in loops written out for gray, YCbCr and RGB; YCbCr to
RGB is one helper shared with the whole-image decode.
On the 48,000 × 34,200 4:4:4 file with a restart interval per line of MCUs (Ryzen 7 7800X3D,
idle), one thread decodes a band of 8 rows in about 3.0 ms (5.8 ms before), and 16 threads
decode the whole image in 2.05 s (3.8 s before).
Co-Authored-By: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
* craft-jpeg: bands report damage, never fail on it, and see data left before a restart
Two cases a mutation run through PhotoCraft's import turned up.
- Data that fails in the first MCU made a band read fail, as decode() refuses a stream with no
image data. The baseline decoder PhotoCraft uses for other files opens such files (grey, with
restarts the rest of the image), so a band read now marks it damaged and goes on: with
restart intervals everything after the first decodes. Refusing a file with nothing
decodable is the caller's, from the size of its scan data.
- An interval that decodes without an invalid code but leaves whole bytes before its restart
marker was decoded from cut, shifted or added data. It now counts as damage (libjpeg warns
"extraneous bytes before marker"). The check runs after an interval's last MCU, so readers
that seek and readers that read in order agree.
Over 1,074 sequential 8-bit JPEGs on one machine the damage flag agrees with djpeg's warnings on
every file, reading from seek points equals reading in order on all 151 with restart intervals,
and the samples are within mean 0.57, max 8 of zune-jpeg's.
Co-Authored-By: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
* craft-jpeg: seek points follow the restart markers in the data, as reading in order does
find_seek_points gave up on a file whose markers were not all there in sequence, so a damaged
or cut-short file with a restart interval at every line of MCUs decoded in order on one core
(about 13 s for an undamaged 48,000 × 34,200 file on an idle 8-core machine, against 2 s on
16 threads). With these seek points the same file with one marker renumbered, cut at 60%, or
its first interval overwritten decodes in 2.8 to 4.4 s on 16 threads. A reader that reads in
order pairs the k-th restart marker in the data with interval k, whatever its number, and
resumes after each, so seek points built the same way give the same samples for any data:
missing, misnumbered or added markers only change which bands are seek points.
Tested: misnumbered, missing (file cut), added and removed markers read in order and by
seeking; the mutation run's seek-equals-in-order check (more of its cases now seek); 600,000
runs of the bands fuzz target, which asserts the same; 169 local files with restarts.
Co-Authored-By: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
* craft-jpeg: Bands::halo, the MCU lines a reader decodes around its bands
A reader that seeks decodes the MCU line above and below its bands when a component is
subsampled down. A caller splitting the work across threads needs to know it: reading one band
from each seek point of a 4:2:0 file decodes three lines per band (4.97 s for a 48,000 × 34,200
file on 16 threads, against 3.09 s reading four bands from each), while for 4:4:4 the run length
makes no difference. Tested against the sampling factors of every interleaved layout.
Co-Authored-By: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
* craft-jpeg: band decoder review fixes
- Data left over after the last MCU, before EOI, is damage, as it already
was before a restart marker.
- A scan that names a component twice is Malformed (T.81 B.2.3), not
Unsupported.
- The upsampling maps are sized from the header and checked against
max_alloc before they are made.
- The bands fuzz target and the mutation test fail on a read or seek error
once Bands::new has accepted a stream; damage never surfaces as an error.
- Known-sample CMYK and YCCK fixtures check the ink polarity directly; the
bands-equal-decode tests cannot, since both share the colour conversion.
- Docs: a single-component frame's bands are 8 rows; Reader::damaged and
the scan's size cannot tell a failure at the first MCU from later damage;
with a halo a seek decodes from the previous seek point; each fuzz target
writes to its own corpus; doc comments on the remaining new functions.
Co-Authored-By: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
* craft-jpeg: test YCCK chroma and trailing-data samples
- The ink fixture gains a YCCK case with Cb and Cr away from neutral, with
the T.871 values worked out by hand: a conversion that drops or swaps
the chroma now fails it.
- Bytes added before EOI leave the samples as the clean stream's; the test
now checks that, not only that reading in order and seeking agree.
Co-Authored-By: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
---------
Co-authored-by: Νύξ (Nyx, AI) 🌑 <nyx@despotak.is>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
render keeps a column map and a row map for each component while it writes
the output, but the check before allocating counted only the coefficients,
the reconstructed planes and the output. The maps are now counted too, at
their real element size.
libjpeg's encoder drops trailing zero bytes, so a flat tail of a few thousand blocks decodes from zeros supplied at EOI. Damage is data that runs to the end of the file or a lost restart marker. whitetail-ari.jpg (cjpeg -arithmetic) and its Huffman transcode cover it.
The test encoder takes several components, scans of any of them, segments
between scans and per-table conditioning, and drops trailing zero bytes as
libjpeg does.
tests/mutation.rs runs a seeded mutation pass over the test images and
small SOF9, SOF10 and 12-bit seeds, checking that decode never panics
and stays within small limits. fuzz/ is a cargo-fuzz crate (own
workspace) with a decode target and those seeds. jpeg.yml runs fmt,
clippy, tests and a wasm32 check on Linux. The README gains a testing
section.
Check coefficients, planes and output against max_alloc together. Stop an
arithmetic scan once it has consumed more than 8 zero bytes past its data or
misses a restart marker, and cap the decoded blocks at 64 passes over the
frame instead of capping the scan count.
Commit testorig.jpg, testimgari.jpg and testorig12.jpg with a jpegtran
Huffman transcode of testimgari.jpg and libjpeg-turbo's 12-bit decode
of testorig12.jpg. The arithmetic-coded image must match its transcode
exactly; the 8-bit and 12-bit decodes are compared against zune-jpeg
and libjpeg-turbo within a small tolerance. Replaces the ignored
conformance_samples test.
Every scan walks its whole MCU grid, and an arithmetic-coded scan does so
even with no entropy-coded data (the decoder is fed zeros), so a small file
repeating empty scan headers multiplied the work by the frame size. Scans
past the cap are dropped and the image is reported damaged, keeping what
the earlier scans decoded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The jpeg_ext path set exif, xmp and dpi but not photoshop_resources, so APP13 8BIM blocks
(paths, Photoshop settings) were lost on import and re-export. Both decoder paths now fill
the same Metadata. Test: a 12-bit JPEG with an APP13 block keeps it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bits now tracks how many buffered bits are zero padding supplied at a
marker or the end of the data; a read that consumes padding marks the
scan exhausted and the block returns an error, so the scan is reported
as damaged (or fails if no MCU decoded) even when EOI is present.
Arithmetic termination is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zune-jpeg decodes neither 12-bit extended sequential (SOF1, P=12) nor
arithmetic-coded (SOF9/SOF10, DAC) JPEGs. Route those files to a new
clean-room T.81 decoder (codecs/jpeg_ext.rs): Huffman sequential 12-bit,
Annex D arithmetic decoding with DAC conditioning (sequential F.2.4 and
progressive G.1.3, 8 or 12-bit), restart intervals, 1/3/4 components and
any sampling factors. Float IDCT, centred linear upsampling, the existing
YCbCr / Adobe CMYK / YCCK conventions; 12-bit output is U16 (scaled).
Allocations are checked against Limits first; every stream read is
bounds-checked and damaged entropy data stops the scan with a Truncated
warning instead of panicking.
Tests: a test-only arithmetic encoder round-trips coefficients through
sequential and progressive (incl. successive approximation) scans with
restarts; a hand-built 12-bit SOF1 block; baseline files relabelled SOF1
match zune; truncation/byte-flip sweeps never panic. Locally the
conformance samples testorig12.jpg and testimgari.jpg decode with a mean
abs diff < 1 level vs testorig.jpg (samples not committed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>