craft-jpeg: decode sequential 8-bit JPEG a band of rows at a time (#743)

* 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>
This commit is contained in:
authored and GitHub committed 2026-11-11 07:37:06 -05:00
1 parent 861c5e172d
commit 97c83dc9e8
9 files changed
+1701 -94

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "craft-jpeg"
description = "Clean-room decoder for 12-bit and arithmetic-coded JPEG (ITU-T T.81)."
description = "Clean-room JPEG decoder (ITU-T T.81): 12-bit and arithmetic-coded frames, and baseline frames a band of rows at a time."
version.workspace = true
edition.workspace = true
license.workspace = true
+61 -6
View File
@@ -2,6 +2,8 @@
A clean-room JPEG decoder for the Craft apps, written from ITU-T T.81 (ISO/IEC 10918-1). It
covers the processes that common Rust JPEG decoders skip: 12-bit samples and arithmetic coding.
It also decodes the common sequential 8-bit files a band of rows at a time ([`Bands`](#decoding-in-bands)),
for images too large to decode into one buffer.
No `unsafe`, no panics on malformed input, and it builds for `wasm32-unknown-unknown`. The only
dependency is `thiserror`.
@@ -97,6 +99,46 @@ fn use_craft_jpeg(bytes: &[u8]) -> bool {
SOF11 (lossless) falls in that range and returns `Unsupported`.
## Decoding in bands
`Bands` decodes a sequential Huffman-coded (SOF0, SOF1) 8-bit file whose first scan holds every
component, one line of MCUs at a time: a band is 8 rows times the largest vertical sampling
factor (8 rows for a single-component frame, whatever its sampling factors), and the last band
may be shorter. Its samples equal `decode`'s for the same rows (the same inverse transform, upsampling
and colour conversion); only a few MCU lines are held at once. Other files return
`Error::Unsupported`: decode them whole.
```rust
use craft_jpeg::{Bands, Limits};
let mut limits = Limits::default();
limits.max_pixels = 65_535 * 65_535; // the image is never held whole
let bands = Bands::new(&bytes, &limits, adobe_transform)?;
let mut reader = bands.reader();
while let Some(band) = reader.next_band()? {
// band.y, band.rows, band.samples: interleaved like `Decoded::samples`
}
if reader.damaged() { /* warn: part of the image is missing */ }
```
- `limits.max_pixels` bounds the image; `limits.max_alloc` bounds one reader's buffers
(`Bands::reader_bytes`) and the upsampling maps, not the image.
- **In parallel.** A band that starts a restart interval is a seek point once
`Bands::find_seek_points` has found the restart marker before it (one pass over the scan
data). The k-th marker in the data begins interval k, as a reader that reads in order pairs
them, so missing, misnumbered or added markers in a damaged file change nothing but which
bands are seek points. Readers on separate threads can `seek` to seek points and decode the
bands up to the next one. Files with a restart interval at every line of MCUs, common in files
Photoshop saved, decode on every core.
- **Damaged data** leaves the rest of its restart interval at zero coefficients (mid grey) and
decoding resumes at the next restart marker; without restart intervals the rest of the scan
stays at zero, as in `decode`. An interval, or the scan, that leaves more than its padding
unread before the next marker counts as damaged too. `Reader::damaged` is set either way, and
the samples don't depend on where reading started. Damage never makes a read fail, also in the
first MCU (where `decode` reports that there is no image data). `Reader::damaged` does not
tell that case from later damage, and neither does the size of the scan: data that fails at
its first code can be any length.
## Boundaries
- In scope: decoding the frames above into samples.
@@ -119,26 +161,39 @@ SOF11 (lossless) falls in that range and returns `Unsupported`.
supplied at EOI) decodes undamaged and equal to its Huffman transcode.
- `testorig.jpg` matches `zune-jpeg`, and `testorig12.jpg` matches libjpeg-turbo's 12-bit
decode, within the float-against-integer IDCT difference (mean under 0.75, max 4).
- `src/bands/tests.rs`: `Bands` against `decode`, sample for sample, over gray, YCbCr, CMYK and
YCCK streams from `jpeg-encoder` (4:4:4, 4:2:2, 4:2:0, 4:4:0; restart intervals of 1 and 7
MCUs and of one and two MCU lines) and over interleaved layouts from a test-only encoder
(factors of 3 and 4 across and down, four components), read in order and by seeking; against
`zune-jpeg`; on damaged data (with and without restarts, in the first interval, and bytes
added before a restart marker), limits, and the files it refuses.
- `tests/conformance.rs` also decodes the Huffman-coded 8-bit test images in bands.
- `tests/mutation.rs`: a seeded mutation run (truncations, bit flips, byte and frame-header
overwrites, deleted and repeated spans) over the test images and the fuzz seeds. Decoding must
not panic and must stay within small `Limits`.
not panic and must stay within small `Limits`. The band decoder gets the same mutations, and
wherever seek points survive one, reading from them must give the samples reading in order
does.
`.github/workflows/jpeg.yml` runs fmt, clippy, the tests and a `wasm32-unknown-unknown` check on
Linux.
### Fuzzing
`fuzz/` is a cargo-fuzz crate outside the workspace, with one target, `decode`. Its seeds in
`fuzz/corpus/decode` are the test images plus small SOF9 (with restarts), 12-bit SOF9, SOF10
and 12-bit SOF1 streams. With nightly and `cargo install cargo-fuzz`:
`fuzz/` is a cargo-fuzz crate outside the workspace, with two targets: `decode`, and `bands`,
which also checks that reading from the seek points gives the samples reading in order does.
The seeds in `fuzz/corpus/decode` are the test images plus small SOF9 (with restarts), 12-bit
SOF9, SOF10 and 12-bit SOF1 streams; `bands` starts from them too. With nightly and
`cargo install cargo-fuzz`:
```sh
cd crates/jpeg/fuzz
cargo +nightly fuzz run decode
cargo +nightly fuzz run bands corpus/bands corpus/decode
```
Running it adds the inputs it finds to `fuzz/corpus/decode`; commit only seeds that cover
something new.
Each target adds the inputs it finds to the first corpus directory it reads: `decode` to
`fuzz/corpus/decode`, `bands` to `fuzz/corpus/bands`. Commit only seeds that cover something
new.
## Credits
+7
View File
@@ -21,3 +21,10 @@ path = "fuzz_targets/decode.rs"
test = false
doc = false
bench = false
[[bin]]
name = "bands"
path = "fuzz_targets/bands.rs"
test = false
doc = false
bench = false
+46
View File
@@ -0,0 +1,46 @@
#![no_main]
use craft_jpeg::{Bands, Limits};
use libfuzzer_sys::fuzz_target;
/// Limits that keep each input's work small: 1 MP and 64 MiB.
fn fuzz_limits() -> Limits {
let mut limits = Limits::default();
limits.max_pixels = 1 << 20;
limits.max_alloc = 64 << 20;
limits
}
fuzz_target!(|data: &[u8]| {
let limits = fuzz_limits();
// The input length picks the Adobe transform, as in the `decode` target.
let adobe = [None, Some(0), Some(1), Some(2)][data.len() % 4];
let Ok(mut bands) = Bands::new(data, &limits, adobe) else { return };
assert!(bands.reader_bytes() <= limits.max_alloc);
assert!(u64::from(bands.width()) * u64::from(bands.height()) <= limits.max_pixels);
let mut reader = bands.reader();
let mut in_order = Vec::new();
loop {
match reader.next_band() {
Ok(Some(band)) => in_order.extend_from_slice(band.samples),
Ok(None) => break,
// Damage is reported by `Reader::damaged`, never as an error.
Err(e) => panic!("a read after Bands::new accepted the stream: {e}"),
}
}
assert_eq!(in_order.len(), bands.width() as usize * bands.height() as usize * usize::from(bands.components()));
// Reading from every seek point, last first, gives the same samples.
bands.find_seek_points();
let points: Vec<usize> = bands.seek_points().collect();
let mut sought = vec![0u8; in_order.len()];
let mut reader = bands.reader();
for (k, &p) in points.iter().enumerate().rev() {
reader.seek(p).expect("a seek to an advertised seek point");
for _ in p..points.get(k + 1).copied().unwrap_or(bands.bands()) {
let band = reader.next_band().expect("a read after a seek").expect("a band after a seek");
let at = band.index * bands.band_bytes();
sought[at..at + band.samples.len()].copy_from_slice(band.samples);
}
}
assert!(sought == in_order, "seeking changes the samples");
});
+542
View File
@@ -0,0 +1,542 @@
//! Band-at-a-time decoding of sequential Huffman-coded 8-bit JPEG: see [`Bands`].
use crate::{
Bits, Error, Header, Huffman, HuffmanScan, Idct, Limits, ScanDecoder, Tables, be16, err, idct_basis, idct_block, is_ycc, parse_dac, parse_dht,
parse_dqt, parse_header, parse_scan, to_color, to_u8, unsupported, upsample_map, ycc_to_rgb,
};
/// A component as the bands decode it.
struct Comp {
/// Blocks of the component in one MCU, across and down: H × V in an interleaved scan, 1 × 1
/// when it is the scan's only component.
hb: usize,
vb: usize,
/// Samples per row of an MCU row's plane: the blocks of a line of MCUs, × 8.
stride: usize,
/// The quantization table (zigzag order) as it was at the scan.
q: [u16; 64],
/// The interpolation of a component subsampled across or down, as the whole-image decode
/// makes it ([`upsample_map`]); `None` when it has a sample for every output column or row.
xs: Option<Vec<(usize, usize, f32)>>,
ys: Option<Vec<(usize, usize, f32)>>,
}
/// A sequential Huffman-coded (SOF0, SOF1) 8-bit JPEG whose first scan holds every component,
/// decoded a band of rows at a time so that no buffer the size of the image exists.
///
/// A band is the rows of one line of MCUs (A.2): 8 times the largest vertical sampling factor
/// (8 for a single-component frame); the last band may be shorter. A [`Reader`] decodes bands
/// in order, into samples equal to [`decode`](crate::decode)'s for the same rows: the same
/// inverse transform, upsampling and colour conversion, at most a few MCU lines of them at a
/// time.
///
/// The JPEG processes [`decode`](crate::decode) covers beyond this (arithmetic coding,
/// progressive and 12-bit frames) and frames whose components come in several scans return
/// [`Error::Unsupported`]: decode those whole.
///
/// Bands can also be decoded out of order, by [`Reader`]s on separate threads: a restart
/// interval (B.2.4.4, `DRI`) that begins with a band makes the band a seek point once
/// [`Bands::find_seek_points`] has found the restart marker before it.
///
/// **Damaged data.** Entropy-coded data that is cut short or corrupt leaves the rest of its
/// restart interval at zero coefficients (mid grey), and decoding resumes at the next restart
/// marker; without restart intervals the rest of the scan stays at zero, as in
/// [`decode`](crate::decode), which does not resume at restart markers. Either way
/// [`Reader::damaged`] is set, and the result does not depend on where the decoding started.
/// An interval, or the scan, that leaves more than its padding unread before the next marker is
/// damaged too (its data was cut, shifted or added to). Damaged data never makes a read fail,
/// also in the first MCU, where [`decode`](crate::decode) reports that the stream holds no image
/// data. [`Reader::damaged`] does not tell that case from later damage, and neither does the
/// size of the scan: data that fails at its first code can be any length.
pub struct Bands<'a> {
/// The file from the scan's entropy-coded data on.
data: &'a [u8],
width: usize,
height: usize,
/// In frame order (the output's channel order).
comps: Vec<Comp>,
/// The scan's components in scan order: frame index, DC and AC table.
scan: Vec<(usize, Huffman, Huffman)>,
/// MCUs per line and lines of MCUs (bands) of the scan.
mcux: usize,
mcuy: usize,
band_rows: usize,
/// MCUs per restart interval; 0 without restart intervals.
restart: usize,
ycc: bool,
/// 1 when a component is subsampled down: a band's rows then read the last sample row of the
/// MCU line above it and the first of the one below.
lag: usize,
/// Bands decoding can start at, with the offset of their entropy-coded data in `data`;
/// ascending, band 0 first.
seek: Vec<(usize, usize)>,
}
impl<'a> Bands<'a> {
/// Reads the headers of `bytes` up to the first scan.
///
/// `limits.max_pixels` bounds the image; `limits.max_alloc` bounds what one [`Reader`]
/// holds ([`Bands::reader_bytes`]) and the sampling maps kept here, not the image.
/// `adobe_transform` is the transform byte of the APP14 `Adobe` segment, as for
/// [`decode`](crate::decode).
pub fn new(bytes: &'a [u8], limits: &Limits, adobe_transform: Option<u8>) -> Result<Self, Error> {
if bytes.get(..2) != Some(&[0xFF, 0xD8]) {
return Err(err("not a JPEG stream"));
}
let mut tables = Tables::default();
let mut header: Option<Header> = None;
let mut i = 2;
while let Some(&v) = bytes.get(i) {
if v != 0xFF {
i += 1;
continue;
}
let Some(&m) = bytes.get(i + 1) else { break };
match m {
0xFF => {
i += 1;
continue;
}
0x00 | 0x01 | 0xD0..=0xD8 => {
i += 2;
continue;
}
0xD9 => break,
_ => {}
}
let Ok(len) = be16(bytes, i + 2) else { break };
let len = usize::from(len);
if len < 2 {
return Err(err("bad marker segment length"));
}
let seg = bytes.get(i + 4..i + 2 + len).ok_or_else(|| err("marker segment runs past the end of the file"))?;
i += 2 + len;
match m {
0xDB => parse_dqt(seg, &mut tables)?,
0xC4 => parse_dht(seg, &mut tables)?,
0xCC => parse_dac(seg, &mut tables)?,
0xDD => tables.restart = usize::from(be16(seg, 0)?),
0xC0..=0xCF => {
if header.is_some() {
return Err(unsupported("JPEG with more than one frame is not supported"));
}
let hd = parse_header(m, seg)?;
if hd.arithmetic || hd.progressive || hd.precision != 8 {
return Err(unsupported("only sequential Huffman-coded 8-bit JPEG is decoded in bands"));
}
header = Some(hd);
}
0xDA => {
let hd = header.ok_or_else(|| err("scan before the frame header"))?;
let data = bytes.get(i..).unwrap_or_default();
return Self::open(data, hd, seg, &tables, limits, adobe_transform);
}
_ => {}
}
}
Err(err(if header.is_some() { "the file ends before any image data" } else { "no frame header" }))
}
/// The bands of the frame `hd` and its first scan `sos`, with the tables in effect at the
/// scan; checked against `limits` before any buffer is sized from the headers.
fn open(data: &'a [u8], hd: Header, sos: &[u8], tables: &Tables, limits: &Limits, adobe_transform: Option<u8>) -> Result<Self, Error> {
let ids: Vec<u8> = hd.comps.iter().map(|c| c.0).collect();
let scan = parse_scan(sos, &ids, false)?;
let mut seen = [false; 4];
for &(ci, _, _) in &scan.comps {
match seen.get_mut(ci) {
Some(s) if !*s => *s = true,
// T.81 B.2.3: a scan's components follow the frame's order, so none repeats.
_ => return Err(err("the scan names a component twice")),
}
}
if scan.comps.len() != hd.comps.len() {
return Err(unsupported("JPEG whose components are in more than one scan is not decoded in bands"));
}
let (width, height) = (hd.width, hd.height);
if width == 0 || height == 0 {
return Err(err(format!("zero-sized image {width}x{height}")));
}
let pixels = (width as u64).saturating_mul(height as u64);
if pixels > limits.max_pixels {
return Err(Error::LimitExceeded(format!("{pixels} pixels exceed max {}", limits.max_pixels)));
}
let single = scan.comps.len() == 1;
let (mcux, mcuy) = match hd.comps.first() {
Some(&(_, h, v, _)) if single => {
let (cw, ch) = hd.samples(h, v);
(cw.div_ceil(8), ch.div_ceil(8))
}
_ => (hd.mcux, hd.mcuy),
};
let band_rows = if single { 8 } else { 8 * hd.vmax };
let mut comps = Vec::with_capacity(hd.comps.len());
for &(_, h, v, tq) in &hd.comps {
let (hb, vb) = if single { (1, 1) } else { (h, v) };
let q = tables.quant.get(tq).copied().flatten().ok_or_else(|| err("scan uses an undefined quantization table"))?;
// The interpolation maps are made once the limit check below has passed.
comps.push(Comp { hb, vb, stride: mcux * hb * 8, q, xs: None, ys: None });
}
let mut coding = Vec::with_capacity(scan.comps.len());
for &(ci, td, ta) in &scan.comps {
let dc = tables.dc.get(td).and_then(Option::as_ref).ok_or_else(|| err("scan uses an undefined Huffman table"))?;
let ac = tables.ac.get(ta).and_then(Option::as_ref).ok_or_else(|| err("scan uses an undefined Huffman table"))?;
coding.push((ci, dc.clone(), ac.clone()));
}
let lag = usize::from(hd.comps.iter().any(|&(_, _, v, _)| v != hd.vmax));
let mut bands = Bands {
data,
width,
height,
comps,
scan: coding,
mcux,
mcuy,
band_rows,
restart: tables.restart,
ycc: is_ycc(ids.into_iter(), adobe_transform),
lag,
seek: vec![(0, 0)],
};
// Checked before the maps are made: one entry per output column (or row) of each
// component subsampled across (or down).
let entry = std::mem::size_of::<(usize, usize, f32)>() as u64;
let maps: u64 =
hd.comps.iter().map(|&(_, h, v, _)| (u64::from(h != hd.hmax) * width as u64 + u64::from(v != hd.vmax) * height as u64) * entry).sum();
let total = bands.reader_bytes().saturating_add(maps);
let cap = limits.max_alloc.min(isize::MAX as u64);
if total > cap {
return Err(Error::LimitExceeded(format!("{total} bytes of band buffers exceed the limit of {cap}")));
}
for (&(_, h, v, _), c) in hd.comps.iter().zip(&mut bands.comps) {
let (cw, ch) = hd.samples(h, v);
c.xs = (h != hd.hmax).then(|| upsample_map(width, cw, h, hd.hmax));
c.ys = (v != hd.vmax).then(|| upsample_map(height, ch, v, hd.vmax));
}
Ok(bands)
}
/// The image's width in pixels.
pub fn width(&self) -> u32 {
self.width as u32
}
/// The image's height in pixels: the frame's (a `DNL` height is not supported).
pub fn height(&self) -> u32 {
self.height as u32
}
/// Samples per pixel, as [`Decoded::components`](crate::Decoded::components) describes them:
/// 1 gray, 3 RGB, 4 CMYK.
pub fn components(&self) -> u8 {
self.comps.len() as u8
}
/// Rows in a band (the last one may have fewer).
pub fn band_rows(&self) -> u32 {
self.band_rows as u32
}
/// How many bands make up the image.
pub fn bands(&self) -> usize {
self.mcuy
}
/// MCU lines a [`Reader`] decodes on each side of the bands it hands out: 1 when a component
/// is subsampled down (a band's rows interpolate with the lines above and below), else 0.
/// When it is 1, a [`Reader`] that [seeks](Reader::seek) to seek point `p` starts decoding at
/// the seek point `q` before it: reading `n` bands from `p` decodes `p − q + n + 1` lines,
/// `n + 2` when restart intervals begin every line of MCUs. A caller that splits the work
/// reads a few bands from each seek point then.
pub fn halo(&self) -> u32 {
self.lag as u32
}
/// Bytes of the samples of a full band.
pub fn band_bytes(&self) -> usize {
self.band_rows * self.width * self.comps.len()
}
/// Bytes one [`Reader`] holds while it decodes: the reconstructed MCU lines it keeps, one
/// interpolated row per component and a band of samples.
pub fn reader_bytes(&self) -> u64 {
let slots = (2 * self.lag + 1) as u64;
let planes: u64 = self.comps.iter().map(|c| slots * (c.vb * 8 * c.stride) as u64 * 4).sum();
let lines = (self.comps.len() * self.width) as u64 * 4;
planes.saturating_add(lines).saturating_add(self.band_bytes() as u64)
}
/// Finds the restart markers in the scan, so that every band a restart interval begins with
/// becomes a seek point. One pass over the entropy-coded data. The k-th restart marker in the
/// data begins interval k, whatever its number: as a reader that reads in order pairs them,
/// so that reading from a seek point gives the same samples also when markers are missing,
/// misnumbered or added (damaged data), and the bands past the last marker found are reached
/// in order from the last seek point.
pub fn find_seek_points(&mut self) {
self.seek.truncate(1);
let (restart, mcux) = (self.restart, self.mcux);
if restart == 0 {
return;
}
let intervals = self.mcux.saturating_mul(self.mcuy).div_ceil(restart);
let d = self.data;
let (mut i, mut k) = (0usize, 0usize);
while let Some(p) = d.get(i..).and_then(|r| r.iter().position(|&v| v == 0xFF)) {
i += p;
match d.get(i + 1) {
Some(0x00) => i += 2,
Some(0xFF) => i += 1,
Some(0xD0..=0xD7) => {
k += 1;
if k >= intervals {
return;
}
let first = k * restart;
if first.is_multiple_of(mcux) {
self.seek.push((first / mcux, i + 2));
}
i += 2;
}
_ => return,
}
}
}
/// The bands a [`Reader`] can [seek](Reader::seek) to, ascending; band 0 always.
pub fn seek_points(&self) -> impl ExactSizeIterator<Item = usize> + '_ {
self.seek.iter().map(|s| s.0)
}
/// A reader at band 0.
pub fn reader(&self) -> Reader<'_> {
let slots = 2 * self.lag + 1;
Reader {
dec: HuffmanScan { bits: Bits::new(self.data), tables: self.scan.iter().map(|(_, dc, ac)| (dc, ac)).collect(), pred: [0; 4] },
planes: self.comps.iter().map(|c| vec![0f32; slots * c.vb * 8 * c.stride]).collect(),
lines: self.comps.iter().map(|c| if c.xs.is_some() || c.ys.is_some() { vec![0f32; self.width] } else { Vec::new() }).collect(),
out: vec![0u8; self.band_bytes()],
idct: idct_basis(),
bands: self,
decoded: 0,
next: 0,
fresh: true,
skip_to: 0,
damaged: false,
}
}
}
/// One band of samples, interleaved by pixel, rows top to bottom.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Band<'r> {
/// Its index: rows from `index × band_rows` on.
pub index: usize,
/// The image row of its first row.
pub y: u32,
pub rows: u32,
pub samples: &'r [u8],
}
/// Decodes [`Bands`] in order from a seek point.
pub struct Reader<'b> {
bands: &'b Bands<'b>,
dec: HuffmanScan<'b>,
/// Per component: the reconstructed samples of the last `2 × lag + 1` MCU lines decoded,
/// line `m` in slot `m % slots`.
planes: Vec<Vec<f32>>,
/// Per subsampled component: one output row of interpolated samples.
lines: Vec<Vec<f32>>,
out: Vec<u8>,
idct: Idct,
/// The next MCU line to decode and the next band to hand out.
decoded: usize,
next: usize,
/// At a seek point: the first MCU follows its restart marker already.
fresh: bool,
/// MCUs before this one are left at zero coefficients (damaged data).
skip_to: usize,
damaged: bool,
}
impl<'b> Reader<'b> {
/// Moves to `band`, which must be one of [`Bands::seek_points`]: the next band handed out is
/// `band`. When [`Bands::halo`] is 1 the decoding starts at the seek point before `band`, for
/// the line above it.
pub fn seek(&mut self, band: usize) -> Result<(), Error> {
let b = self.bands;
let at = b.seek.binary_search_by_key(&band, |s| s.0).map_err(|_| unsupported(format!("band {band} is not a seek point")))?;
// A band reads the last sample row of the MCU line above it when a component is
// subsampled down: start at the seek point before it.
let from = if b.lag > 0 && at > 0 { at - 1 } else { at };
let &(first, offset) = b.seek.get(from).ok_or_else(|| unsupported("no seek point"))?;
self.dec.bits = Bits { pos: offset, ..Bits::new(b.data) };
self.dec.pred = [0; 4];
self.decoded = first;
self.next = band;
self.fresh = true;
self.skip_to = 0;
Ok(())
}
/// Whether any data decoded so far was damaged (see [`Bands`]).
pub fn damaged(&self) -> bool {
self.damaged
}
/// The next band, or `None` past the last one.
pub fn next_band(&mut self) -> Result<Option<Band<'_>>, Error> {
let b = self.bands;
if self.next >= b.mcuy {
return Ok(None);
}
let need = (self.next + b.lag).min(b.mcuy - 1);
while self.decoded <= need {
self.decode_line(self.decoded)?;
self.decoded += 1;
}
let index = self.next;
let y = index * b.band_rows;
let rows = b.band_rows.min(b.height - y);
self.render(y, rows);
self.next += 1;
let len = rows * b.width * b.comps.len();
Ok(Some(Band { index, y: y as u32, rows: rows as u32, samples: self.out.get(..len).unwrap_or_default() }))
}
/// Entropy-decodes MCU line `m` and reconstructs its blocks into the planes.
fn decode_line(&mut self, m: usize) -> Result<(), Error> {
let b = self.bands;
let slots = 2 * b.lag + 1;
let shift = 128.0;
for mx in 0..b.mcux {
let i = m * b.mcux + mx;
if b.restart > 0 && i > 0 && i.is_multiple_of(b.restart) && !self.fresh {
self.dec.restart();
}
self.fresh = false;
for (sc, &(ci, _, _)) in b.scan.iter().enumerate() {
let (Some(c), Some(plane)) = (b.comps.get(ci), self.planes.get_mut(ci)) else { return Err(err("bad scan component")) };
let base = (m % slots) * c.vb * 8 * c.stride;
for by in 0..c.vb {
for bx in 0..c.hb {
let at = base + by * 8 * c.stride + (mx * c.hb + bx) * 8;
let out = plane.get_mut(at..).unwrap_or_default();
if i < self.skip_to {
for y in 0..8 {
if let Some(row) = out.get_mut(y * c.stride..y * c.stride + 8) {
row.fill(shift);
}
}
continue;
}
let mut blk = [0i32; 64];
if self.dec.block(sc, &mut blk).is_err() {
// The rest of the restart interval (or of the scan) stays at zero;
// the partly decoded block is kept, as the whole-image decode keeps it.
self.damaged = true;
self.skip_to = i.checked_div(b.restart).map_or(usize::MAX, |k| (k + 1) * b.restart);
}
idct_block(&blk, &c.q, &self.idct, 255.0, out, c.stride);
}
}
}
// An interval, or the scan, that ends with more than its padding before the next
// marker decoded from data that was cut, shifted or added to: damaged, though no code
// was invalid.
let end = i + 1;
let total = b.mcux * b.mcuy;
let interval_end = b.restart > 0 && end.is_multiple_of(b.restart);
if (interval_end || end == total) && i >= self.skip_to && self.dec.bits.data_left() {
self.damaged = true;
}
}
Ok(())
}
/// Writes output rows `y..y + rows` into `out`.
fn render(&mut self, y: usize, rows: usize) {
let Reader { bands: b, planes, lines, out, decoded, .. } = self;
let (b, decoded) = (*b, *decoded);
let (w, nc) = (b.width, b.comps.len());
for r in 0..rows {
let yy = y + r;
// Subsampled components: interpolated as the whole-image decode does.
for ((c, comp), line) in b.comps.iter().enumerate().zip(lines.iter_mut()) {
if comp.xs.is_none() && comp.ys.is_none() {
continue;
}
let (y0, y1, fy) = comp.ys.as_ref().and_then(|m| m.get(yy).copied()).unwrap_or((yy, yy, 0.0));
let (r0, r1) = (plane_row(b, planes, decoded, c, y0), plane_row(b, planes, decoded, c, y1));
let at = |row: &[f32], x: usize| row.get(x).copied().unwrap_or(0.0);
for (x, o) in line.iter_mut().enumerate() {
let (x0, x1, fx) = comp.xs.as_ref().and_then(|m| m.get(x).copied()).unwrap_or((x, x, 0.0));
let top = at(r0, x0) + (at(r0, x1) - at(r0, x0)) * fx;
let bottom = at(r1, x0) + (at(r1, x1) - at(r1, x0)) * fx;
*o = top + (bottom - top) * fy;
}
}
let mut src: [&[f32]; 4] = [&[]; 4];
for (((c, comp), line), s) in b.comps.iter().enumerate().zip(lines.iter()).zip(src.iter_mut()) {
let row = if comp.xs.is_none() && comp.ys.is_none() { plane_row(b, planes, decoded, c, yy) } else { line.as_slice() };
*s = row.get(..w).unwrap_or_default();
}
let Some(out) = out.get_mut(r * w * nc..(r + 1) * w * nc) else { continue };
convert_row(&src, nc, b.ycc, out);
}
}
}
/// One output row from the components' rows `src` (`w` samples each; a short one reads as
/// zeros): the colour conversion and rounding of [`to_color`] and [`to_u8`], with the common
/// layouts written out so they compile to straight loops.
fn convert_row(src: &[&[f32]; 4], nc: usize, ycc: bool, out: &mut [u8]) {
let w = out.len() / nc.max(1);
let [a, b, c, d] = src.map(|s| if s.len() == w { s } else { &[][..] });
match (nc, ycc, a.len() == w && (nc < 2 || b.len() == w) && (nc < 3 || c.len() == w) && (nc < 4 || d.len() == w)) {
(1, _, true) => {
for (o, &g) in out.iter_mut().zip(a) {
*o = to_u8(g);
}
}
(3, true, true) => {
for (o, ((&y, &cb), &cr)) in out.as_chunks_mut::<3>().0.iter_mut().zip(a.iter().zip(b).zip(c)) {
let [r, g, bl] = ycc_to_rgb(y, cb, cr, 128.0);
*o = [to_u8(r), to_u8(g), to_u8(bl)];
}
}
(3, false, true) => {
for (o, ((&r, &g), &bl)) in out.as_chunks_mut::<3>().0.iter_mut().zip(a.iter().zip(b).zip(c)) {
*o = [to_u8(r), to_u8(g), to_u8(bl)];
}
}
_ => {
let mut px = [0f32; 4];
for (x, o) in out.chunks_exact_mut(nc).enumerate() {
for (p, row) in px.iter_mut().zip(src) {
*p = row.get(x).copied().unwrap_or(0.0);
}
to_color(&mut px, nc, ycc, 255.0);
for (o, &p) in o.iter_mut().zip(&px) {
*o = to_u8(p);
}
}
}
}
}
/// The samples of component `c`'s row `r` (in its own coordinates) in `planes`, which hold the
/// MCU lines before `decoded`; empty when that line isn't held, which a band never asks for.
fn plane_row<'p>(b: &Bands<'_>, planes: &'p [Vec<f32>], decoded: usize, c: usize, r: usize) -> &'p [f32] {
let (Some(comp), Some(plane)) = (b.comps.get(c), planes.get(c)) else { return &[] };
let rows = comp.vb * 8;
let (m, k) = (r / rows, r % rows);
let slots = 2 * b.lag + 1;
if m >= decoded || m + slots < decoded {
return &[];
}
let at = ((m % slots) * rows + k) * comp.stride;
plane.get(at..at + comp.stride).unwrap_or_default()
}
#[cfg(test)]
mod tests;
+708
View File
@@ -0,0 +1,708 @@
use super::*;
use crate::{Decoded, Samples, decode};
// --- Helpers ----------------------------------------------------------------------------------
/// The transform byte of the stream's APP14 `Adobe` segment, as an app would pass it.
fn adobe(b: &[u8]) -> Option<u8> {
let at = b.windows(9).position(|w| w[..2] == [0xFF, 0xEE] && &w[4..9] == b"Adobe")?;
b.get(at + 4 + 11).copied()
}
/// The whole-image decode of `b`, with the transform its own APP14 segment states.
fn whole(b: &[u8]) -> Result<Decoded, Error> {
decode(b, &Limits::default(), adobe(b))
}
/// The 8-bit samples of a decode.
fn eight(d: &Decoded) -> &[u8] {
match &d.samples {
Samples::U8(v) => v,
Samples::U16(_) => panic!("16-bit samples"),
}
}
/// Every band, read in order from band 0, joined; and whether the reader saw damage.
fn in_order(b: &Bands) -> Result<(Vec<u8>, bool), Error> {
let mut r = b.reader();
let mut out = Vec::new();
let mut expect = 0;
while let Some(band) = r.next_band()? {
assert_eq!(band.index, expect);
assert_eq!(band.y, expect as u32 * b.band_rows());
assert_eq!(band.samples.len(), band.rows as usize * b.width() as usize * usize::from(b.components()));
out.extend_from_slice(band.samples);
expect += 1;
}
assert_eq!(expect, b.bands());
Ok((out, r.damaged()))
}
/// Every band, read from each seek point to the next, the stretches taken in a scrambled order
/// by two readers that take turns; joined in image order.
fn by_seeking(b: &Bands) -> Result<(Vec<u8>, bool), Error> {
let points: Vec<usize> = b.seek_points().collect();
let band_bytes = b.band_rows() as usize * b.width() as usize * usize::from(b.components());
let total = b.width() as usize * b.height() as usize * usize::from(b.components());
let mut out = vec![0u8; total];
let mut readers = [b.reader(), b.reader()];
let mut order: Vec<usize> = (0..points.len()).collect();
order.reverse();
order.rotate_left(points.len() / 3);
for (turn, &k) in order.iter().enumerate() {
let r = &mut readers[turn % 2];
r.seek(points[k])?;
let end = points.get(k + 1).copied().unwrap_or(b.bands());
for index in points[k]..end {
let band = r.next_band()?.expect("a band");
assert_eq!(band.index, index);
let at = index * band_bytes;
out[at..at + band.samples.len()].copy_from_slice(band.samples);
}
}
let damaged = readers.iter().any(Reader::damaged);
Ok((out, damaged))
}
struct Rng(u64);
impl Rng {
/// The next value of the xorshift* sequence.
fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
/// A value in `lo..=hi`.
fn range(&mut self, lo: i32, hi: i32) -> i32 {
lo + (self.next() % (hi - lo + 1) as u64) as i32
}
}
/// A test picture: gradients, edges and noise in every channel.
fn picture(w: usize, h: usize, channels: usize, seed: u64) -> Vec<u8> {
let mut rng = Rng(seed | 1);
let mut px = Vec::with_capacity(w * h * channels);
for y in 0..h {
for x in 0..w {
for c in 0..channels {
let base = match c {
0 => x * 255 / w.max(1),
1 => y * 255 / h.max(1),
2 => {
if (x / 5 + y / 3) % 2 == 0 {
40
} else {
220
}
}
_ => (x * 3 + y * 7) % 256,
};
px.push((base as i32 + rng.range(-20, 20)).clamp(0, 255) as u8);
}
}
}
px
}
/// A `w`×`h` picture encoded by `jpeg-encoder` at quality 85, with a restart interval of
/// `restart` MCUs (0 for none).
fn jpeg_encoder_stream(w: u16, h: u16, color: jpeg_encoder::ColorType, sampling: jpeg_encoder::SamplingFactor, restart: u16) -> Vec<u8> {
let channels = match color {
jpeg_encoder::ColorType::Luma => 1,
jpeg_encoder::ColorType::Rgb => 3,
_ => 4,
};
let px = picture(usize::from(w), usize::from(h), channels, u64::from(w) * 31 + u64::from(h));
let mut out = Vec::new();
let mut enc = jpeg_encoder::Encoder::new(&mut out, 85);
enc.set_sampling_factor(sampling);
if restart > 0 {
enc.set_restart_interval(restart);
}
enc.encode(&px, w, h, color).unwrap();
out
}
// --- A test-only baseline Huffman encoder (T.81 F.1.2), for layouts other encoders don't write ---
/// A component of [`Stream`]: identifier, sampling factors and quantization table.
#[derive(Clone, Copy)]
struct C {
id: u8,
h: usize,
v: usize,
tq: u8,
}
/// A sequential Huffman stream with one interleaved scan (or a single component), random
/// coefficients and canonical code tables in which every symbol has the same length.
struct Stream {
w: usize,
h: usize,
comps: Vec<C>,
restart: usize,
seed: u64,
/// Number the restart markers from this instead of 0.
rst_base: u8,
/// Instead of random coefficients, blocks with only a DC term: every sample of component
/// `i` reconstructs to `flat[i]`.
flat: Option<[u8; 4]>,
}
/// The AC symbols a baseline scan uses (F.1.2.2): EOB, ZRL and run/size 0..15/1..10.
fn ac_symbols() -> Vec<u8> {
let mut v = vec![0x00, 0xF0];
for r in 0..16u8 {
for s in 1..=10u8 {
v.push(r << 4 | s);
}
}
v
}
struct BitWriter {
out: Vec<u8>,
acc: u64,
n: u32,
}
impl BitWriter {
/// Appends the `len` low bits of `v`, stuffing a 0 byte after every `FF` (F.1.2.3).
fn put(&mut self, v: u32, len: u32) {
if len == 0 {
return;
}
self.acc = (self.acc << len) | (u64::from(v) & ((1u64 << len) - 1));
self.n += len;
while self.n >= 8 {
let b = (self.acc >> (self.n - 8)) as u8;
self.out.push(b);
if b == 0xFF {
self.out.push(0);
}
self.n -= 8;
}
}
/// Pads with 1 bits to a byte boundary (F.1.2.3).
fn flush(&mut self) {
let pad = (8 - self.n % 8) % 8;
if pad > 0 {
self.put((1 << pad) - 1, pad);
}
self.acc = 0;
}
}
/// The size category of a coefficient or DC difference (F.1.2.1): its magnitude's bit length.
fn category(v: i32) -> u32 {
32 - v.unsigned_abs().leading_zeros()
}
/// The `s` low bits that code `v` of category `s` (F.1.2.1).
fn magnitude(v: i32, s: u32) -> u32 {
let v = if v < 0 { v - 1 } else { v };
(v as u32) & ((1u32 << s) - 1)
}
impl Stream {
/// A `w`×`h` stream whose components have the given (H, V) sampling factors; the first uses
/// quantization table 0, the others table 1.
fn new(w: usize, h: usize, comps: &[(usize, usize)], restart: usize, seed: u64) -> Self {
let comps = comps.iter().enumerate().map(|(i, &(h, v))| C { id: i as u8 + 1, h, v, tq: (i > 0) as u8 }).collect();
Stream { w, h, comps, restart, seed, rst_base: 0, flat: None }
}
/// Quantization table `t` (zigzag order): `Q0` is `1 + t`.
fn quant(t: u8) -> [u8; 64] {
std::array::from_fn(|k| 1 + (k as u8) / 4 + t)
}
/// The JPEG file: SOI, the tables, the frame, one scan and EOI.
fn encode(&self) -> Vec<u8> {
let hmax = self.comps.iter().map(|c| c.h).max().unwrap();
let vmax = self.comps.iter().map(|c| c.v).max().unwrap();
let single = self.comps.len() == 1;
let (mcux, mcuy) = if single { (self.w.div_ceil(8), self.h.div_ceil(8)) } else { (self.w.div_ceil(8 * hmax), self.h.div_ceil(8 * vmax)) };
let mut rng = Rng(self.seed | 1);
let mut s = vec![0xFF, 0xD8];
let seg = |s: &mut Vec<u8>, m: u8, body: &[u8]| {
s.extend([0xFF, m]);
s.extend(((body.len() + 2) as u16).to_be_bytes());
s.extend(body);
};
for t in 0..2u8 {
let mut b = vec![t];
b.extend(Self::quant(t));
seg(&mut s, 0xDB, &b);
}
let mut sof = vec![8];
sof.extend((self.h as u16).to_be_bytes());
sof.extend((self.w as u16).to_be_bytes());
sof.push(self.comps.len() as u8);
for c in &self.comps {
sof.extend([c.id, (c.h << 4 | c.v) as u8, c.tq]);
}
seg(&mut s, 0xC0, &sof);
// DC: 12 symbols of 4 bits; AC: 162 symbols of 8 bits.
let mut dht = vec![0x00];
dht.extend([0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
dht.extend(0..12u8);
let ac = ac_symbols();
dht.push(0x10);
dht.extend([0, 0, 0, 0, 0, 0, 0, ac.len() as u8, 0, 0, 0, 0, 0, 0, 0, 0]);
dht.extend(&ac);
seg(&mut s, 0xC4, &dht);
if self.restart > 0 {
seg(&mut s, 0xDD, &(self.restart as u16).to_be_bytes());
}
let mut sos = vec![self.comps.len() as u8];
for c in &self.comps {
sos.extend([c.id, 0x00]);
}
sos.extend([0, 63, 0]);
seg(&mut s, 0xDA, &sos);
let mut bw = BitWriter { out: s, acc: 0, n: 0 };
let mut pred = vec![0i32; self.comps.len()];
let total = mcux * mcuy;
let blocks_per_mcu: Vec<usize> = self.comps.iter().map(|c| if single { 1 } else { c.h * c.v }).collect();
for m in 0..total {
if self.restart > 0 && m > 0 && m % self.restart == 0 {
bw.flush();
let n = (m / self.restart - 1) as u8;
bw.out.extend([0xFF, 0xD0 + (n.wrapping_add(self.rst_base)) % 8]);
pred.iter_mut().for_each(|p| *p = 0);
}
for (ci, &n) in blocks_per_mcu.iter().enumerate() {
for _ in 0..n {
// A DC term d alone reconstructs to d × Q0 / 8 + 128 (A.3.3).
let dc = match self.flat {
Some(f) => (i32::from(f[ci]) - 128) * 8 / i32::from(Self::quant(self.comps[ci].tq)[0]),
None => rng.range(-300, 300),
};
let diff = dc - pred[ci];
pred[ci] = dc;
let t = category(diff);
bw.put(t, 4);
bw.put(magnitude(diff, t), t);
let mut run = 0;
let last = if self.flat.is_some() { 0 } else { rng.next() % 4 };
let last = match last {
0 => 0,
1 => 5,
2 => 20,
_ => 63,
};
for k in 1..64 {
let v = if k <= last && rng.next().is_multiple_of(3) { rng.range(-40, 40) / (1 + k / 8) } else { 0 };
if v == 0 {
run += 1;
continue;
}
while run > 15 {
bw.put(ac.iter().position(|&a| a == 0xF0).unwrap() as u32, 8);
run -= 16;
}
let sz = category(v);
let sym = (run << 4 | sz) as u8;
bw.put(ac.iter().position(|&a| a == sym).unwrap() as u32, 8);
bw.put(magnitude(v, sz), sz);
run = 0;
}
if run > 0 {
bw.put(0, 8);
}
}
}
}
bw.flush();
let mut out = bw.out;
out.extend([0xFF, 0xD9]);
out
}
}
// --- Tests ------------------------------------------------------------------------------------
/// Checks `stream` decodes in bands to exactly the whole-image decode's samples, in order and by
/// seeking. Returns the bands, or `None` when the stream isn't one bands decode.
fn check_equal<'a>(stream: &'a [u8], what: &str) -> Option<Bands<'a>> {
let mut b = match Bands::new(stream, &Limits::default(), adobe(stream)) {
Ok(b) => b,
Err(Error::Unsupported(_)) => return None,
Err(e) => panic!("{what}: {e}"),
};
let reference = whole(stream).unwrap_or_else(|e| panic!("{what}: {e}"));
assert_eq!((b.width(), b.height(), b.components()), (reference.width, reference.height, reference.components), "{what}");
let (seq, damaged) = in_order(&b).unwrap();
assert!(!damaged, "{what}");
assert!(seq == eight(&reference), "{what}: bands differ from the whole-image decode");
b.find_seek_points();
let (sought, damaged) = by_seeking(&b).unwrap();
assert!(!damaged, "{what}");
assert!(sought == seq, "{what}: seeking changes the samples");
Some(b)
}
#[test]
fn bands_equal_the_whole_image_decode() {
use jpeg_encoder::{ColorType, SamplingFactor};
let mut checked = 0;
for color in [ColorType::Luma, ColorType::Rgb, ColorType::Cmyk, ColorType::CmykAsYcck] {
for sampling in [SamplingFactor::R_4_4_4, SamplingFactor::R_4_2_2, SamplingFactor::R_4_2_0, SamplingFactor::R_4_4_0] {
for (w, h) in [(37u16, 29u16), (1, 1), (64, 64), (100, 9), (9, 70)] {
let plain = jpeg_encoder_stream(w, h, color, sampling, 0);
let what = format!("{color:?} {sampling:?} {w}x{h}");
let Some(b) = check_equal(&plain, &what) else { panic!("{what}: not decoded in bands") };
let row = b.mcux as u16;
for restart in [1, 7, row, 2 * row] {
let s = jpeg_encoder_stream(w, h, color, sampling, restart);
let b = check_equal(&s, &format!("{what} restart {restart}")).unwrap();
if restart % row == 0 {
// Every band (or every other one) begins a restart interval.
let expect = b.bands().div_ceil(usize::from(restart / row));
assert_eq!(b.seek_points().len(), expect, "{what} restart {restart}");
}
checked += 1;
}
}
}
}
assert_eq!(checked, 4 * 4 * 5 * 4);
}
/// Interleaved layouts `jpeg-encoder` doesn't write (factors of 3 and 4 across and down), from
/// the test encoder, with and without restart intervals.
#[test]
fn interleaved_layouts_with_any_sampling_factors() {
let layouts: [&[(usize, usize)]; 8] = [
&[(4, 1), (1, 1), (1, 1)],
&[(4, 2), (1, 1), (1, 1)],
&[(1, 4), (1, 1), (1, 1)],
&[(2, 4), (1, 2), (1, 2)],
&[(3, 1), (1, 1), (1, 1)],
&[(1, 3), (1, 1), (1, 1)],
&[(2, 2), (1, 1), (1, 1), (2, 2)],
&[(2, 2)],
];
for (n, comps) in layouts.iter().enumerate() {
for (w, h) in [(53, 47), (8, 8), (200, 33)] {
let plain = Stream::new(w, h, comps, 0, n as u64 * 7 + w as u64).encode();
let what = format!("{comps:?} {w}x{h}");
let b = check_equal(&plain, &what).expect("decoded in bands");
let down = comps.len() > 1 && comps.iter().any(|c| c.1 < comps.iter().map(|c| c.1).max().unwrap_or(1));
assert_eq!(b.halo(), u32::from(down), "{what}");
let row = b.mcux;
for restart in [1, 5, row, 3 * row] {
let s = Stream { restart, ..Stream::new(w, h, comps, 0, n as u64 * 7 + w as u64) }.encode();
let b = check_equal(&s, &format!("{what} restart {restart}")).unwrap();
if restart % row == 0 {
assert_eq!(b.seek_points().len(), b.bands().div_ceil(restart / row), "{what} restart {restart}");
}
}
}
}
}
/// The test encoder's streams decode as `zune-jpeg` decodes them, within the difference between
/// its integer inverse transform and upsampling and these (mean under 1.5, as for
/// `decode`).
#[test]
fn bands_match_zune_jpeg() {
use jpeg_encoder::{ColorType, SamplingFactor};
for sampling in [SamplingFactor::R_4_4_4, SamplingFactor::R_4_2_0, SamplingFactor::R_4_2_2] {
let s = jpeg_encoder_stream(91, 67, ColorType::Rgb, sampling, 4);
let b = Bands::new(&s, &Limits::default(), None).unwrap();
let (ours, _) = in_order(&b).unwrap();
let mut dec = zune_jpeg::JpegDecoder::new(zune_jpeg::zune_core::bytestream::ZCursor::new(&s));
let zune = dec.decode().unwrap();
assert_eq!(ours.len(), zune.len());
let diffs: Vec<u8> = ours.iter().zip(&zune).map(|(a, b)| a.abs_diff(*b)).collect();
let mean = diffs.iter().map(|&d| f32::from(d)).sum::<f32>() / diffs.len() as f32;
let max = diffs.iter().copied().max().unwrap();
let (bound_mean, bound_max) = if sampling == SamplingFactor::R_4_4_4 { (0.75, 4) } else { (1.5, 255) };
assert!(mean < bound_mean && max <= bound_max, "{sampling:?}: mean {mean}, max {max}");
}
}
/// Without restart intervals, damaged data leaves the rest of the scan at zero, exactly as the
/// whole-image decode does. Data that fails in the first MCU, which the whole-image decode
/// refuses, is a damaged (grey) image here.
#[test]
fn damage_without_restarts_matches_the_whole_image_decode() {
let s = Stream::new(61, 45, &[(2, 2), (1, 1), (1, 1)], 0, 3).encode();
let sos = s.windows(2).rposition(|w| w == [0xFF, 0xDA]).unwrap();
let data = sos + 2 + 12;
let mut cuts: Vec<usize> = (data..s.len()).step_by(37).collect();
cuts.extend([data, data + 1, s.len() - 2]);
for cut in cuts {
let t = &s[..cut];
let reference = decode(t, &Limits::default(), None);
let ours = Bands::new(t, &Limits::default(), None).and_then(|b| in_order(&b));
let (o, damaged) = ours.unwrap_or_else(|e| panic!("cut at {cut}: {e}"));
match reference {
Ok(r) => {
assert!(o == eight(&r), "cut at {cut}");
assert_eq!(damaged, r.damaged, "cut at {cut}");
}
Err(_) => assert!(damaged, "cut at {cut}"),
}
}
// Corrupt data in the middle of the scan.
let mut t = s.clone();
for b in &mut t[data + 200..data + 230] {
*b = 0xFE;
}
let r = decode(&t, &Limits::default(), None).unwrap();
let (o, damaged) = in_order(&Bands::new(&t, &Limits::default(), None).unwrap()).unwrap();
assert!(r.damaged && damaged);
assert!(o == eight(&r));
}
/// With restart intervals, damage stays inside its interval: the next one decodes as in the
/// undamaged file, whether read in order or by seeking.
#[test]
fn damage_with_restarts_resumes_at_the_next_interval() {
let (w, h) = (80, 96);
let s = Stream { restart: 10, ..Stream::new(w, h, &[(2, 2), (1, 1), (1, 1)], 0, 9) }.encode();
let clean = in_order(&Bands::new(&s, &Limits::default(), None).unwrap()).unwrap().0;
// 4:2:0, 5 MCUs a line: each interval of 10 MCUs is two bands of 16 rows. Corrupt the
// second interval (rows 32..64) with bytes no code begins with.
let markers: Vec<usize> = s.windows(2).enumerate().filter(|(_, p)| p[0] == 0xFF && (0xD0..=0xD7).contains(&p[1])).map(|(i, _)| i).collect();
assert_eq!(markers.len(), 2);
let mut t = s.clone();
for b in &mut t[markers[0] + 20..markers[0] + 40] {
*b = 0xFE;
}
let mut b = Bands::new(&t, &Limits::default(), None).unwrap();
let (seq, damaged) = in_order(&b).unwrap();
assert!(damaged);
b.find_seek_points();
assert_eq!(b.seek_points().collect::<Vec<_>>(), [0, 2, 4]);
let (sought, damaged) = by_seeking(&b).unwrap();
assert!(damaged);
assert!(sought == seq);
// Row 31 interpolates chroma with the damaged interval's first row and row 64 with its last;
// the rows before and after decode as in the undamaged file.
let row = w * 3;
assert!(seq[..31 * row] == clean[..31 * row]);
assert!(seq[32 * row..64 * row] != clean[32 * row..64 * row]);
assert!(seq[65 * row..] == clean[65 * row..]);
// The whole-image decode stops at the damage instead: the same up to it.
let reference = decode(&t, &Limits::default(), None).unwrap();
assert!(reference.damaged);
assert!(seq[..31 * row] == eight(&reference)[..31 * row]);
assert!(seq[65 * row..] != eight(&reference)[65 * row..]);
// The second interval's data cut short (its last bytes gone, the marker after it kept): it
// runs out of data, and the third still decodes as in the undamaged file.
let mut t = s.clone();
t.drain(markers[1] - 30..markers[1]);
let mut b = Bands::new(&t, &Limits::default(), None).unwrap();
let (seq, damaged) = in_order(&b).unwrap();
assert!(damaged);
b.find_seek_points();
assert_eq!(b.seek_points().len(), 3);
assert!(by_seeking(&b).unwrap().0 == seq);
assert!(seq[..31 * row] == clean[..31 * row]);
assert!(seq[65 * row..] == clean[65 * row..]);
}
/// Data that fails in the first MCU damages only its interval: with restarts the rest of the
/// image decodes as in the undamaged file.
#[test]
fn a_bad_first_interval_is_damage_not_an_error() {
let (w, h) = (80, 96);
let s = Stream { restart: 10, ..Stream::new(w, h, &[(2, 2), (1, 1), (1, 1)], 0, 9) }.encode();
let clean = in_order(&Bands::new(&s, &Limits::default(), None).unwrap()).unwrap().0;
let sos = s.windows(2).rposition(|p| p == [0xFF, 0xDA]).unwrap();
let mut t = s.clone();
for b in &mut t[sos + 14..sos + 30] {
*b = 0xFE;
}
assert!(decode(&t, &Limits::default(), None).is_err(), "the whole-image decode refuses it");
let mut b = Bands::new(&t, &Limits::default(), None).unwrap();
let (seq, damaged) = in_order(&b).unwrap();
assert!(damaged);
b.find_seek_points();
assert!(by_seeking(&b).unwrap().0 == seq);
let row = w * 3;
assert!(seq[33 * row..] == clean[33 * row..]);
}
/// Bytes added to an interval, or moved within it, leave more than its padding before the
/// restart marker: the image is damaged, whether or not a code failed, read in order or by
/// seeking.
#[test]
fn data_left_before_a_restart_marker_is_damage() {
let (w, h) = (64, 64);
let s = Stream { restart: 4, ..Stream::new(w, h, &[(1, 1), (1, 1), (1, 1)], 0, 21) }.encode();
let markers: Vec<usize> = s.windows(2).enumerate().filter(|(_, p)| p[0] == 0xFF && (0xD0..=0xD7).contains(&p[1])).map(|(i, _)| i).collect();
// Two bytes before the third marker; a stuffed FF 00 before the fifth.
for insert in [&[0x12u8, 0x34][..], &[0xFF, 0x00]] {
for k in [2, 4] {
let mut t = s.clone();
t.splice(markers[k]..markers[k], insert.iter().copied());
let mut b = Bands::new(&t, &Limits::default(), None).unwrap();
let (seq, damaged) = in_order(&b).unwrap();
assert!(damaged, "{insert:?} before marker {k}");
b.find_seek_points();
let (sought, hurt) = by_seeking(&b).unwrap();
assert!(hurt && sought == seq, "{insert:?} before marker {k}");
}
}
// Fill bytes before a marker are not data.
let mut t = s.clone();
t.splice(markers[2]..markers[2], [0xFF, 0xFF]);
assert!(!in_order(&Bands::new(&t, &Limits::default(), None).unwrap()).unwrap().1);
}
/// Bytes added after the last MCU leave more than its padding before EOI: damaged, with restart
/// intervals or without, read in order or by seeking, with the clean stream's samples. Fill
/// bytes there are not data.
#[test]
fn data_left_before_eoi_is_damage() {
for restart in [0, 4] {
let s = Stream { restart, ..Stream::new(64, 64, &[(2, 2), (1, 1), (1, 1)], 0, 23) }.encode();
let eoi = s.len() - 2;
assert_eq!(s[eoi..], [0xFF, 0xD9]);
let (clean, hurt) = in_order(&Bands::new(&s, &Limits::default(), None).unwrap()).unwrap();
assert!(!hurt, "restart {restart}");
for (insert, damaged) in [(&[0x12u8, 0x34][..], true), (&[0xFF, 0x00], true), (&[0xFF, 0xFF], false)] {
let mut t = s.clone();
t.splice(eoi..eoi, insert.iter().copied());
let mut b = Bands::new(&t, &Limits::default(), None).unwrap();
let (seq, hurt) = in_order(&b).unwrap();
assert_eq!(hurt, damaged, "{insert:?} before EOI, restart {restart}");
// The added bytes follow the last MCU's codes: the samples are the clean stream's.
assert!(seq == clean, "{insert:?} before EOI, restart {restart}");
b.find_seek_points();
let (sought, hurt) = by_seeking(&b).unwrap();
assert!(hurt == damaged && sought == seq, "{insert:?} before EOI, restart {restart}");
}
}
}
/// Restart markers misnumbered, missing (the file cut short), added inside an interval or
/// removed: seek points follow the markers in the data as a reader in order meets them, so
/// reading from them gives the samples reading in order does.
#[test]
fn damaged_restart_markers_seek_as_reading_in_order_does() {
let base = Stream { restart: 4, ..Stream::new(32, 40, &[(1, 1)], 0, 5) };
let good = base.encode();
let mut b = Bands::new(&good, &Limits::default(), None).unwrap();
b.find_seek_points();
assert_eq!(b.seek_points().len(), 5);
let markers: Vec<usize> = good.windows(2).enumerate().filter(|(_, p)| p[0] == 0xFF && (0xD0..=0xD7).contains(&p[1])).map(|(i, _)| i).collect();
let misnumbered = Stream { rst_base: 1, ..base }.encode();
let cut = good[..markers[2] + 10].to_vec();
let mut added = good.clone();
added.splice(markers[1] - 7..markers[1] - 7, [0xFF, 0xD5]);
let mut removed = good.clone();
removed.drain(markers[2]..markers[2] + 2);
for (what, t, points, damaged) in [
("misnumbered", misnumbered, vec![0, 1, 2, 3, 4], false),
("cut", cut, vec![0, 1, 2, 3], true),
("added", added, vec![0, 1, 2, 3, 4], true),
("removed", removed, vec![0, 1, 2, 3], true),
] {
let mut b = Bands::new(&t, &Limits::default(), None).unwrap();
let (seq, hurt) = in_order(&b).unwrap();
assert_eq!(hurt, damaged, "{what}");
b.find_seek_points();
assert_eq!(b.seek_points().collect::<Vec<_>>(), points, "{what}");
let (sought, hurt) = by_seeking(&b).unwrap();
assert_eq!(hurt, damaged, "{what}");
assert!(sought == seq, "{what}: seeking changes the samples");
}
}
#[test]
fn other_processes_and_split_scans_are_unsupported() {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/");
for f in
["tests/data/testorig12.jpg", "tests/data/testimgari.jpg", "tests/data/testorig-ari-progressive.jpg", "fuzz/corpus/decode/sof1-12bit.jpg"]
{
let b = std::fs::read(format!("{dir}{f}")).unwrap();
assert!(matches!(Bands::new(&b, &Limits::default(), None), Err(Error::Unsupported(_))), "{f}");
}
let mut progressive = Vec::new();
let mut enc = jpeg_encoder::Encoder::new(&mut progressive, 80);
enc.set_progressive(true);
enc.encode(&picture(16, 16, 3, 1), 16, 16, jpeg_encoder::ColorType::Rgb).unwrap();
assert!(matches!(Bands::new(&progressive, &Limits::default(), None), Err(Error::Unsupported(_))));
// jpeg-encoder writes each component in its own scan for factors of 4.
let split = jpeg_encoder_stream(32, 32, jpeg_encoder::ColorType::Rgb, jpeg_encoder::SamplingFactor::R_4_1_1, 0);
assert_eq!(split.windows(2).filter(|w| *w == [0xFF, 0xDA]).count(), 3);
assert!(matches!(Bands::new(&split, &Limits::default(), None), Err(Error::Unsupported(_))));
}
/// A scan that names a component twice breaks T.81 (B.2.3: its components follow the frame's
/// order): malformed, not a valid feature left to the whole-image decode.
#[test]
fn a_component_named_twice_in_the_scan_is_malformed() {
let mut s = Stream::new(16, 16, &[(1, 1), (1, 1), (1, 1)], 0, 3).encode();
let sos = s.windows(2).position(|w| w == [0xFF, 0xDA]).unwrap();
assert_eq!(s[sos + 4], 3);
s[sos + 7] = s[sos + 5];
assert!(matches!(Bands::new(&s, &Limits::default(), None), Err(Error::Malformed(_))));
}
#[test]
fn limits_are_checked_from_the_headers() {
let s = Stream::new(40, 30, &[(2, 1), (1, 1), (1, 1)], 0, 2).encode();
let b = Bands::new(&s, &Limits::default(), None).unwrap();
// The chroma's column maps: 40 entries of 24 bytes each.
let need = b.reader_bytes() + 2 * 40 * 24;
for (max_pixels, max_alloc, ok) in [(1200, need, true), (1199, need, false), (1200, need - 1, false)] {
let limits = Limits { max_pixels, max_alloc };
assert_eq!(Bands::new(&s, &limits, None).is_ok(), ok, "{max_pixels} {max_alloc}");
}
// A frame of 65535² under the default limits: refused before anything is sized from it.
let mut big = s.clone();
let sof = big.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap();
big[sof + 5..sof + 9].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
assert!(matches!(Bands::new(&big, &Limits::default(), None), Err(Error::LimitExceeded(_))));
let wide = Limits { max_pixels: u64::MAX, ..Limits::default() };
let b = Bands::new(&big, &wide, None).unwrap();
assert!(b.reader_bytes() < 64 << 20);
}
#[test]
fn readers_seek_only_to_seek_points() {
let s = Stream { restart: 5, ..Stream::new(40, 40, &[(1, 1), (1, 1), (1, 1)], 0, 4) }.encode();
let mut b = Bands::new(&s, &Limits::default(), None).unwrap();
assert!(b.reader().seek(1).is_err());
b.find_seek_points();
assert_eq!(b.seek_points().collect::<Vec<_>>(), [0, 1, 2, 3, 4]);
let mut r = b.reader();
assert!(r.seek(5).is_err());
r.seek(4).unwrap();
assert_eq!(r.next_band().unwrap().map(|band| band.index), Some(4));
assert!(r.next_band().unwrap().is_none());
}
/// CMYK and YCCK samples of known ink, checked against T.81's reconstruction rather than
/// against the whole-image decode (both renderers share the colour conversion). Adobe CMYK is
/// stored inverted (255 = no ink); YCCK's C, M and Y come out of YCbCr as ink, and K inverted.
#[test]
fn four_component_samples_are_ink_amounts() {
let comps = [(1, 1), (1, 1), (1, 1), (1, 1)];
// YCCK with neutral chroma gives C, M and Y equal to the luma. With Cb 150 and Cr 110 (T.871):
// C = 100 + 1.402 × −18 = 74.76, M = 100 − 0.344136 × 22 − 0.714136 × −18 = 105.28 and
// Y = 100 + 1.772 × 22 = 138.98.
for (transform, stored, ink) in
[(0, [228, 128, 28, 178], [27, 127, 227, 77]), (2, [100, 128, 128, 178], [100, 100, 100, 77]), (2, [100, 150, 110, 178], [75, 105, 139, 77])]
{
let s = Stream { flat: Some(stored), ..Stream::new(16, 16, &comps, 0, 1) }.encode();
let b = Bands::new(&s, &Limits::default(), Some(transform)).unwrap();
let (seq, damaged) = in_order(&b).unwrap();
assert!(!damaged);
assert!(seq.as_chunks::<4>().0.iter().all(|px| *px == ink), "transform {transform}: {:?}", &seq[..4]);
let whole = decode(&s, &Limits::default(), Some(transform)).unwrap();
assert!(eight(&whole) == seq, "transform {transform}");
}
}
+238 -86
View File
@@ -13,6 +13,10 @@
//! SOF11) and hierarchical (SOF5–SOF7, SOF13–SOF15) files, and DCT precisions other than 8 and
//! 12 bits.
//!
//! [`Bands`] decodes the common case, a sequential Huffman-coded 8-bit frame in one scan, a band
//! of rows at a time, into the samples [`decode`] would give for those rows, so that images too
//! large for one buffer can be decoded straight into the caller's storage.
//!
//! An app that also uses a baseline decoder routes each file with [`frame`], which reports the
//! first frame marker and its precision without decoding. For example, to send this crate the
//! arithmetic-coded files and the Huffman-coded ones that aren't 8-bit:
@@ -197,11 +201,18 @@ struct Huffman {
valptr: [i32; 17],
mincode: [i32; 17],
values: Vec<u8>,
/// For each value of the next [`LOOKAHEAD`] bits: `length << 8 | value` of the code they
/// begin with, or 0 when it is longer (or not a code). Built from the procedure of
/// [`Huffman::decode`] itself, so both give the same symbol.
fast: Vec<u16>,
}
/// Bits [`Huffman::fast`] resolves at once; nearly every code in real tables is this short.
const LOOKAHEAD: u32 = 9;
impl Huffman {
fn new(counts: &[u8], values: &[u8]) -> Result<Self, Error> {
let mut h = Huffman { maxcode: [-1; 17], valptr: [0; 17], mincode: [0; 17], values: values.to_vec() };
let mut h = Huffman { maxcode: [-1; 17], valptr: [0; 17], mincode: [0; 17], values: values.to_vec(), fast: Vec::new() };
let mut code: i32 = 0;
let mut k: i32 = 0;
for (l, &n) in (1..=16usize).zip(counts) {
@@ -218,10 +229,29 @@ impl Huffman {
}
code <<= 1;
}
h.fast = (0..1u32 << LOOKAHEAD)
.map(|bits| {
let mut code: i32 = 0;
for l in 1..=LOOKAHEAD as usize {
code = (code << 1) | ((bits >> (LOOKAHEAD as usize - l)) & 1) as i32;
if code <= h.maxcode[l] {
let value = usize::try_from(h.valptr[l] + code - h.mincode[l]).ok().and_then(|i| h.values.get(i));
return value.map_or(0, |&v| (l as u16) << 8 | u16::from(v));
}
}
0
})
.collect();
Ok(h)
}
fn decode(&self, bits: &mut Bits) -> Result<u8, Error> {
if let Some(&e) = self.fast.get(bits.peek(LOOKAHEAD) as usize)
&& e != 0
{
bits.get(u32::from(e >> 8));
return Ok(e as u8);
}
let mut code: i32 = 0;
for l in 1..=16 {
code = (code << 1) | bits.bit() as i32;
@@ -362,7 +392,32 @@ fn parse_dac(seg: &[u8], t: &mut Tables) -> Result<(), Error> {
Ok(())
}
fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result<Frame, Error> {
/// A frame header (B.2.2), validated, before any buffer is sized from it.
struct Header {
arithmetic: bool,
progressive: bool,
precision: u8,
width: usize,
height: usize,
hmax: usize,
vmax: usize,
/// MCUs per line and MCU lines of an interleaved scan (A.2.3).
mcux: usize,
mcuy: usize,
/// Identifier, sampling factors (H, V) and quantization table of each component.
comps: Vec<(u8, usize, usize, usize)>,
}
impl Header {
/// Samples of a component with sampling factors `h`, `v` (B.2.2: ⌈X·H/Hmax⌉ × ⌈Y·V/Vmax⌉).
fn samples(&self, h: usize, v: usize) -> (usize, usize) {
((self.width * h).div_ceil(self.hmax), (self.height * v).div_ceil(self.vmax))
}
}
/// The frame header (B.2.2) of a SOF segment: its size, precision and components, and the MCU
/// grid they make. Sizes no image buffer, so callers can check limits first.
fn parse_header(marker: u8, seg: &[u8]) -> Result<Header, Error> {
let (arithmetic, progressive) = match marker {
0xC0 | 0xC1 => (false, false),
0xC9 => (true, false),
@@ -396,16 +451,24 @@ fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result<Frame, Error>
}
let hmax = raw.iter().map(|c| c.1).max().unwrap_or(1);
let vmax = raw.iter().map(|c| c.2).max().unwrap_or(1);
let bytes_per_pixel = nf as u64 * if precision > 8 { 2 } else { 1 };
limits.check(width, height, bytes_per_pixel)?;
let mcux = width.div_ceil(8 * hmax);
let mcuy = height.div_ceil(8 * vmax);
Ok(Header { arithmetic, progressive, precision, width, height, hmax, vmax, mcux, mcuy, comps: raw })
}
/// The frame of a whole-image decode: the header checked against `limits`, with a coefficient
/// buffer for every block of every component.
fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result<Frame, Error> {
let hd = parse_header(marker, seg)?;
let (width, height, mcux, mcuy) = (hd.width, hd.height, hd.mcux, hd.mcuy);
let bytes_per_pixel = hd.comps.len() as u64 * if hd.precision > 8 { 2 } else { 1 };
limits.check(width, height, bytes_per_pixel)?;
// `render` holds the coefficients (4 bytes) and the reconstructed plane (4 bytes) of every
// padded sample, and a column and a row map for upsampling each component, while it writes
// the output, so all of them count against `max_alloc` together.
let mut total = (width as u64 * height as u64).saturating_mul(bytes_per_pixel);
let maps = (width as u64 + height as u64).saturating_mul(size_of::<UpsampleStep>() as u64);
for &(_, h, v, _) in &raw {
for &(_, h, v, _) in &hd.comps {
let samples = (mcux * h * 8) as u64 * (mcuy * v * 8) as u64;
total = total.saturating_add(samples.saturating_mul(8)).saturating_add(maps);
}
@@ -415,23 +478,14 @@ fn parse_frame(marker: u8, seg: &[u8], limits: &Limits) -> Result<Frame, Error>
if total > cap {
return Err(Error::LimitExceeded(format!("{total} bytes of decoder buffers and output exceed the limit of {cap}")));
}
let mut comps = Vec::with_capacity(nf);
for (id, h, v, tq) in raw {
let mut comps = Vec::with_capacity(hd.comps.len());
for &(id, h, v, tq) in &hd.comps {
let (bw, bh) = (mcux * h, mcuy * v);
let blocks = bw.checked_mul(bh).ok_or_else(|| Error::LimitExceeded("too many blocks".into()))?;
comps.push(Component {
id,
h,
v,
tq,
cw: (width * h).div_ceil(hmax),
ch: (height * v).div_ceil(vmax),
bw,
bh,
coefs: vec![[0; 64]; blocks],
quant: None,
});
let (cw, ch) = hd.samples(h, v);
comps.push(Component { id, h, v, tq, cw, ch, bw, bh, coefs: vec![[0; 64]; blocks], quant: None });
}
let Header { arithmetic, progressive, precision, hmax, vmax, .. } = hd;
Ok(Frame { arithmetic, progressive, precision, width, height, hmax, vmax, mcux, mcuy, comps })
}
@@ -529,6 +583,28 @@ impl<'a> Bits<'a> {
self.get(1)
}
/// Whether whole bytes of entropy-coded data are left before the next marker: more than the
/// padding (under 8 bits, F.1.2.3) that ends a scan or a restart interval.
fn data_left(&self) -> bool {
if self.n.saturating_sub(self.pad) >= 8 {
return true;
}
let mut p = self.pos;
while self.d.get(p) == Some(&0xFF) {
p += 1;
}
// A data byte, or a stuffed `FF 00` (fill bytes before a marker are not data).
self.d.get(p).is_some_and(|&v| p == self.pos || v == 0x00)
}
/// The next `n` (≤ 16) bits, left in place.
fn peek(&mut self, n: u32) -> u32 {
if self.n < n {
self.fill();
}
self.acc >> (32 - n)
}
/// The next `n` (≤ 16) bits.
fn get(&mut self, n: u32) -> u32 {
if n == 0 {
@@ -547,11 +623,14 @@ impl<'a> Bits<'a> {
v
}
/// Past the next restart marker, with nothing buffered: a fresh start, including after an
/// interval whose data ran out (the band decoder resumes there; [`decode`] stops at it).
fn restart(&mut self) {
self.pos = skip_restart(self.d, self.pos).0;
self.acc = 0;
self.n = 0;
self.pad = 0;
self.exhausted = false;
}
}
@@ -1018,7 +1097,8 @@ fn walk_scan(frame: &mut Frame, scan: &Scan, restart: usize, dec: &mut dyn ScanD
Ok(())
}
fn parse_scan(seg: &[u8], frame: &Frame) -> Result<Scan, Error> {
/// A scan header (B.2.3) of a frame whose components have the identifiers `ids`.
fn parse_scan(seg: &[u8], ids: &[u8], progressive: bool) -> Result<Scan, Error> {
let ns = usize::from(byte(seg, 0)?);
if !(1..=4).contains(&ns) {
return Err(err("bad number of scan components"));
@@ -1026,7 +1106,7 @@ fn parse_scan(seg: &[u8], frame: &Frame) -> Result<Scan, Error> {
let mut comps = Vec::with_capacity(ns);
for s in 0..ns {
let (cs, t) = (byte(seg, 1 + 2 * s)?, byte(seg, 2 + 2 * s)?);
let ci = frame.comps.iter().position(|c| c.id == cs).ok_or_else(|| err("scan names an unknown component"))?;
let ci = ids.iter().position(|&id| id == cs).ok_or_else(|| err("scan names an unknown component"))?;
let (td, ta) = (usize::from(t >> 4), usize::from(t & 15));
if td > 3 || ta > 3 {
return Err(err("bad entropy table id"));
@@ -1036,7 +1116,7 @@ fn parse_scan(seg: &[u8], frame: &Frame) -> Result<Scan, Error> {
let at = 1 + 2 * ns;
let (ss, se, a) = (usize::from(byte(seg, at)?), usize::from(byte(seg, at + 1)?), byte(seg, at + 2)?);
let (ah, al) = (a >> 4, a & 15);
if frame.progressive {
if progressive {
let ok = ss <= se && se <= 63 && (ss != 0 || se == 0) && (ss == 0 || ns == 1) && al <= 13;
if !ok {
return Err(err("bad progressive scan parameters"));
@@ -1156,7 +1236,8 @@ fn decode_coefficients(b: &[u8], limits: &Limits) -> Result<(Frame, Tables, bool
}
0xDA => {
let f = frame.as_mut().ok_or_else(|| err("scan before the frame header"))?;
let scan = parse_scan(seg, f)?;
let ids: Vec<u8> = f.comps.iter().map(|c| c.id).collect();
let scan = parse_scan(seg, &ids, f.progressive)?;
let budget = f.comps.iter().map(|c| c.coefs.len()).sum::<usize>().saturating_mul(SCAN_PASSES);
blocks = blocks.saturating_add(scan_grid(f, &scan)?.2);
if blocks > budget {
@@ -1187,8 +1268,14 @@ fn decode_coefficients(b: &[u8], limits: &Limits) -> Result<(Frame, Tables, bool
// ---------------------------------------------------------------------------------------------
// Reconstruction
/// `T[x][u] = C(u)/2 · cos((2x+1)uπ/16)`: one dimension of the inverse DCT (A.3.3).
fn idct_basis() -> [[f32; 8]; 8] {
/// One dimension of the inverse DCT (A.3.3): `t[x][u] = C(u)/2 · cos((2x+1)uπ/16)`, and its
/// transpose.
struct Idct {
t: [[f32; 8]; 8],
tt: [[f32; 8]; 8],
}
fn idct_basis() -> Idct {
let mut t = [[0f32; 8]; 8];
for (x, row) in t.iter_mut().enumerate() {
for (u, v) in row.iter_mut().enumerate() {
@@ -1196,49 +1283,86 @@ fn idct_basis() -> [[f32; 8]; 8] {
*v = c / 2.0 * (((2 * x + 1) * u) as f32 * std::f32::consts::PI / 16.0).cos();
}
}
t
let mut tt = [[0f32; 8]; 8];
for (x, row) in t.iter().enumerate() {
for (u, &v) in row.iter().enumerate() {
tt[u][x] = v;
}
}
Idct { t, tt }
}
/// One block's samples (A.3.3): `blk` (zigzag order) dequantized with `q`, inverse transformed,
/// level-shifted and clamped to `0..=max`, written to the 8 rows of 8 at the start of `out`
/// that are `stride` apart. Rows that `out` doesn't reach are left out.
fn idct_block(blk: &[i32; 64], q: &[u16; 64], idct: &Idct, max: f32, out: &mut [f32], stride: usize) {
let (basis, tt) = (&idct.t, &idct.tt);
let shift = (max + 1.0) / 2.0;
if blk[1..].iter().all(|&c| c == 0) {
// Only DC: every sample is T[y][0] · T[x][0] · F[0][0], the same products the full
// transform below sums (its other terms are zero), so the result is identical.
let t = basis[0][0];
let v = (t * (t * (blk[0] as f32 * f32::from(q[0]))) + shift).clamp(0.0, max);
for y in 0..8 {
if let Some(row) = out.get_mut(y * stride..y * stride + 8) {
row.fill(v);
}
}
return;
}
// F[v][u], dequantized, and which rows v hold a coefficient.
let mut f = [[0f32; 8]; 8];
let mut rows = 0u8;
for (k, (&c, &z)) in blk.iter().zip(&ZIGZAG).enumerate() {
if c != 0 {
f[z / 8][z % 8] = c as f32 * f32::from(q[k]);
rows |= 1 << (z / 8);
}
}
// Rows: r[v][x] = Σu T[x][u] F[v][u]; then columns: s[y][x] = Σv T[y][v] r[v][x]. Each sum is
// taken in order of u (of v), eight values of x at a time; a zero term changes no sum but the
// sign of a zero, which the level shift then removes.
let mut r = [[0f32; 8]; 8];
for (v, (fv, rv)) in f.iter().zip(r.iter_mut()).enumerate() {
if rows & (1 << v) == 0 {
continue;
}
let mut acc = [-0.0f32; 8];
for (&fu, tu) in fv.iter().zip(tt) {
if fu != 0.0 {
for (a, &t) in acc.iter_mut().zip(tu) {
*a += t * fu;
}
}
}
*rv = acc;
}
for (y, ty) in basis.iter().enumerate() {
let mut acc = [-0.0f32; 8];
for (v, (&t, rv)) in ty.iter().zip(&r).enumerate() {
if rows & (1 << v) != 0 {
for (a, &rx) in acc.iter_mut().zip(rv) {
*a += t * rx;
}
}
}
if let Some(row) = out.get_mut(y * stride..y * stride + 8) {
for (o, a) in row.iter_mut().zip(acc) {
*o = (a + shift).clamp(0.0, max);
}
}
}
}
/// The component's samples (padded to whole blocks, row stride `bw * 8`), level-shifted and
/// clamped to `0..=max`.
fn reconstruct(comp: &Component, q: &[u16; 64], basis: &[[f32; 8]; 8], max: f32) -> Vec<f32> {
fn reconstruct(comp: &Component, q: &[u16; 64], idct: &Idct, max: f32) -> Vec<f32> {
let stride = comp.bw * 8;
let mut plane = vec![0f32; stride * comp.bh * 8];
let shift = (max + 1.0) / 2.0;
for (n, blk) in comp.coefs.iter().enumerate() {
let (bx, by) = (n % comp.bw, n / comp.bw);
if blk[1..].iter().all(|&c| c == 0) {
// Only DC: every sample is T[y][0] · T[x][0] · F[0][0], the same products the full
// transform below sums (its other terms are zero), so the result is identical.
let t = basis[0][0];
let v = (t * (t * (blk[0] as f32 * f32::from(q[0]))) + shift).clamp(0.0, max);
for y in 0..8 {
let row = (by * 8 + y) * stride + bx * 8;
if let Some(out) = plane.get_mut(row..row + 8) {
out.fill(v);
}
}
continue;
}
let mut f = [0f32; 64];
for (k, &c) in blk.iter().enumerate() {
f[ZIGZAG[k]] = c as f32 * f32::from(q[k]);
}
// Rows: r[v][x] = Σu T[x][u] F[v][u]; then columns: s[y][x] = Σv T[y][v] r[v][x].
let mut r = [0f32; 64];
for v in 0..8 {
for x in 0..8 {
r[v * 8 + x] = (0..8).map(|u| basis[x][u] * f[v * 8 + u]).sum();
}
}
for (y, ty) in basis.iter().enumerate() {
let row = (by * 8 + y) * stride + bx * 8;
if let Some(out) = plane.get_mut(row..row + 8) {
for (x, o) in out.iter_mut().enumerate() {
let s: f32 = ty.iter().enumerate().map(|(v, t)| t * r[v * 8 + x]).sum();
*o = (s + shift).clamp(0.0, max);
}
}
if let Some(out) = plane.get_mut(by * 8 * stride + bx * 8..) {
idct_block(blk, q, idct, max, out, stride);
}
}
plane
@@ -1260,29 +1384,70 @@ fn upsample_map(out: usize, samples: usize, factor: usize, fmax: usize) -> Vec<U
.collect()
}
/// Whether a frame's components, with identifiers `ids`, hold YCbCr (or YCCK) to convert, as
/// [`Decoded::components`] describes: three components unless the Adobe transform is 0 or the
/// identifiers are `R`, `G`, `B`; four only with Adobe transform 2.
fn is_ycc(ids: impl Iterator<Item = u8>, adobe_transform: Option<u8>) -> bool {
let ids: Vec<u8> = ids.collect();
match ids.len() {
3 => adobe_transform != Some(0) && ids != *b"RGB",
4 => adobe_transform == Some(2),
_ => false,
}
}
/// YCbCr → RGB (JFIF / T.871) of samples centred on `center`; for YCCK the result is the
/// inverted CMY of Adobe's convention, i.e. the ink amounts.
#[inline]
fn ycc_to_rgb(yy: f32, cb: f32, cr: f32, center: f32) -> [f32; 3] {
let (cb, cr) = (cb - center, cr - center);
[yy + 1.402 * cr, yy - 0.344_136 * cb - 0.714_136 * cr, yy + 1.772 * cb]
}
/// The output colour of one pixel's `nc` reconstructed samples (`0..=max`), in place.
fn to_color(px: &mut [f32; 4], nc: usize, ycc: bool, max: f32) {
if ycc {
let [r, g, b] = ycc_to_rgb(px[0], px[1], px[2], (max + 1.0) / 2.0);
(px[0], px[1], px[2]) = (r, g, b);
if nc == 4 {
px[3] = max - px[3];
}
} else if nc == 4 {
// Adobe-style CMYK is stored inverted (max = no ink).
for p in px {
*p = max - *p;
}
}
}
/// An 8-bit output sample: `p` (finite) clamped to `0..=255` and rounded half away from zero.
#[inline]
fn to_u8(p: f32) -> u8 {
let p = p.clamp(0.0, 255.0);
// On `0..=255` the truncation and the fraction it leaves are exact, so this is `p.round()`
// without the library call `round` is on targets without SSE4.1.
let t = p as u8;
t.saturating_add(u8::from(p - f32::from(t) >= 0.5))
}
/// Turn the coefficients into interleaved output samples.
fn render(frame: Frame, tables: &Tables, adobe_transform: Option<u8>) -> Result<(u8, Samples), Error> {
let max = if frame.precision > 8 { 4095.0 } else { 255.0 };
let basis = idct_basis();
let idct = idct_basis();
let (w, h) = (frame.width, frame.height);
let mut planes = Vec::with_capacity(frame.comps.len());
for comp in &frame.comps {
// A component no scan reached has only zero coefficients; any defined table will do.
let q = comp.quant.or_else(|| tables.quant.get(comp.tq).copied().flatten());
let q = q.ok_or_else(|| err("component uses an undefined quantization table"))?;
let plane = reconstruct(comp, &q, &basis, max);
let plane = reconstruct(comp, &q, &idct, max);
let xs = upsample_map(w, comp.cw, comp.h, frame.hmax);
let ys = upsample_map(h, comp.ch, comp.v, frame.vmax);
planes.push((plane, comp.bw * 8, xs, ys));
}
let nc = planes.len();
let sixteen = frame.precision > 8;
let rgb_ids = frame.comps.iter().map(|c| c.id).eq(*b"RGB");
let ycc = match nc {
3 => adobe_transform != Some(0) && !rgb_ids,
4 => adobe_transform == Some(2),
_ => false,
};
let ycc = is_ycc(frame.comps.iter().map(|c| c.id), adobe_transform);
let (mut wide, mut narrow) = (Vec::new(), Vec::new());
if sixteen {
wide.reserve_exact(w * h * nc);
@@ -1290,7 +1455,6 @@ fn render(frame: Frame, tables: &Tables, adobe_transform: Option<u8>) -> Result<
narrow.reserve_exact(w * h * nc);
}
let mut px = [0f32; 4];
let center = (max + 1.0) / 2.0;
for y in 0..h {
for x in 0..w {
for (c, (plane, stride, xs, ys)) in planes.iter().enumerate() {
@@ -1304,28 +1468,13 @@ fn render(frame: Frame, tables: &Tables, adobe_transform: Option<u8>) -> Result<
*p = top + (bottom - top) * fy;
}
}
if ycc {
// YCbCr → RGB (JFIF / T.871); for YCCK the result is the inverted CMY of Adobe's
// convention, i.e. the ink amounts.
let (yy, cb, cr) = (px[0], px[1] - center, px[2] - center);
px[0] = yy + 1.402 * cr;
px[1] = yy - 0.344_136 * cb - 0.714_136 * cr;
px[2] = yy + 1.772 * cb;
if nc == 4 {
px[3] = max - px[3];
}
} else if nc == 4 {
// Adobe-style CMYK is stored inverted (max = no ink).
for p in &mut px {
*p = max - *p;
}
}
to_color(&mut px, nc, ycc, max);
for &p in px.iter().take(nc) {
let p = p.clamp(0.0, max);
if sixteen {
wide.push((p * 65535.0 / max).round() as u16);
} else {
narrow.push(p.round() as u8);
narrow.push(to_u8(p));
}
}
}
@@ -1349,5 +1498,8 @@ pub fn decode(bytes: &[u8], limits: &Limits, adobe_transform: Option<u8>) -> Res
Ok(Decoded { width, height, components, samples, damaged })
}
mod bands;
pub use bands::{Band, Bands, Reader};
#[cfg(test)]
mod tests;
+25
View File
@@ -126,3 +126,28 @@ fn twelve_bit_image_matches_libjpeg_turbo() {
// Observed: mean 0.50, max 2.46 (of 4095).
const MAX_MEAN_12: f64 = 0.75;
const MAX_ABS_12: f64 = 4.0;
/// Every band of each Huffman-coded 8-bit test image, read in order, joined.
fn bands_of(bytes: &[u8]) -> (Vec<u8>, bool) {
let bands = craft_jpeg::Bands::new(bytes, &Limits::default(), None).unwrap_or_else(|e| panic!("bands: {e}"));
let mut reader = bands.reader();
let mut out = Vec::new();
while let Some(band) = reader.next_band().unwrap_or_else(|e| panic!("band: {e}")) {
out.extend_from_slice(band.samples);
}
(out, reader.damaged())
}
/// The sequential Huffman-coded 8-bit test images (4:2:0 colour, gray, and gray from the
/// arithmetic white-tail test) decode in bands to exactly the whole-image decode's samples.
#[test]
fn baseline_images_decode_in_bands_as_whole() {
for name in ["testorig.jpg", "testimgari-huffman.jpg", "whitetail-huffman.jpg"] {
let bytes = fixture(name);
let whole = decode_clean(name);
let Samples::U8(px) = &whole.samples else { panic!("{name}: expected 8-bit samples") };
let (bands, damaged) = bands_of(&bytes);
assert!(!damaged, "{name}");
assert!(&bands == px, "{name}: bands differ from the whole-image decode");
}
}
+73 -1
View File
@@ -1,7 +1,7 @@
//! Deterministic mutation run over the test images and fuzz seeds: decoding corrupted input never panics and
//! stays within the caller's limits.
use craft_jpeg::{Limits, Samples, decode, frame};
use craft_jpeg::{Bands, Limits, Samples, decode, frame};
/// Inputs and mutation counts. The 227×149 test images take tens of milliseconds each to decode
/// in a debug build; the small fuzz seeds (SOF9 with restarts, 12-bit SOF9, SOF10, 12-bit SOF1)
@@ -129,3 +129,75 @@ fn mutated_streams_never_panic_and_stay_within_limits() {
// A mutation run that only produces errors would no longer reach the entropy decoders.
assert!(ok > 50 && errors > 50, "{ok} decoded, {errors} rejected");
}
/// Every band read in order, and (when the restart markers make more than band 0 a seek point)
/// read again from each seek point; `None` when `Bands::new` rejects the stream. Once it accepts
/// one, damage is reported, never an error: a failed read or seek fails the test.
fn read_bands(bytes: &[u8], limits: &Limits) -> Option<(Vec<u8>, Option<Vec<u8>>)> {
let mut bands = Bands::new(bytes, limits, None).ok()?;
assert!(bands.reader_bytes() <= limits.max_alloc);
assert!(u64::from(bands.width()) * u64::from(bands.height()) <= limits.max_pixels);
let mut reader = bands.reader();
let mut in_order = Vec::new();
while let Some(band) = reader.next_band().expect("a read after Bands::new accepted the stream") {
in_order.extend_from_slice(band.samples);
}
assert_eq!(in_order.len(), bands.width() as usize * bands.height() as usize * usize::from(bands.components()));
bands.find_seek_points();
let points: Vec<usize> = bands.seek_points().collect();
if points.len() < 2 {
return Some((in_order, None));
}
let mut sought = vec![0u8; in_order.len()];
let band_bytes = bands.band_bytes();
let mut reader = bands.reader();
for (k, &p) in points.iter().enumerate().rev() {
reader.seek(p).expect("a seek to an advertised seek point");
for _ in p..points.get(k + 1).copied().unwrap_or(bands.bands()) {
let band = reader.next_band().expect("a read after a seek").expect("a band after a seek");
let at = band.index * band_bytes;
sought[at..at + band.samples.len()].copy_from_slice(band.samples);
}
}
Some((in_order, Some(sought)))
}
/// A 4:2:0 baseline stream with a restart interval at every line of MCUs, from `jpeg-encoder`.
fn restart_stream() -> Vec<u8> {
let (w, h) = (48u16, 40u16);
let px: Vec<u8> = (0..usize::from(w) * usize::from(h)).flat_map(|i| [(i * 7) as u8, (i / 3) as u8, (i % 97) as u8]).collect();
let mut out = Vec::new();
let mut enc = jpeg_encoder::Encoder::new(&mut out, 80);
enc.set_sampling_factor(jpeg_encoder::SamplingFactor::R_4_2_0);
enc.set_restart_interval(3);
enc.encode(&px, w, h, jpeg_encoder::ColorType::Rgb).unwrap();
out
}
/// The band decoder on the same kind of mutations: it never panics, stays within the limits,
/// and wherever seek points survive, reading from them gives the samples reading in order does.
#[test]
fn mutated_streams_never_panic_in_bands() {
let mut rng = Rng(0xD1B5_4A32_D192_ED03);
let limits = test_limits();
let fixture = |name: &str| std::fs::read(format!("{}/{name}", env!("CARGO_MANIFEST_DIR"))).unwrap();
let inputs = [(fixture("tests/data/testorig.jpg"), 40), (fixture("tests/data/testimgari-huffman.jpg"), 40), (restart_stream(), 400)];
let (mut ok, mut errors, mut sought) = (0, 0, 0);
for (original, cases) in &inputs {
assert!(read_bands(original, &limits).is_some());
for case in 0..*cases {
let bytes = mutate(&mut rng, original);
match read_bands(&bytes, &limits) {
Some((in_order, Some(by_seeking))) => {
assert!(in_order == by_seeking, "case {case}: seeking changes the samples");
sought += 1;
ok += 1;
}
Some(_) => ok += 1,
None => errors += 1,
}
}
}
eprintln!("{ok} decoded ({sought} also by seeking), {errors} rejected");
assert!(ok > 50 && errors > 20 && sought > 50, "{ok} decoded ({sought} by seeking), {errors} rejected");
}