Skip to content

v0.1.8

Latest

Choose a tag to compare

@MagicalTux MagicalTux released this 15 Jun 05:07
· 4 commits to master since this release
117ae8f

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 registry feature
  • 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 new JfxxApp0 typed view
    on JpegInfo::jfxx reports the thumbnail-storage variant via a
    JfxxThumbnail enum exhaustive over the three extension_code bytes
    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), and Rgb24 { 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-level parse_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", defined extension_code, non-zero thumbnail
    dimensions for 0x11/0x13, declared body fits the payload) and
    never copies the thumbnail body. inspect_jpeg populates the view
    automatically when a JFXX extension APP0 follows the JFIF APP0; the
    extension segment carries no colour-convention signal so it does not
    influence color_hint (independent of jfif, which keeps reporting
    the leading JFIF segment). Standalone surface — no registry feature,
    no oxideav-core dep. 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 at P = 8. This is the Q-coder counterpart of the existing Huffman
    encode_lossless_jpeg_cmyk and the four-component extension of
    encode_lossless_arith_jpeg_rgb: each component is modelled independently
    (§H.1.2) with its own LosslessStats area and L_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 declared H_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 packed PixelFormat::Cmyk (4 bytes/pixel), bit-exact on the
    no-APP14 / Adobe-CMYK paths for every predictor, the half-modulus
    Di = 32768 case (§H.1.2.2), non-zero point transforms, and
    restart-interval emission (each RSTn boundary flushes the Q-coder,
    cycles RST0..=RST7 modulo 8, and re-seeds every component's statistical
    model + difference history + predictor to 2^(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 in tests/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_rgb and the multi-component extension of the
    single-component encode_lossless_arith_jpeg_grayscale: each component is
    modelled independently (§H.1.2) with its own LosslessStats area and its
    own L_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
    declared H_i = V_i = 1, so a lossless MCU is one pixel). The Annex H
    Table H.1 predictors 1..=7 are applied per component over that
    component's own Ra / 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 precision P ∈ 2..=16
    (decode maps P = 8 → packed Rgb24, P ∈ {10, 12, 14} → planar
    Gbrp*Le, every other P → packed Rgb48Le), every predictor, the
    half-modulus Di = 32768 case (§H.1.2.2), non-zero point transforms, and
    restart-interval emission (each RSTn boundary flushes the Q-coder,
    byte-aligns, cycles RST0..=RST7 modulo 8, and re-seeds every component's
    statistical model + difference history + predictor to the scan-origin
    default 2^(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 in tests/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 predictors 1..=7 over
    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
    precision P ∈ 2..=16, every predictor, the half-modulus
    Di = 32768 case (§H.1.2.2), non-zero point transforms, and
    restart-interval emission (each RSTn boundary flushes the Q-coder,
    byte-aligns, cycles RST0..=RST7 modulo 8, and re-seeds the
    statistical model + difference history + predictor to the scan-origin
    default 2^(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 in tests/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 lines Y = 0 in 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
    sees Y = 0, walks to the first scan, reads NL from 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. A Y = 0 stream with no following DNL is rejected (the segment
    is mandatory there), as is a malformed DNL carrying NL = 0
    (Table B.10 constrains NL ∈ 1..=65535). New parse_dnl parser entry,
    markers::DNL constant, and explicit DNL handling in the main marker
    loop. Covered by tests/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_tests unit 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 by Al (§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 position Al.
    • AC first scans (Ss > 0, Ah = 0) run the §F.1.4 sequential AC
      procedure with Kmin = Ss and the EOB decision meaning
      end-of-band (§G.1.3.2); decoded values land left-shifted by
      Al. The DAC marker's Kx conditioning 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-bin jpeg::arith::AcRefineStats area with SE / S0 / SC
      bins per coefficient index, the end-of-band decision bypassed
      while K < EOBx (recovered from the coefficient history), newly
      nonzero coefficients signed by the fixed estimate, and correction
      bits growing existing magnitudes by 2^Al
      (jpeg::arith::decode_ac_refine).
    • Restart intervals re-initialise the coder, the statistics areas
      and the DC predictions at every RSTn, in every scan kind.
    • Same frame constraints and output shaping as the Huffman
      progressive (SOF2) path: P = 8 and P = 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, DAC Kx override, 12-bit, and
      4-component CMYK), comparing the decoded pixels sample-exact
      against a direct IDCT of the source coefficients.
  • SofKind::is_supported_by_decoder now reports ProgressiveArith
    (SOF10) and LosslessArith (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 × 5 L_Context(Da, Db) array of Figure H.2, with the
    magnitude bins selected by X1_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 boundary 2^(L−1) exclusive, small/large boundary 2^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
    precision P ∈ 2..=16, four-component CMYK-class at P = 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: origin 2^(P−Pt−1) at
    scan/interval start, the 1-D horizontal predictor across the first
    line of the scan and of each restart interval, Rb at the start
    of every other line. The precision-driven output shaping is shared
    with SOF3 via the extracted shape_lossless_frame helper, so the
    pixel-format policy is identical (Gray8 / Gray10Le / Gray12Le
    / Gray16Le, packed Rgb24 / planar Gbrp*Le / packed Rgb48Le,
    packed Cmyk).

  • 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, 0xFF stacking and
    0xFF 0x00 stuffing (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_magnitude mirrors 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 = 8 across all seven predictors, grayscale P = 16 with
    pseudorandom samples through the deep end of the magnitude tree,
    three-component RGB P = 8, line-aligned restart intervals,
    DAC (L = 2, U = 5) conditioning, non-zero point transform) plus a
    SOF10 still-rejected guard.

  • IccProfileChunks aggregated 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 cumulative total_payload_len of profile bytes seen
    across the segments, and the per-segment (seq_no, payload_len)
    ordering in source order, plus is_complete() returning true when
    the chunks cover every sequence number 1..=total exactly once. A
    borrowing IccProfileApp2Chunk<'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_jpeg populates the summary automatically; APP2 segments
    whose identifier is not "ICC_PROFILE\0" (FPXR, IPTC-bearing APP2,
    etc.) are silently ignored and APP2 does not influence color_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-embedded Ghostscript
    sRGB fixture (one chunk of total_payload_len = 2576).

  • markers::APP2 = 0xE2 constant, the standard JPEG APPn byte that
    carries the embedded ICC profile (in addition to the existing APP0
    / APP14 constants).

  • AdobeApp14 typed view of the Adobe APP14 marker segment (T.872
    §6.5.3 / Adobe Technical Note 5116 §18) on JpegInfo::adobe.
    Carries the raw dct_encode_version u16 (commonly 100), the
    two encoder-hint flag words flags_0 and flags_1, and an
    AdobeColorTransform enum (Unknown / YCbCr / Ycck,
    exhaustive over the spec's three legal transform bytes with
    as_byte() for re-encoding), plus is_standard_version() (true
    for the universally-used 100) and an as_color_hint()
    projection back to the inspector-level ColorHint enum. A new
    top-level parse_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_jpeg populates
    JpegInfo.adobe automatically when an APP14 carries a structurally
    valid Adobe segment; reserved transform bytes leave the typed
    view as None but the inspector's coarse ColorHint path still
    flips to AdobeUntransformed as 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.

  • JfifApp0 typed view of the JFIF APP0 marker segment (T.871 §10.1)
    on JpegInfo::jfif. Carries the version_major / version_minor
    bytes, a JfifUnits enum (AspectRatio / DotsPerInch /
    DotsPerCm, exhaustive per the spec's "shall be one of" wording
    with as_byte() for re-encoding), h_density / v_density, and
    the thumbnail_width / thumbnail_height pair, plus
    has_thumbnail(), thumbnail_payload_len(), version(),
    pixel_aspect_ratio(), and h_density_dpi() / v_density_dpi()
    unit-aware accessors that convert DotsPerCm to dots-per-inch via
    integer (d × 254 + 50) / 100 and return None for 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 × Vthumb bytes fit in the payload) and never
    allocates. inspect_jpeg populates JpegInfo.jfif automatically
    when the leading APP0 carries valid JFIF; structurally malformed
    JFIF segments still flip the existing ColorHint::JfifYCbCr hint
    but leave the typed view as None (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 every Nf ∈ 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 at Ah = 0, Al = 0. The bitstream layout
    is SOI / 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 routes Gray8 input +
    set_progressive(true) through the new path; set_lossless(true)
    continues to win over progressive (SOF3 lossless takes priority) and
    set_restart_interval is 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_meta APP1 pass-through; one
    new integration test in tests/roundtrip.rs covers 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 a JpegInfo carrying a SofKind discriminator
    (Baseline / ExtendedSequential / Progressive / Lossless /
    ExtendedSequentialArith / ProgressiveArith / LosslessArith /
    HierarchicalDct / HierarchicalArith) plus precision / width / height /
    per-component sampling and quant-table selectors, a
    ChromaSubsampling discriminator (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, a ColorHint from the APP0 JFIF (T.871) and APP14
    Adobe (T.872 §6.5.3) tags, and the restart_interval from 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_arithmetic expose the routing-relevant predicates so
    callers can negotiate fallback without matching every variant by
    hand. Standalone surface — built without the registry Cargo
    feature, no oxideav-core dep. Exercised by 23 new unit tests in
    src/jpeg/inspect.rs covering 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.rs that 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 declared H = V = 1, and
    all three bind the single luma quantiser table — the chroma table is
    never emitted. An Adobe APP14 segment with transform = 0 flags 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) add DRI + RSTn emission (same RST0..=RST7 cycling 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 APP14 transform = 0
    flag or the 'R'/'G'/'B' component-id triple and emits a single
    packed PixelFormat::Rgb24 plane (stride = width * 3) instead of
    reinterpreting the planes as YCbCr. The matching tests/docs_corpus.rs
    helpers (infer_pix_fmt + flatten_frame) gain an Rgb24 branch so
    the baseline-rgb-32x32 corpus fixture passes its PsnrFloor
    threshold without the planar-YUV reinterpretation fallback. The
    registry-side trait API (MjpegEncoder::send_frame) accepts
    PixelFormat::Rgb24 input 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
    in tests/roundtrip.rs cover 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 usual SOI / 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
    variants encode_jpeg_grayscale_with_opts(..., restart_interval) and
    encode_jpeg_grayscale_with_meta(..., restart_interval, meta) add DRI

    • RSTn emission (same once-per-restart_interval-MCUs scheme the
      YUV baseline path uses, with predictor reset and RST0..=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,
      and Q = 100 collapses 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::Gray8 without set_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
    Unsupported error surfaces when callers forget. Three new
    integration tests in tests/roundtrip.rs cover 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_errors test is
    rewritten as registry_encoder_gray8_without_lossless_flag_takes_baseline
    to assert the new SOF0 emission shape, paired with a fresh
    registry_encoder_gray12_without_lossless_flag_errors to 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),
    and src/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
    in src/.

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 = 1 per 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 = 8 and the
    shared decode_lossless_scan packs the resulting four sample planes
    into a PixelFormat::Cmyk VideoFrame (4 bytes/pixel C 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 with Error::Unsupported because the workspace
    PixelFormat enum 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::PacketizeOpts add 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 for RSTn boundaries (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 with F = L = 1 plus the index of each fragment's first interval in the 14-bit
    Restart Count (wrapping modulo 0x3FFF, the value reserved for
    whole-frame reassembly). A single oversize interval returns
    MjpegError::Unsupported instead of falling back silently to byte
    boundaries. When the source has no DRI the flag is a no-op and the
    output equals rtp::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 through JpegDepacketizer that
    shows the reassembled scan preserves every source RSTn position.

  • New benches/codec.rs Criterion 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
    the Decoder trait), 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, no docs/ reads, no third-party library calls. Pinned
    to criterion = "0.5" to match the existing cross-codec
    bench fleet (flac / tta / tiff / magicyuv / huffyuv / pcx / qoi).

  • New arith_decode cargo-fuzz target wraps fuzz-supplied bytes in a
    minimal SOF9 (extended-sequential arithmetic-coded) JPEG envelope so
    the src/jpeg/arith.rs Q-coder (ArithDecoder::new / Initdec /
    Renorm_d / Byte_in / decode_dc_diff / decode_ac /
    decode_magnitude) and the decode_arith_scan per-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 in fuzz/fuzz_targets/.