Other
- JFIF extension APP0 (JFXX) thumbnail view (T.871 §10.2-10.5)
- SOF11 four-component (CMYK-class) lossless arithmetic encode
- SOF11 three-component (RGB-class) lossless arithmetic encode (T.81 §H.1.2.3)
- lossless arithmetic (SOF11) grayscale encode via T.81 §H.1.2.3
- DNL (Define Number of Lines) support for SOF Y = 0 (T.81 §B.2.5)
- progressive arithmetic JPEG (SOF10) via T.81 §G.1.3 scan procedures
- lossless arithmetic JPEG (SOF11) via T.81 §H.1.2.3 statistical model
- skip ICC-fixture inspect test when docs/ absent (CI fix)
- typed APP2 ICC_PROFILE chunks view (T.872 / Annex L) on JpegInfo
- typed Adobe APP14 view (T.872 §6.5.3) on JpegInfo
- typed JFIF APP0 view (T.871 §10.1) on JpegInfo
- drop release-plz.toml — use release-plz defaults across the workspace
- progressive (SOF2) single-component grayscale encode
- decode-free JPEG SOF discriminator + metadata inspector
- baseline (SOF0) packed-Rgb24 lossy encode + decoder RGB tag
- baseline (SOF0) single-component Gray8 lossy encode
- four-component lossless (SOF3, P=8) round trip
- restart-interval-aligned scan splitting for the packetizer
- criterion harness for encode + decode hot paths
- scrub decorative external-implementation attribution
- add arith_decode cargo-fuzz target for SOF9 Q-coder surface
- gate cmyk_roundtrip on the
registryfeature - promote 4-component CMYK / YCCK helpers to the public API
Added
-
JFIF extension APP0 (JFXX) inspector view (T.871 §10.2-10.5) — the
decode-free inspector now surfaces the JFIF extension APP0 segment
(identifier"JFXX\0") that conformant writers use to carry a
thumbnail (the JFIF APP0's own inline thumbnail is rarely populated —
most files emit a JFXX segment instead). A newJfxxApp0typed view
onJpegInfo::jfxxreports the thumbnail-storage variant via a
JfxxThumbnailenum exhaustive over the threeextension_codebytes
T.871 §10.2 defines:JpegEncoded { jpeg_len }(0x10, §10.3 — an
embedded baseline JPEG, length reported without recursion),
PaletteRgb { width, height }(0x11, §10.4 — a 768-byte palette +
one-byte indices), andRgb24 { width, height }(0x13, §10.5 —
packed 24-bit RGB, same layout as the §10.1 inline thumbnail).
JfxxThumbnail::extension_code()recovers the literal byte for
re-serialisation. A top-levelparse_jfxx_app0(payload) -> Result<JfxxApp0>validator is exported for callers that already hold
the APP0 payload bytes; it enforces the structural invariants
(identifier"JFXX\0", definedextension_code, non-zero thumbnail
dimensions for0x11/0x13, declared body fits the payload) and
never copies the thumbnail body.inspect_jpegpopulates the view
automatically when a JFXX extension APP0 follows the JFIF APP0; the
extension segment carries no colour-convention signal so it does not
influencecolor_hint(independent ofjfif, which keeps reporting
the leading JFIF segment). Standalone surface — noregistryfeature,
nooxideav-coredep. Eleven new tests cover the three storage
variants, the short-payload / bad-identifier / reserved-code /
zero-dimension / body-overflow rejection paths, the JFIF+JFXX
dual-APP0 inspector aggregation, and the no-JFXX baseline. -
Lossless arithmetic (SOF11) four-component (CMYK-class) encode (T.81
Annex H + §H.1.2.3) —encoder::encode_lossless_arith_jpeg_cmyk(width, height, planes, strides, predictor, adobe_transform)and its
_with_opts(..., restart_interval, point_transform)companion emit a
standalone four-component interleaved SOF11 (lossless, arithmetic-coded)
JPEG atP = 8. This is the Q-coder counterpart of the existing Huffman
encode_lossless_jpeg_cmykand the four-component extension of
encode_lossless_arith_jpeg_rgb: each component is modelled independently
(§H.1.2) with its ownLosslessStatsarea andL_Context(Da, Db)/
X1_Context(Db)difference history (§H.1.2.3.2), while a single
arithmetic-coded segment carries one residual per component per pixel
position in scan-component order (each component declaredH_i = V_i = 1).
The Adobe APP14 colour-transform flag is honoured identically to the
Huffman CMYK helper —None(no APP14, plain CMYK),Some(0)(Adobe
CMYK, samples inverted on the wire),Some(2)(Adobe YCCK, K inverted) —
with the segment emitted before SOF11 so the decoder's existing
four-component un-inversion / YCCK → CMYK path applies on output. Output
decodes to packedPixelFormat::Cmyk(4 bytes/pixel), bit-exact on the
no-APP14 / Adobe-CMYK paths for every predictor, the half-modulus
Di = 32768case (§H.1.2.2), non-zero point transforms, and
restart-interval emission (eachRSTnboundary flushes the Q-coder,
cyclesRST0..=RST7modulo 8, and re-seeds every component's statistical
model + difference history + predictor to2^(P − Pt − 1), §H.1.1 /
§H.1.2.3.4). The matching SOF11 four-component decode path already
existed. Covered by six new round-trips intests/lossless_roundtrip.rs. -
Lossless arithmetic (SOF11) three-component encode (T.81 Annex H +
§H.1.2.3) —encoder::encode_lossless_arith_jpeg_rgb(width, height, planes, strides, precision, predictor)and its
_with_opts(..., restart_interval, point_transform)companion emit a
standalone three-component interleaved SOF11 (lossless, arithmetic-coded)
JPEG. This is the Q-coder counterpart of the existing Huffman
encode_lossless_jpeg_rgband the multi-component extension of the
single-componentencode_lossless_arith_jpeg_grayscale: each component is
modelled independently (§H.1.2) with its ownLosslessStatsarea and its
ownL_Context(Da, Db)/X1_Context(Db)difference history (§H.1.2.3.2),
while a single arithmetic-coded entropy segment carries one residual per
component per pixel position in scan-component order (each component
declaredH_i = V_i = 1, so a lossless MCU is one pixel). The Annex H
Table H.1 predictors1..=7are applied per component over that
component's ownRa/Rb/Rc; no DAC segment is emitted so the
decoder uses the default conditioning bounds(L, U) = (0, 1)
(§H.1.2.3.3). Output is bit-exact for every precisionP ∈ 2..=16
(decode mapsP = 8→ packedRgb24,P ∈ {10, 12, 14}→ planar
Gbrp*Le, every otherP→ packedRgb48Le), every predictor, the
half-modulusDi = 32768case (§H.1.2.2), non-zero point transforms, and
restart-interval emission (eachRSTnboundary flushes the Q-coder,
byte-aligns, cyclesRST0..=RST7modulo 8, and re-seeds every component's
statistical model + difference history + predictor to the scan-origin
default2^(P − Pt − 1), §H.1.1 / §H.1.2.3.4). The matching SOF11
multi-component decode path already existed. Covered by six new
round-trips intests/lossless_roundtrip.rs. -
Lossless arithmetic (SOF11) grayscale encode (T.81 Annex H + §H.1.2.3) —
encoder::encode_lossless_arith_jpeg_grayscale(width, height, samples, stride, precision, predictor)and its
_with_opts(..., restart_interval, point_transform)companion emit a
standalone single-component SOF11 (lossless, arithmetic-coded) JPEG.
The spatial model reuses the Annex H Table H.1 predictors1..=7over
Ra/Rb/Rc, but each prediction difference is coded with the
Q-coder arithmetic statistical model of §H.1.2.3 (Table H.3 —
L_Context(Da, Db)/X1_Context(Db)conditioning over neighbouring
differences) rather than a Huffman magnitude category. No DAC segment
is emitted, so the decoder applies the default conditioning bounds
(L, U) = (0, 1)per §H.1.2.3.3. Output is bit-exact for every
precisionP ∈ 2..=16, every predictor, the half-modulus
Di = 32768case (§H.1.2.2), non-zero point transforms, and
restart-interval emission (eachRSTnboundary flushes the Q-coder,
byte-aligns, cyclesRST0..=RST7modulo 8, and re-seeds the
statistical model + difference history + predictor to the scan-origin
default2^(P − Pt − 1), §H.1.1 / §H.1.2.3.4). This is the
encoder-side counterpart to the existing SOF11 decode path and the
first arithmetic-coded entry point on the encoder side. Covered by
six new round-trips intests/lossless_roundtrip.rs. -
DNL (Define Number of Lines) decode support (T.81 §B.2.2 / §B.2.5) —
JPEG frames may code the number of linesY = 0in the SOF header, in
which case the real line count is supplied by a mandatory DNL segment
(0xFFDC) immediately after the first scan. The decoder now performs an
up-front marker-stream pre-pass (resolve_dnl_height) that, when it
seesY = 0, walks to the first scan, readsNLfrom the following
DNL segment, and patches the frame height before any scan decoder runs —
so every path (baseline fast path, sequential / progressive / arithmetic
accumulators, lossless) decodes at the correct height with no per-path
changes. AY = 0stream with no following DNL is rejected (the segment
is mandatory there), as is a malformed DNL carryingNL = 0
(Table B.10 constrainsNL ∈ 1..=65535). Newparse_dnlparser entry,
markers::DNLconstant, and explicit DNL handling in the main marker
loop. Covered bytests/dnl.rs(YUV 4:4:4 / 4:2:2 / 4:2:0 round-trips,
a non-MCU-aligned height, plus the two negative cases) and four
decoder::dnl_unit_testsunit tests. -
Progressive arithmetic JPEG (SOF10) decode — the SOF2 multi-scan
spectral-selection / successive-approximation structure with the
Annex D Q-coder as the entropy layer, per T.81 §G.1.3:- DC first scans (
Ss = Se = 0,Ah = 0) reuse the sequential
§F.1.4.1 DC statistical model on the point-transformed values
(DC point transform = arithmetic shift right); the decoded
difference accumulates into the per-component prediction and lands
left-shifted byAl(§G.1.3.1). - DC refinement scans (
Ah > 0) decode one binary decision per
block with the fixed 0.5 probability estimate (Qe = 0x5A1D,
MPS = 0, non-adapting) and OR the bit into the existing DC value
at bit positionAl. - AC first scans (
Ss > 0,Ah = 0) run the §F.1.4 sequential AC
procedure withKmin = Ssand the EOB decision meaning
end-of-band (§G.1.3.2); decoded values land left-shifted by
Al. The DAC marker'sKxconditioning is honoured (default 5). - AC refinement scans (
Ah > 0) follow the §G.1.3.3 coding model
(Figures G.10 / G.11) under the Table G.2 statistics layout — a
new 189-binjpeg::arith::AcRefineStatsarea withSE / S0 / SC
bins per coefficient index, the end-of-band decision bypassed
whileK < EOBx(recovered from the coefficient history), newly
nonzero coefficients signed by the fixed estimate, and correction
bits growing existing magnitudes by2^Al
(jpeg::arith::decode_ac_refine). - Restart intervals re-initialise the coder, the statistics areas
and the DC predictions at everyRSTn, in every scan kind. - Same frame constraints and output shaping as the Huffman
progressive (SOF2) path:P = 8andP = 12(Annex G processes
4 and 8), 1- and 3-component plus 4-component CMYK / YCCK at
P = 8(Adobe APP14 transform flag honoured), shared coefficient
accumulator + EOI render. - Round-trip tests drive every scan kind from an encoder-side
mirror of the §G.1.3 procedures (spectral selection only, full
progression, two successive-approximation levels, interleaved
4:2:0 DC, restart intervals, DACKxoverride, 12-bit, and
4-component CMYK), comparing the decoded pixels sample-exact
against a direct IDCT of the source coefficients.
- DC first scans (
-
SofKind::is_supported_by_decodernow reportsProgressiveArith
(SOF10) andLosslessArith(SOF11) as supported — the SOF11 decode
path landed previously but the inspector helper had not been
updated alongside it. -
Lossless arithmetic JPEG (SOF11) decode — the Annex H predictor
coding model with the modulo-2^16 prediction differences
entropy-coded by the Annex D Q-coder under the T.81 §H.1.2.3
two-dimensional statistical model. Each binary decision is
conditioned on the classifications of the differences coded for the
sample to the left (Da) and the sample in the line above (Db) via
the 5 × 5L_Context(Da, Db)array of Figure H.2, with the
magnitude bins selected byX1_Context(Db)— 158 statistics bins
per scan component per §H.1.2.3.2 / Table H.3
(jpeg::arith::LosslessStats). The DAC marker's DC-conditioning
(L, U)bounds are honoured (defaults(0, 1)per §H.1.2.3.3;
small/zero boundary2^(L−1)exclusive, small/large boundary2^U
inclusive per F.1.4.4.1.2). Coverage matches the SOF3 Huffman path:
single-component grayscale and three-component RGB-class at every
precisionP ∈ 2..=16, four-component CMYK-class atP = 8, all
Table H.1 predictors, point transform, and restart intervals
(statistics + conditioning + prediction re-initialised at each RSTn
per §H.1.2.3.4 / §H.2.1, with the coder re-initialised past the
marker). Prediction follows §H.1.2.1: origin2^(P−Pt−1)at
scan/interval start, the 1-D horizontal predictor across the first
line of the scan and of each restart interval,Rbat the start
of every other line. The precision-driven output shaping is shared
with SOF3 via the extractedshape_lossless_framehelper, so the
pixel-format policy is identical (Gray8/Gray10Le/Gray12Le
/Gray16Le, packedRgb24/ planarGbrp*Le/ packedRgb48Le,
packedCmyk). -
Q-coder arithmetic encoder (
jpeg::arith::ArithEncoder) per
T.81 Annex D §D.1: Initenc (Figure D.12), Code_MPS / Code_LPS with
conditional MPS/LPS exchange (Figures D.3 / D.4), Renorm_e
(Figure D.7), Byte_out with carry resolution,0xFFstacking and
0xFF 0x00stuffing (Figures D.8–D.11), and the Flush /
Clear_final_bits / Discard_final_zeros termination sequence
(Figures D.13–D.15) — validated byte-exactly against the Annex K.4.1
256-bit test sequence (the encoder reproduces the spec's listed
compressed stream, stuffed byte included), plus
encode_lossless_diff/encode_magnitudemirrors of the Table H.3
decision tree used by the SOF11 round-trip tests. -
Eleven new tests: K.4.1 encode reproduction, multi-context
encode/decode self-consistency, lossless-diff round-trip across
default + DAC-overridden bounds, Figure H.2 context-base /
classification checks, and SOF11 decode round-trips (grayscale
P = 8across all seven predictors, grayscaleP = 16with
pseudorandom samples through the deep end of the magnitude tree,
three-component RGBP = 8, line-aligned restart intervals,
DAC(L = 2, U = 5)conditioning, non-zero point transform) plus a
SOF10 still-rejected guard. -
IccProfileChunksaggregated view of every APP2"ICC_PROFILE\0"
marker segment seen in the prefix (T.872 / Annex L of T.871; see
docs/image/jpeg/jpeg-fixtures-and-traces.md§3.11) on
JpegInfo::icc_profile. The summary reports the declared chunk
total(every segment must agree — mismatches drop the dissenting
chunks), the cumulativetotal_payload_lenof profile bytes seen
across the segments, and the per-segment(seq_no, payload_len)
ordering in source order, plusis_complete()returning true when
the chunks cover every sequence number1..=totalexactly once. A
borrowingIccProfileApp2Chunk<'a>(seq_no,total,
profile_bytes: &'a [u8]) plus a new top-level
parse_icc_profile_app2(payload) -> Result<IccProfileApp2Chunk<'_>>
validator are exported for callers that already hold the APP2 payload
bytes; the validator enforces the structural invariants (identifier
equals"ICC_PROFILE\0", payload ≥ 14 bytes,total ≥ 1,
1 ≤ seq_no ≤ total) and never allocates or copies the ICC body.
inspect_jpegpopulates the summary automatically; APP2 segments
whose identifier is not"ICC_PROFILE\0"(FPXR, IPTC-bearing APP2,
etc.) are silently ignored and APP2 does not influencecolor_hint
(the ICC profile is colour-management metadata, separate from the
YCbCr/RGB mapping signalled by APP0 JFIF / APP14 Adobe). Twelve new
tests cover the minimal one-chunk success path, body propagation,
payload-too-short / bad-identifier /total = 0/ zero-seq /
seq-above-total rejection, the inspector's aggregation across one
/ three / partial / duplicate / mismatched-total streams, the
non-ICC APP2 ignore path, the no-APP2 baseline, plus an integration
test against the docs corpus'with-icc-profile-embeddedGhostscript
sRGB fixture (one chunk oftotal_payload_len = 2576). -
markers::APP2 = 0xE2constant, the standard JPEG APPn byte that
carries the embedded ICC profile (in addition to the existing APP0
/ APP14 constants). -
AdobeApp14typed view of the Adobe APP14 marker segment (T.872
§6.5.3 / Adobe Technical Note 5116 §18) onJpegInfo::adobe.
Carries the rawdct_encode_versionu16(commonly100), the
two encoder-hint flag wordsflags_0andflags_1, and an
AdobeColorTransformenum (Unknown/YCbCr/Ycck,
exhaustive over the spec's three legaltransformbytes with
as_byte()for re-encoding), plusis_standard_version()(true
for the universally-used100) and anas_color_hint()
projection back to the inspector-levelColorHintenum. A new
top-levelparse_adobe_app14(payload) -> Result<AdobeApp14>
validator is exported for callers that already hold the APP14
payload bytes; it enforces the three structural invariants
(identifier == "Adobe", payload ≥ 12 bytes,transform ∈ {0, 1, 2}) and never allocates.inspect_jpegpopulates
JpegInfo.adobeautomatically when an APP14 carries a structurally
valid Adobe segment; reservedtransformbytes leave the typed
view asNonebut the inspector's coarseColorHintpath still
flips toAdobeUntransformedas before, since the colour-hint
signal is more tolerant by design. Independent of the JFIF view —
streams with both an APP0 JFIF and an APP14 Adobe populate both
typed views, and the colour hint continues to prefer Adobe when
both are present (existing inspector precedence). Eight new tests
cover the standard-version success path, the encoder-flag bits,
payload-too-short / bad-identifier / reserved-transform rejection,
the inspector's reserved-transform tolerance, the JFIF+Adobe
dual-segment case, and the only-first-segment-wins rule for
duplicate APP14s. -
JfifApp0typed view of the JFIF APP0 marker segment (T.871 §10.1)
onJpegInfo::jfif. Carries theversion_major/version_minor
bytes, aJfifUnitsenum (AspectRatio/DotsPerInch/
DotsPerCm, exhaustive per the spec's "shall be one of" wording
withas_byte()for re-encoding),h_density/v_density, and
thethumbnail_width/thumbnail_heightpair, plus
has_thumbnail(),thumbnail_payload_len(),version(),
pixel_aspect_ratio(), andh_density_dpi()/v_density_dpi()
unit-aware accessors that convertDotsPerCmto dots-per-inch via
integer(d × 254 + 50) / 100and returnNonefor the
aspect-ratio case where DPI has no meaning. A new top-level
parse_jfif_app0(payload) -> Result<JfifApp0>validator is
exported for callers that already hold the APP0 payload bytes; it
enforces the four T.871 §10.1 invariants (identifier == "JFIF\0",
units ∈ {0, 1, 2}, both densities non-zero, trailing
3 × Hthumb × Vthumbbytes fit in the payload) and never
allocates.inspect_jpegpopulatesJpegInfo.jfifautomatically
when the leading APP0 carries valid JFIF; structurally malformed
JFIF segments still flip the existingColorHint::JfifYCbCrhint
but leave the typed view asNone(the magic alone is a sufficient
colour-convention signal). Nine new tests cover the DPI / DPCM /
aspect-ratio variants, illegal-units / zero-density / truncated-
header / bad-identifier / thumbnail-overflow rejection paths, the
2×2-thumbnail success case, the only-first-segment-wins rule for
duplicate APP0s, the malformed-but-magic-present hint-still-set
case, and a JFIF-disjoint Adobe-only stream. -
encoder::encode_jpeg_progressive_grayscale(width, height, samples, stride, quality)emits a standalone progressive (SOF2)
single-component grayscale JPEG at 8-bit precision. T.81 §G.1.1
permits the progressive coding process at everyNf ∈ 1..=4; the
single-component case ships every block's DC and AC coefficients
across three spectral-selection scans with(Ss, Se) = (0, 0)/
(1, 5)/(6, 63), all atAh = 0, Al = 0. The bitstream layout
isSOI / JFIF APP0 / DQT (luma) / SOF2 (Nf = 1, H = V = 1, P = 8) / DHT (Annex K luma DC + AC) / SOS_DC / scan / SOS_AC_low / scan / SOS_AC_high / scan / EOI— one DQT, one DC + one AC DHT, no chroma
table, no DRI /RSTn. The companion variant
encode_jpeg_progressive_grayscale_with_meta(.., meta)replaces
the default JFIF APP0 with caller-supplied APP/COM segments
harvested via [extract_app_segments]. The trait-API encoder
(MjpegEncoder::send_frame) now routesGray8input +
set_progressive(true)through the new path;set_lossless(true)
continues to win over progressive (SOF3 lossless takes priority) and
set_restart_intervalis ignored on the progressive path (the
3-component progressive encoder doesn't expose DRI emission either,
kept consistent so the flag has the same meaning across every
progressive variant). Six new unit tests cover the SOF2 single-
component header walker (SOF2 + single DQT + luma DC+AC DHT only +
exactly three SOS scans at(0,0)/(1,5)/(6,63)), Q=100 ±4 LSB
near-lossless ceiling, Q=75 ≥30 dB PSNR floor, short-stride and
short-buffer rejection, and the_with_metaAPP1 pass-through; one
new integration test intests/roundtrip.rscovers the trait-API
routing (SOF2 present, SOF0 + SOF3 absent, round-trip ≥ 20 dB). -
inspect_jpeg(bytes) -> Result<JpegInfo>— decode-free typed
inspector. Walks the JPEG marker prefix (T.81 §B.1) up to the first
SOS and returns aJpegInfocarrying aSofKinddiscriminator
(Baseline / ExtendedSequential / Progressive / Lossless /
ExtendedSequentialArith / ProgressiveArith / LosslessArith /
HierarchicalDct / HierarchicalArith) plus precision / width / height /
per-component sampling and quant-table selectors, a
ChromaSubsamplingdiscriminator (4:4:4 / 4:2:2 / 4:2:0 / 4:1:1 /
GrayscaleOnly / Custom) derived from the SOF sampling factors per
T.81 §A.1.1, aColorHintfrom the APP0 JFIF (T.871) and APP14
Adobe (T.872 §6.5.3) tags, and therestart_intervalfrom any DRI
segment before SOS. No entropy decoding, no DCT, no allocation
proportional to the scan body — the walk is O(prefix-length) and
stops at the first SOS marker.SofKind::is_supported_by_decoder/
is_dct/is_arithmeticexpose the routing-relevant predicates so
callers can negotiate fallback without matching every variant by
hand. Standalone surface — built without theregistryCargo
feature, nooxideav-coredep. Exercised by 23 new unit tests in
src/jpeg/inspect.rscovering all SOFn variants + all chroma-
subsampling classes + APP0/APP14 colour-hint extraction +
malformed-input rejection (missing SOI, EOI before SOS, SOS before
SOF, malformed SOF length), plus 8 new integration tests in
tests/inspect.rsthat round-trip the inspector against the
in-tree encoder's baseline / progressive / lossless outputs at
multiple chroma subsamplings. -
encoder::encode_jpeg_rgb24(width, height, samples, stride, quality)
emits a standalone baseline (SOF0) three-component RGB JPEG at 8-bit
precision from a packed RGB triple buffer. Components are written with
IDs'R' / 'G' / 'B'(82 / 71 / 66), each declaredH = V = 1, and
all three bind the single luma quantiser table — the chroma table is
never emitted. An Adobe APP14 segment withtransform = 0flags the
stream as plain R/G/B for conformant decoders. The companion variants
encode_jpeg_rgb24_with_opts(.., restart_interval)and
encode_jpeg_rgb24_with_meta(.., restart_interval, meta)addDRI + RSTnemission (sameRST0..=RST7cycling and per-component predictor
reset the YUV / grayscale paths use, per T.81 §F.1.1.5.2) and APP /
COM pass-through respectively. The companion baseline decoder now
detects 3-component RGB via either the Adobe APP14transform = 0
flag or the'R'/'G'/'B'component-id triple and emits a single
packedPixelFormat::Rgb24plane (stride = width * 3) instead of
reinterpreting the planes as YCbCr. The matchingtests/docs_corpus.rs
helpers (infer_pix_fmt+flatten_frame) gain anRgb24branch so
thebaseline-rgb-32x32corpus fixture passes itsPsnrFloor
threshold without the planar-YUV reinterpretation fallback. The
registry-side trait API (MjpegEncoder::send_frame) accepts
PixelFormat::Rgb24input and routes it through the new baseline RGB
path;set_lossless(true)is ignored for RGB input (lossless mode
stays grayscale-only). Seven new unit tests cover the encoder shape
(SOF0 RGB header walker, APP14 transform=0 emission, single DQT, luma
DC + AC DHT only), Q=100 ±4 LSB near-lossless ceiling, Q=75 ≥30 dB
PSNR floor, short stride / short buffer rejection, DRI + RSTn
emission round-trip, and APP1 pass-through (with component-id
fallback signalling RGB to the decoder). Three new integration tests
intests/roundtrip.rscover the trait-API default-quality round
trip, the lossless-flag-ignored-on-RGB path, and the short-stride
rejection. -
encoder::encode_jpeg_grayscale(width, height, samples, stride, quality)
emits a standalone baseline (SOF0) single-component grayscale JPEG at
8-bit precision. The bitstream layout is the usualSOI / JFIF APP0 / DQT (one luma table, scaled by quality) / SOF0 (Nf=1, H=V=1, P=8) / DHT (Annex K luma DC + AC) / SOS (Ns=1) / scan / EOI. The companion
variantsencode_jpeg_grayscale_with_opts(..., restart_interval)and
encode_jpeg_grayscale_with_meta(..., restart_interval, meta)add DRIRSTnemission (same once-per-restart_interval-MCUs scheme the
YUV baseline path uses, with predictor reset andRST0..=RST7
cycling per T.81 §F.1.1.5.2) and APP/COM pass-through respectively.
Bitstreams round-trip through the existing SOF0 single-component
decode path: high quality stays within ±4 LSB on smooth content,
default quality (75) sits above 30 dB PSNR on synthetic gradients,
andQ = 100collapses to the all-1 luma quantiser so any residual
error comes from f32 DCT/IDCT rounding alone.
-
MjpegEncoder::send_frame(registry-side trait API) now accepts
PixelFormat::Gray8withoutset_lossless(true)and routes it to
the new baseline grayscale path.set_lossless(true)keeps the
bit-exact SOF3 path for the same input. Higher-precision grayscale
(Gray10Le/Gray12Le/Gray16Le) still requires
set_lossless(true)— the DCT path is 8-bit by spec — and a clear
Unsupportederror surfaces when callers forget. Three new
integration tests intests/roundtrip.rscover the trait-API
baseline path (default quality round-trip with PSNR floor), the
lossless-flag-still-bit-exact path, and the
high-bit-depth-without-lossless rejection. The existing
registry_encoder_gray8_without_lossless_flag_errorstest is
rewritten asregistry_encoder_gray8_without_lossless_flag_takes_baseline
to assert the new SOF0 emission shape, paired with a fresh
registry_encoder_gray12_without_lossless_flag_errorsto lock in
the high-bit-depth rejection contract.
Changed
- docs: scrub decorative external-implementation attribution from
src/encoder.rs(DEFAULT_QUALITY,encode_jpeg,encode_jpeg_progressive,
encode_jpeg_progressive_sa),src/jpeg/quant.rs(scale_for_quality),
andsrc/mjpeg_container.rs(DEFAULT_FRAME_RATE). Quality-factor scaling
is described against the Annex K Q=50 base tables; conformant-SOF2 round
trip phrased neutrally. - README: paraphrase the residual decorative external-implementation
attribution from the encoder + progressive sections so the JPEG
quality-factor scaling and SOF2 round-trip claim match the language used
insrc/.
Added
-
encoder::encode_lossless_jpeg_cmyk(width, height, [c, m, y, k], strides, predictor, adobe_transform)and its
encode_lossless_jpeg_cmyk_with_opts(.., restart_interval, point_transform)companion expose a public four-component lossless
(SOF3) encoder at 8-bit precision. The four planes share one DC Huffman
table and one predictor selector, all components are declared
H_i = V_i = 1per T.81 §H.1.2, and the Adobe APP14 colour-transform
flag is honoured identically to the lossy CMYK helpers (None→ no
APP14 / plain "regular" CMYK,Some(0)→ Adobe CMYK with on-the-wire
inversion,Some(2)→ Adobe YCCK with K-only on-the-wire inversion).
Output: a standalone SOF3 JPEG with one interleaved SOS scan. -
Decoder side: SOF3 now accepts a 4-component scan at
P = 8and the
shareddecode_lossless_scanpacks the resulting four sample planes
into aPixelFormat::CmykVideoFrame(4 bytes/pixelC M Y K).
The Adobe APP14 colour transform on the resulting frame is applied
identically to the existing lossy CMYK render: no APP14 passes the
four bytes through, transform=0 un-inverts the Adobe-CMYK convention,
and transform=2 (YCCK) decodes BT.601 YCbCr → RGB → CMY and
un-inverts K. Wider precisions (P > 8) on a 4-component SOF3 are
rejected withError::Unsupportedbecause the workspace
PixelFormatenum has no high-bit-depth CMYK variant. -
New
lossless_cmyk_*tests exercise predictor 1..=7 bit-exact
roundtrip with no APP14, Adobe-CMYK (transform = 0) bit-exact
roundtrip on a representative predictor sample, the DRI +RSTn
emission path on a width-not-evenly-divided restart interval, the
point-transform quantisation path (Pt = 2 / output equals input with
the low Pt bits cleared), and the invalid-predictor + invalid-
Adobe-transform-byte rejection paths. -
rtp::packetize_with_opts+rtp::PacketizeOptsadd restart-interval
-aligned scan splitting (opt-in via
PacketizeOpts::new(qmode).with_restart_align(true)) to the RFC 2435
packetizer. When the source JPEG carries DRI > 0 the aligned path walks
the entropy-coded scan forRSTnboundaries (T.81 §B.1.1.2 byte
stuffing respected), packs as many complete intervals per fragment as
the MTU allows, and writes a §3.1.7 Restart Marker header withF = L = 1plus the index of each fragment's first interval in the 14-bit
Restart Count (wrapping modulo0x3FFF, the value reserved for
whole-frame reassembly). A single oversize interval returns
MjpegError::Unsupportedinstead of falling back silently to byte
boundaries. When the source has no DRI the flag is a no-op and the
output equalsrtp::packetize(jpeg, max_payload, qmode). New tests
cover the interval walker (3-interval whole-scan walk + 0xFF-stuffed
intra-interval byte), the tight-MTU one-interval-per-packet path, the
loose-MTU multiple-intervals-per-packet path, the no-DRI fallthrough,
the oversize-interval rejection, the qtable-header carriage on the
first fragment, and a round-trip throughJpegDepacketizerthat
shows the reassembled scan preserves every sourceRSTnposition. -
New
benches/codec.rsCriterion harness (cargo bench -p oxideav-mjpeg --bench codec) measures the baseline SOF0 encode (4:2:0 256x256 q75,
4:4:4 64x64 q75), baseline SOF0 decode (4:2:0 256x256 q75 through
theDecodertrait), progressive SOF2 encode (4:2:0 64x64 q75), and
SOF3 lossless grayscale encode with predictors 1 (Ra) and 4
(Ra + Rb − Rc) at 256x256. Every fixture is built deterministically
in-bench from an xorshift32 seed plus a low-amplitude triangle-wave
gradient (so the entropy coder sees realistic run-length patterns
rather than degenerate random-noise worst cases) — no committed
payload files, nodocs/reads, no third-party library calls. Pinned
tocriterion = "0.5"to match the existing cross-codec
bench fleet (flac / tta / tiff / magicyuv / huffyuv / pcx / qoi). -
New
arith_decodecargo-fuzz target wraps fuzz-supplied bytes in a
minimal SOF9 (extended-sequential arithmetic-coded) JPEG envelope so
thesrc/jpeg/arith.rsQ-coder (ArithDecoder::new/Initdec/
Renorm_d/Byte_in/decode_dc_diff/decode_ac/
decode_magnitude) and thedecode_arith_scanper-component
statistics + restart bookkeeping execute on every iteration. A
fuzz-driven control nibble varies component count (1 vs 3), optional
DAC conditioning, optional DRI (restart interval = 1 MCU), the
luma sampling factor (4:4:4 vs 4:2:2), and the image dimension
(8..=64 px square). Bar is "no panic", same as the existing six
robustness targets infuzz/fuzz_targets/.