Skip to content

feat: CodecSet multi-codec registry, one-shot trait methods, prelude (0.1.27)#118

Open
lilith wants to merge 14 commits into
mainfrom
codecset
Open

feat: CodecSet multi-codec registry, one-shot trait methods, prelude (0.1.27)#118
lilith wants to merge 14 commits into
mainfrom
codecset

Conversation

@lilith

@lilith lilith commented Jul 14, 2026

Copy link
Copy Markdown
Member

What

Makes zencodec implementors trivially usable on their own and in groups, per the "registry easy, implementors super easy" design discussion:

  1. CodecSet (src/set.rs, re-exported at the root) — a multi-codec registry over the existing object-safe DynDecoderConfig / DynEncoderConfig traits. Register configs one line each; the set gives you detectdecode, probe, push_decode, animation_decoder, streaming_decoder, and format-keyed encode / encode_with(Fidelity), plus decode_job / encode_job escape hatches with the set's ResourceLimits / StopToken / policies pre-stamped.
  2. One-shot provided methods on the config traits — DecoderConfig::decode(data), DecoderConfig::probe(data), EncoderConfig::encode(pixels) — single-line config→result use with default job settings. Default bodies; no codec crate changes needed.
  3. zencodec::prelude — one-import bundle of every encode/decode trait (generic + dyn).

Shareable app-wide (statically)

CodecSet is Send + Sync + 'static, every operation takes &self, and it's Clone (base set → per-tenant variants). The intended pattern, exercised by a dedicated test that decodes from 4 threads through a static LazyLock<CodecSet>:

static CODECS: LazyLock<CodecSet> = LazyLock::new(|| CodecSet::new()
    .with_decoder(zenjpeg::JpegDecoderConfig::new())
    .with_encoder(zenjpeg::JpegEncoderConfig::new().with_generic_quality(85.0)));

let image = CODECS.decode(&bytes)?;

Design notes

  • No new required methods on the public Dyn* traits. Clone/fidelity capability is captured at registration via private entry supertraits (DecoderEntry / EncoderEntry), so manual implementors of DynDecoderConfig / DynEncoderConfig are unaffected — cargo semver-checks: 196 pass, valid minor.
  • Detection can't be mis-ordered. detect() walks ImageFormatRegistry::common()'s curated priority (AVIF-before-HEIC, DNG-before-TIFF) filtered to registered decoders, then Custom formats in registration order.
  • Encoder configs are templates. Codec-specific options are set on the concrete type before registration; encode_with clones the template and applies a per-call Fidelity.
  • Codec failures pass through as CodecSetError::Codec with the codec's error chain intact (source() / find_cause).

Testing

  • 6 codec-free unit tests in set.rs (Send+Sync+'static assertion, empty-set behavior, typed errors, Clone/Debug).
  • 11 behavior tests in zencodec-testkit/tests/codec_set.rs against the reference codec: roundtrip, registration-scoped detection, custom-format detect→decode end to end, static LazyLock multi-thread sharing, template cloning, metadata via encode_job, animation + streaming through the set, typed errors, one-shot trait methods.
  • Full workspace: 743 tests pass, 0 failed. cargo fmt --check, clippy --workspace --all-targets clean, apidoc snapshot regenerated + ZEN_API_DOC=check green, cargo semver-checks check-release green (0.1.26 → 0.1.27 minor).

Version bumped to 0.1.27 in-tree because 0.1.26 is already published and CI's semver gate enforces version adequacy for the additive API. Publishing remains a separate, user-approved step.

Noticed while editing (not changed here): docs/spec.md mentions ImageFormat::from_magic(), which doesn't exist in source (detection is ImageFormatRegistry::detect()); the repo CLAUDE.md "No CodecError here" line is similarly stale. Happy to batch those corrections separately.

lilith added 4 commits July 13, 2026 21:49
…nfig traits

DecoderConfig::decode(data) / DecoderConfig::probe(data) and
EncoderConfig::encode(pixels) — provided methods with default bodies so
every codec's config gets single-line one-shot use (config -> pixels)
without touching the job layer. Additive: default bodies, no codec changes.
Register DecoderConfig/EncoderConfig values one line each; the set derives
magic-byte detection from what's registered (built-ins in
ImageFormatRegistry::common() priority order, so AVIF-before-HEIC and
DNG-before-TIFF hold regardless of registration order), keys encode by
ImageFormat, and stamps set-level ResourceLimits/StopToken/policies onto
every job. Send + Sync + 'static with &self operations — build once, share
app-wide via LazyLock/OnceLock/Arc. Clone capability is captured at
registration (private entry traits), so the public Dyn* traits gain no new
required methods; per-call quality goes through encode_with(Fidelity) on a
clone of the registered template. Adds zencodec::prelude (one-import trait
bundle) wiring and the regenerated public-API snapshot.
Roundtrip, detection-only-consults-registered-formats, custom-format
detect->decode end to end, static LazyLock sharing across 4 threads,
encode_with template cloning, metadata via encode_job, animation + streaming
through the set, typed NoDecoder/NoEncoder/Codec errors, Clone independence,
and the one-shot trait methods.
…mp to 0.1.27

README quick start gains the one-shot and CodecSet (static LazyLock sharing)
sections; docs/spec.md gains the CodecSet/prelude sections and the provided
methods in the trait signature blocks; CHANGELOG entries under [Unreleased]
with commit hashes. Version 0.1.26 -> 0.1.27: the new API is additive and
0.1.26 is already published (cargo semver-checks: 196 pass, minor change).
@lilith lilith self-assigned this Jul 14, 2026
lilith added 10 commits July 13, 2026 23:32
…e convenience

Mirror the decode/encode one-shots for resource estimation: look up the
registered encoder/decoder for a format and forward to its
estimate_{encode,decode}_resources, returning NoEncoder/NoDecoder when the
format is not registered. Estimation was already reachable via
encoder_for(fmt)?.estimate_encode_resources(..) but had no named sugar; this
puts it next to probe/decode/encode so a CodecSet is a one-stop handle.
…enience

Probe the input for format + dimensions, then estimate the decode of a still
frame at the probed canvas size in the decoder's native output descriptor (its
first supported_descriptors, so bit depth / channel count are real, not guessed
from alpha). The bytes-based counterpart to estimate_decode — as one-call as
probe; same UnrecognizedFormat / NoDecoder / codec errors.
Make estimate-environment construction explicit. host() (std) detects the
running machine — available_parallelism() cores + SimdTier::CurrentHost, RAM
left unknown (std has no portable query). conservative() is the single-core
baseline under an explicit name. new() is deprecated (delegates to
conservative()) and queued for removal in the next 0.x minor; Default and the
internal callers/tests now use conservative().
- CodecSet::estimate_encode_of(format, pixels, compute): pixels-based
  encode estimate that reads dims + descriptor off the slice you already
  hold (the encode analog of estimate_decode_of for decode).
- zencodec-testkit/tests/usage.rs: 10 worked, runnable examples of the
  common CodecSet usages — encode / decode / probe / app-wide sharing /
  fidelity+metadata / pixel-format request / strip streaming / estimate —
  plus two zenpixels-convert-assisted paths: adapt_for_encode into an
  encoder's supported set, and convert_to on a decoded buffer.
- testkit: dev-dep zenpixels-convert 0.2.14; zencodec dep gains
  features=["std"] so the reference codec + ComputeEnvironment::host()
  are reachable from the examples.
- CHANGELOG + docs/spec.md updated for estimate_encode_of and the examples.
…coder

Shortens the common configured-encode and detect->decode paths surfaced by
the usage examples.

- EncodeJob::encode / DynEncodeJob::encode: provided one-shot methods so a
  configured job (metadata / limits / policy) encodes in one call instead of
  job.encoder()?.encode(pixels) / job.into_encoder()?.encode(pixels) — the
  job-level analog of EncoderConfig::encode. CodecSet::encode / encode_with
  use it internally.
- testkit ReferenceZcrDecoderConfig + ZCR_FORMAT: the reference decoder
  registered under a format that self-detects the codec's ZCR1 wire magic, so
  detection-based decode (decode / probe / estimate_decode_of) works end to
  end. usage.rs and codec_set.rs both drop their hand-rolled ~35-line
  custom-format scaffolding.
- usage.rs metadata example uses the job's direct encode; new codec_set
  regression test asserts job.encode == the two-step form (typed + dyn).
- CHANGELOG + docs/spec.md updated; public-API snapshots regenerated (also
  captures the estimate_* + conservative()/host() additions from earlier this
  session that the non-CI apidoc pass had not snapshotted).
- Fix a pre-existing broken intra-doc link (crate::minimal, now pub(crate)).
The headline proxy operation on CodecSet: decode input, carry the source's
metadata under an explicit retention policy, re-encode to the target with a
color-emission policy — one call.

- transcode(input, target, fidelity, metadata_policy, color_policy) composes
  decode() + Metadata::from(&ImageInfo) (ICC/EXIF/XMP/CICP/HDR/orientation,
  with orientation reconciliation) + set_metadata_policy + set_policy(color) +
  the job's encode(). The color policy merges onto the set-level EncodePolicy
  (overriding only its color field), preserving stamped security settings.
- Single-image, no pixel processing, no descriptor adaptation: a source->target
  pair whose decoder-output and encoder-input descriptors are disjoint surfaces
  the encoder's error rather than corrupting (zencodec carries no
  pixel-conversion dep; the common zen codecs bridge on sRGB RGBA8/RGB8).
- Tests: transcode roundtrip + ICC retention (codec_set); a one-call transcode
  example (usage.rs). CHANGELOG + docs/spec.md + public-API snapshot updated.
A descriptor-support audit across the zen codecs showed CodecSet::transcode
calls the codec's encode() directly (the traits crate does no adaptation), and
codecs REJECT any pixel descriptor outside their supported_descriptors(). So a
plain decode() that returns a native descriptor the target can't take (e.g. a
16-bit source into an 8-bit-only encoder) would error needlessly.

transcode now decode_preferring(target_encoder.supported_descriptors()), so the
decoder emits a descriptor the encoder accepts when it can — decoder and encoder
meet on a shared format with no conversion. Failures collapse to pairs whose
supported descriptor sets are genuinely disjoint (among current zen codecs, only
a RAW decoder's 16-bit/f32-only output into BMP/farbfeld's 8-bit-only input).
Strict improvement: decode_preferring falls back to native when unhonored, so no
pair that worked before regresses. Doc + CHANGELOG updated.
…tten

The prior decode_preferring(target.supported_descriptors()) passed the encoder's
list in its native order — which every zen encoder lists 8-bit-first. Since
decode_preferring picks the first descriptor the decoder can produce, a 16-bit
or HDR source that can also emit 8-bit was flattened to 8-bit SDR even when the
target (jpeg-16/png-16/avif/jxl) could carry the full precision — a needless
quality loss (HDR->SDR in the worst case).

transcode now decodes at the source's NATIVE descriptor first and encodes it
verbatim when the target accepts it (membership check), so precision is kept
whenever the target can hold it. It re-decodes (decode_preferring) to bridge
only when the native descriptor is unsupported — a second decode, and a
precision drop only where the target genuinely can't carry the source format.
RAW sources were already safe (16-bit/f32-only, no 8-bit path to fall to). Doc +
CHANGELOG updated.
… design

Synthesizes 8 parallel source surveys (JPEG/PNG/WebP/GIF/AVIF/JXL/HEIC/RAW/
zenbitmaps ×6) into docs/pixel-descriptor-negotiation.md:

- 3-tier probe-certainty verdict: only some codecs can emit a certain source
  descriptor at probe (farbfeld/QOI/PNM/HDR/PNG certain; JPEG/WebP/JXL/BMP/TGA
  caveated; GIF/RAW cannot). Key reframe: negotiation must run in the DECODER
  off the exact native descriptor, not reconstructed from probe's lossy
  scalar SourceColor (probe drops float-ness, RAW decoder-decides output,
  GIF unpopulated).
- Descriptor capability matrix (decode-produces / encode fidelity ceiling).
- Accept-and-downconvert bloat traps: WebP/GIF/JPEG/AVIF advertise *F32 (and
  JPEG/PNG 16-bit) encode descriptors they silently downconvert.
- Decoders handle 'preferred' 5 inconsistent ways today; PNG's use of the
  precision-blind shared negotiate_pixel_format flattens 16-bit/HDR (the bug).
- Navigator: lift JXL choose_pixel_format/can_produce_losslessly (decode.rs:
  391-475) into shared negotiate_pixel_format(source, preferred, available);
  redefine encode supported_descriptors() as a fidelity envelope.

Doc-only; no code change.
Adds the unified pipeline-composition model to pixel-descriptor-negotiation.md
from 4 further surveys (zenpixels-convert; TIFF/PDF multi-page; color-emit
decision; moxcms). Revises the Phase-1 'lift JXL' plan:

- Fidelity is a per-image PIPELINE (decode ∘ convert ∘ encode); worst link wins.
- The fidelity classifier ALREADY EXISTS in zenpixels-convert
  (conversion_cost_with_provenance, loss==0 = lossless; LossBucket) — richer than
  JXL's slice. It's pure descriptor math (no_std-able) → RELOCATE to zenpixels,
  don't reinvent. Two-tier: descriptor classifier (no_std) + moxcms ColorContext
  check (std) for custom-ICC pairs it's blind to.
- StorageFidelity static encode tag still needed (codec-internal downconvert is
  invisible to the descriptor classifier).
- Multi-page: TIFF/PDF pages genuinely differ but decoders FLATTEN (CMYK→RGB);
  MultiPageDecoder is vaporware; ImageInfo single-valued; CMYK→CMYK impossible.
- Color: resolve_color_emit is a lossless signaling retag (zero prod callers);
  pixel CMS unwired; retag-vs-Lossy rule; HDR→SDR fails OPEN (silent highlight
  clip on default build) — a correctness hazard the negotiator must refuse.
- Revised architecture (§13): relocate classifier, thin negotiate wrapper,
  SupportedDescriptor encode type, MultiPageDecoder, wire color decision→mechanism.

Doc-only; no code change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant