Skip to content

feat: Fidelity API — generic lossy/near-lossless/lossless encode quality (#12)#22

Open
lilith wants to merge 1 commit into
mainfrom
feat/fidelity-api
Open

feat: Fidelity API — generic lossy/near-lossless/lossless encode quality (#12)#22
lilith wants to merge 1 commit into
mainfrom
feat/fidelity-api

Conversation

@lilith
Copy link
Copy Markdown
Member

@lilith lilith commented Jun 4, 2026

Implements the generic encode-quality abstraction from #12 — the converged design from the discussion on that issue.

What this adds (zencodec types + EncoderConfig surface)

Fidelity is a sum type — exactly one of lossy, near-lossless, or lossless — so each regime carries the parameter its own metric needs, illegal states (lossy ∧ lossless) are unrepresentable, and lossless is explicit (not quality == 100, which is a footgun on JPEG).

pub enum Fidelity {                       // #[non_exhaustive]
    Lossy(LossyTarget),
    NearLossless(NearLosslessBudget),
    Lossless,
}
pub enum LossyTarget {                    // #[non_exhaustive]
    Quality(f32),                         // single-pass everywhere
    Distance(f32),                        // butteraugli; JXL single-pass, else iterative
    Metric { metric: QualityMetric, target: f32 },
    TargetBytes(u64), Bitrate(f32),       // iterative
}
pub enum QualityMetric { Ssimulacra2, Butteraugli, Dssim, Psnr } // #[non_exhaustive]
pub struct NearLosslessBudget(u16);       // max per-channel L∞ error, parts-per-65535

NearLosslessBudget is a codec-agnostic max-error fraction (every value valid for every lossless codec). 255 × 257 = 65535 makes 8- and 16-bit both exact: from_8bit_steps(2)±2 at 8-bit, ±514 at 16-bit, no float-floor trap. Codecs consume max_error_at_depth(depth) and round the guarantee down.

EncoderConfig methods

fn with_fidelity(self, f: Fidelity) -> Self;                     // infallible, best-effort, chainable
fn try_target_fidelity(&mut self, f: Fidelity) -> FidelityMatch; // fail-fast, rich outcome
fn resolved_target_fidelity(&self) -> Option<Fidelity>;
fn with_alpha_fidelity(self, a: Option<Fidelity>) -> Self;       // lossy color + lossless alpha
fn alpha_fidelity(&self) -> Option<Fidelity>;

try_target_fidelity returns FidelityMatchSupported / MetricTranslated / TargetRaised / TargetLowered / Lossless / Unsupported. A codec may quietly give you better fidelity than asked (TargetRaised, Lossless) but never silently less — a downgrade across the lossy/lossless fence is Unsupported.

EncodeCapabilities gains near_lossless, supports_distance, supports_metric_target, supports_size_target.

Back-compat

Additive, non-breaking. Default impls bridge to the legacy with_generic_quality/with_lossless scalars both ways, so a codec implements either the legacy pair or the new with_fidelity/resolved_target_fidelity pair and gets the other for free. No DynEncoderConfig change (fidelity is set on the concrete config before type-erasure, like quality/lossless today).

Out of scope (follow-ups, each in its own codec crate)

  • WebP/PNG honor ε and set caps.near_lossless = true; AVIF/GIF/JXL promote NearLosslessLossless; JPEG leaves the default.
  • Iterative LossyTarget arms (Distance/Metric/TargetBytes/Bitrate) land with their capability flags + FidelityMatch reporting.

Verification

  • cargo test — 10 unit tests for the budget math / classification + doctests, all pass
  • cargo clippy --all-targets -D warnings — clean
  • cargo fmt --check — clean
  • cargo doc — clean (the only -D warnings doc-link failures are pre-existing in gainmap.rs/limits.rs, untouched here)

Design rationale + per-codec mapping: docs/near-lossless-design.md (rewritten in this PR; supersedes the standalone docs/near-lossless-design branch).

…ity (#12)

Sum-type fidelity abstraction on the EncoderConfig surface:

- Fidelity { Lossy(LossyTarget), NearLossless(NearLosslessBudget), Lossless }
- LossyTarget (non-exhaustive): Quality / Distance / Metric{QualityMetric,target}
  / TargetBytes / Bitrate — arms differ in cost/support
- NearLosslessBudget(u16): codec-agnostic max per-channel L-infinity error as a
  parts-per-65535 fraction; exact at 8- and 16-bit via max_error_at_depth
- FidelityMatch: Supported / MetricTranslated / TargetRaised / TargetLowered /
  Lossless / Unsupported (returned by try_target_fidelity)
- QualityMetric (non-exhaustive): Ssimulacra2 / Butteraugli / Dssim / Psnr

EncoderConfig gains with_fidelity (infallible, best-effort, chainable),
try_target_fidelity (fail-fast, returns FidelityMatch), resolved_target_fidelity,
with_alpha_fidelity/alpha_fidelity. EncodeCapabilities gains near_lossless +
supports_{distance,metric_target,size_target}.

Default impls bridge to the legacy with_generic_quality/with_lossless scalars
both ways, so a codec implements either pair and gets the other free — additive,
non-breaking. Codec impls land separately.

Design + per-codec mapping: docs/near-lossless-design.md
@lilith lilith self-assigned this Jun 4, 2026
@lilith
Copy link
Copy Markdown
Member Author

lilith commented Jun 4, 2026

Closing per request. The design and types remain captured in docs/near-lossless-design.md and issue #12; branch feat/fidelity-api is preserved if we revisit.

@lilith lilith closed this Jun 4, 2026
@lilith
Copy link
Copy Markdown
Member Author

lilith commented Jun 4, 2026

Reopened — my misread; the close was meant for the standalone docs/near-lossless-design branch (now deleted, superseded by this PR's doc), not this PR.

@lilith lilith reopened this Jun 4, 2026
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