zencodecA living guide · main
Shared contracts for image & media codecs

Current trait and adapter improvements → Exact signatures, reasons, executing examples and remaining video gaps.

One image.
Three points of view.

Start with a single call. Add complexity when the application gives you a reason.

Follow the image
The first useful result
let image = config.decode(bytes)?;

One photo becomes a preview.
Then a gallery. Then a remote viewer.
Then moving images.

Show imports, caller & trait signature ↘Rust · no_std + alloc · sans-I/O core
Consumer intent & resultsAdapter transport & schedulingImplementor codec state & promises
Start here / conveniences before machinery

Find your first call.

One codec, a set of codecs, or a configured job. Open the code above and below each call to see its complete, compiled context.

Bring the concrete codec crate for each format you register. zencodec supplies traits and CodecSet; pixel views come from zenpixels. zencodec::prelude::* imports the common encode/decode traits. These examples use explicit imports so you can see exactly which trait enables each method.

CodecSet · detect + decode

Register the formats your app accepts once. CodecSet lives in zencodec; concrete codec configurations come from codec crates. Unknown input returns an error.

In scope. Setup uses zenbitmaps::{QoiDecoderConfig, BmpDecoderConfig}, both constructed with new(). bytes is an encoded file; DecodeOutput owns the result. image.width() and image.height() return u32 dimensions. Expand Imports & setup for the complete registration.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup17 lines
use zenbitmaps::{BmpDecoderConfig, BmpEncoderConfig, QoiDecoderConfig, QoiEncoderConfig};
use zencodec::decode::DecodeOutput;
use zencodec::{CodecSet, CodecSetError};

// Build once at application startup, then share by reference or Arc.
pub fn gallery_codecs() -> CodecSet {
    CodecSet::new()
        .with_decoder(QoiDecoderConfig::new())
        .with_decoder(BmpDecoderConfig::new())
        .with_encoder(QoiEncoderConfig::new())
        .with_encoder(BmpEncoderConfig::new())
}

pub fn registered_preview(
    codecs: &CodecSet,
    bytes: &[u8],
) -> Result<DecodeOutput, CodecSetError> {
    let image = codecs.decode(bytes)?;
Return & continuation3 lines
    // Detects among registered decoders; codecs remains available for the next request.
    Ok(image)
}

Signatures from this revision · bodies omitted

Inherent methodszencodec::CodecSet
impl CodecSet {
    pub fn new() -> Self;

    pub fn with_decoder(mut self, config: impl DecoderConfig + 'static) -> Self;

    pub fn decode<'a>(&'a self, data: &'a [u8]) -> Result<DecodeOutput, CodecSetError>;
}

Selected methods · 46 more in the full contract.

Read defaults, imports and full contract ↗

One codec · decode

Already know the codec? DecoderConfig::decode creates the default job and decoder for you. Import DecoderConfig (or zencodec::prelude::*) to call the trait method.

In scope. bytes: &[u8] is the encoded file. The concrete config is QoiDecoderConfig in this runnable fixture. This call consumes the config; clone a reusable config first.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup6 lines
use zenbitmaps::QoiDecoderConfig;
use zencodec::decode::{DecodeOutput, DecoderConfig};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn preview(bytes: &[u8]) -> Result<DecodeOutput, Error> {
    let config = QoiDecoderConfig::new();
    let image = config.decode(bytes)?;
Return & continuation2 lines
    Ok(image)
}

Signatures from this revision · bodies omitted

Trait methodszencodec::decode::DecoderConfig
pub trait DecoderConfig: Clone + Send + Sync {
    type Error: core::error::Error + Send + Sync + 'static;

    type Job<'a>: DecodeJob<'a, Error = Self::Error> + 'static;

    fn decode(self, data: &[u8]) -> Result<DecodeOutput, Self::Error>
    where
        Self: Sized;

    fn probe(&self, data: &[u8]) -> Result<ImageInfo, Self::Error>
    where
        Self: Sized;

    fn job<'a>(self) -> Self::Job<'a>;
}

Selected methods · 4 more in the full contract.

Required methods: formats, supported_descriptors, job. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗

One codec · probe

Read ImageInfo before allocating the decoded raster. Config-level probe borrows the config; use a job when probing must honor per-request settings.

In scope. The input is the same encoded byte slice. The returned ImageInfo contains the dimensions and metadata reported by the codec’s probe.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup7 lines
use zenbitmaps::QoiDecoderConfig;
use zencodec::ImageInfo;
use zencodec::decode::DecoderConfig;
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn probe(bytes: &[u8]) -> Result<ImageInfo, Error> {
    let config = QoiDecoderConfig::new();
    let info = config.probe(bytes)?;
Return & continuation3 lines
    // Probe borrows the config; another probe can reuse it.
    Ok(info)
}

Signatures from this revision · bodies omitted

Trait methodszencodec::decode::DecoderConfig
pub trait DecoderConfig: Clone + Send + Sync {
    type Error: core::error::Error + Send + Sync + 'static;

    type Job<'a>: DecodeJob<'a, Error = Self::Error> + 'static;

    fn decode(self, data: &[u8]) -> Result<DecodeOutput, Self::Error>
    where
        Self: Sized;

    fn probe(&self, data: &[u8]) -> Result<ImageInfo, Self::Error>
    where
        Self: Sized;

    fn job<'a>(self) -> Self::Job<'a>;
}

Selected methods · 4 more in the full contract.

Required methods: formats, supported_descriptors, job. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗

One codec · encode

EncoderConfig::encode accepts a typed pixel view, including row stride, and supplies the default job. EncodeOutput owns the encoded bytes.

In scope. pixels: PixelSlice carries width, row count, stride and descriptor. The caller owns its backing storage for the call. Pixel types are imported from zenpixels.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup7 lines
use zenbitmaps::QoiEncoderConfig;
use zencodec::encode::{EncodeOutput, EncoderConfig};
use zenpixels::PixelSlice;
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn encode(pixels: PixelSlice<'_>) -> Result<EncodeOutput, Error> {
    let config = QoiEncoderConfig::new();
    let output = config.encode(pixels)?;
Return & continuation2 lines
    Ok(output)
}

Signatures from this revision · bodies omitted

Trait methodszencodec::encode::EncoderConfig
pub trait EncoderConfig: Clone + Send + Sync {
    type Error: core::error::Error + Send + Sync + 'static;

    type Job: EncodeJob<Error = Self::Error>;

    fn format() -> ImageFormat;

    fn supported_descriptors() -> &'static [PixelDescriptor];

    fn capabilities() -> &'static EncodeCapabilities;

    fn job(self) -> Self::Job;

    fn encode(self, pixels: PixelSlice<'_>) -> Result<EncodeOutput, Self::Error>
    where
        Self: Sized,
        <Self::Job as EncodeJob>::Enc: Encoder<Error = Self::Error>;
}

Selected methods · 16 more in the full contract.

Required methods: format, supported_descriptors, job. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗

Configured job · encode

After choosing per-operation metadata, use EncodeJob::encode directly. You only need encoder() when you will drive row or animation methods yourself.

In scope. image is an existing DecodeOutput. The policy is explicit; this QOI fixture checks API composition and pixels, not EXIF/ICC preservation.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use zenbitmaps::QoiEncoderConfig;
use zencodec::MetadataPolicy;
use zencodec::decode::DecodeOutput;
use zencodec::encode::{EncodeJob, EncodeOutput, EncoderConfig};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn retained(image: &DecodeOutput) -> Result<EncodeOutput, Error> {
    let config = QoiEncoderConfig::new();
    let output = config
        .job()
        .with_metadata_policy(image.metadata(), MetadataPolicy::Web)
        .encode(image.pixels())?;
Return & continuation2 lines
    Ok(output)
}

Signatures from this revision · bodies omitted

Trait methodszencodec::encode::EncodeJob
pub trait EncodeJob: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    type Enc: Sized + 'static;

    type AnimationFrameEnc: Sized + Send + 'static;

    fn with_metadata_policy(self, meta: Metadata, policy: crate::MetadataPolicy) -> Self;

    fn encode(self, pixels: PixelSlice<'_>) -> Result<EncodeOutput, Self::Error>
    where
        Self::Enc: Encoder<Error = Self::Error>;

    fn encoder(self) -> Result<Self::Enc, Self::Error>;
}

Selected methods · 20 more in the full contract.

Required methods: with_pulse, with_limits, with_metadata, encoder, animation_frame_encoder. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗

CodecSet · encode

Choose the output format explicitly; the registered encoder template supplies its options. encode_with adds a per-call Fidelity request without changing the shared set.

In scope. codecs: &CodecSet already has the desired encoder registered. For a derivative with per-operation metadata, use encode_job and DynEncodeJob setters.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use zencodec::encode::EncodeOutput;
use zencodec::{CodecSet, CodecSetError, ImageFormat};
use zenpixels::PixelSlice;

pub fn registered_encode(
    codecs: &CodecSet,
    pixels: PixelSlice<'_>,
) -> Result<EncodeOutput, CodecSetError> {
    let output = codecs.encode(ImageFormat::Qoi, pixels)?;
Return & continuation3 lines
    // The set must already contain a QOI encoder configuration.
    Ok(output)
}

Signatures from this revision · bodies omitted

Inherent methodszencodec::CodecSet
impl CodecSet {
    pub fn encode(
        &self,
        format: ImageFormat,
        pixels: PixelSlice<'_>,
    ) -> Result<EncodeOutput, CodecSetError>;

    pub fn encode_with(
        &self,
        format: ImageFormat,
        fidelity: Fidelity,
        pixels: PixelSlice<'_>,
    ) -> Result<EncodeOutput, CodecSetError>;
}

Selected methods · 47 more in the full contract.

Read defaults, imports and full contract ↗

CodecSet · transcode

The still-image convenience takes explicit fidelity, metadata and color policies. It does no resize or color conversion and refuses sequence, auxiliary-image and descriptor barriers.

In scope. The pinned QOI/BMP fixtures reach the descriptor guard: decode yields unknown-transfer RGB while QOI advertises sRGB input. That is a visible refusal, tracked in zenbitmaps issue 28, not a reason to silently relabel pixels.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup7 lines
use zencodec::encode::{EncodeOutput, Fidelity};
use zencodec::{CodecSet, CodecSetError, ColorEmitPolicy, ImageFormat, MetadataPolicy};

pub fn registered_transcode(
    codecs: &CodecSet,
    bytes: &[u8],
) -> Result<EncodeOutput, CodecSetError> {
    let output = codecs.transcode(
        bytes,
        ImageFormat::Qoi,
        Fidelity::Lossless,
        MetadataPolicy::Web,
        ColorEmitPolicy::Balanced,
    )?;
Return & continuation4 lines
    // May refuse a descriptor, sequence, or auxiliary-image barrier.
    // This convenience performs no resize, CMS conversion, or silent first-frame selection.
    Ok(output)
}

Signatures from this revision · bodies omitted

Inherent methodszencodec::CodecSet
impl CodecSet {
    pub fn transcode(
        &self,
        input: &[u8],
        target: ImageFormat,
        fidelity: Fidelity,
        metadata: MetadataPolicy,
        color: ColorEmitPolicy,
    ) -> Result<EncodeOutput, CodecSetError>;
}

Selected methods · 48 more in the full contract.

Read defaults, imports and full contract ↗

Fixture limitation: zenbitmaps #28 — decoded color description.

Repository main includes unpublished interfaces. Native QOI and BMP are small lossless fixtures for the examples. Use the codecs your application needs; this is not a browser-format selection. zencodec-io drivers come later, when bytes stop being a resident slice.

The map / before the details

Every boundary has an owner.

Colors identify responsibility in every diagram. Arrows name what crosses the boundary.

The consumer, adapter, and implementor boundariesThe consumer passes intent to the adapter. The adapter offers bytes and destination rows to the codec implementor, who returns requests, progress and output. Pixels and frames go back to the consumer. Storage lifetime and cancellation cross these boundaries. 01 / CONSUMERThe applicationConfig → job → resultPixels · policy · presentationOwns the useful output 02 / ADAPTERThe connectionSource → driver → sinkDisk · HTTP · computeOwns admission & delivery 03 / IMPLEMENTORThe codecstep · rows · send / receiveState · kernels · capabilitiesOwns exact codec behavior intentbytesrequestsresultCancellation, identity, and storage lifetime remain visible across the whole path.
consumerThe applicationIntent: pixels, policy, presentation
adapterThe connectionBytes, requests, budgets, delivery
implementorThe codecState, kernels, exact results
Architecture diagram · colors identify responsibility, not separate runtimes or mandatory allocations.

Consumer chooses pixels, metadata policy, and time.

Adapter owns bytes, budgets, scheduling, and delivery.

Implementor reports what this codec configuration can actually do.

The guide / three connected journeys

Choose your side of the interface.

The same photo, viewed from three responsibilities. Each step explains what changed and why it earns another line.

Consumer / 01Executable example

Show the first photo

New requirementThe image is already in memory. The app needs pixels.

Start with a configured codec and one call. The returned image owns its pixels. Clone a reusable configuration when you need another job; the convenience call consumes this one.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup6 lines
use zenbitmaps::QoiDecoderConfig;
use zencodec::decode::{DecodeOutput, DecoderConfig};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn preview(bytes: &[u8]) -> Result<DecodeOutput, Error> {
    let config = QoiDecoderConfig::new();
    let image = config.decode(bytes)?;
Return & continuation2 lines
    Ok(image)
}

Let CodecSet choose the decoder

Registration is application setup. Each decode borrows the set; you can reuse it for the next file.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup17 lines
use zenbitmaps::{BmpDecoderConfig, BmpEncoderConfig, QoiDecoderConfig, QoiEncoderConfig};
use zencodec::decode::DecodeOutput;
use zencodec::{CodecSet, CodecSetError};

// Build once at application startup, then share by reference or Arc.
pub fn gallery_codecs() -> CodecSet {
    CodecSet::new()
        .with_decoder(QoiDecoderConfig::new())
        .with_decoder(BmpDecoderConfig::new())
        .with_encoder(QoiEncoderConfig::new())
        .with_encoder(BmpEncoderConfig::new())
}

pub fn registered_preview(
    codecs: &CodecSet,
    bytes: &[u8],
) -> Result<DecodeOutput, CodecSetError> {
    let image = codecs.decode(bytes)?;
Return & continuation3 lines
    // Detects among registered decoders; codecs remains available for the next request.
    Ok(image)
}

Signatures from this revision · bodies omitted

Trait methodszencodec::decode::DecoderConfig
pub trait DecoderConfig: Clone + Send + Sync {
    type Error: core::error::Error + Send + Sync + 'static;

    type Job<'a>: DecodeJob<'a, Error = Self::Error> + 'static;

    fn decode(self, data: &[u8]) -> Result<DecodeOutput, Self::Error>
    where
        Self: Sized;

    fn probe(&self, data: &[u8]) -> Result<ImageInfo, Self::Error>
    where
        Self: Sized;

    fn job<'a>(self) -> Self::Job<'a>;
}

Selected methods · 4 more in the full contract.

Required methods: formats, supported_descriptors, job. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Inherent methodszencodec::CodecSet
impl CodecSet {
    pub fn new() -> Self;

    pub fn with_decoder(mut self, config: impl DecoderConfig + 'static) -> Self;

    pub fn decode<'a>(&'a self, data: &'a [u8]) -> Result<DecodeOutput, CodecSetError>;
}

Selected methods · 46 more in the full contract.

Read defaults, imports and full contract ↗
Why add this?

A job builder adds no value until this operation needs settings. The one-line call already creates the job and decoder.

Encoded bytes: already resident → config.decode: one operation → Owned pixels: preview uses a viewCONSUMEREncoded bytesalready residentIMPLEMENTORconfig.decodeone operationCONSUMEROwned pixelspreview uses a view
consumerEncoded bytesalready resident
implementorconfig.decodeone operation
consumerOwned pixelspreview uses a view
Encoded bytes → config.decode → Owned pixels
1 of 5 in this journeyNext: Accept a user upload
Consumer / 02Executable example

Accept a user upload

New requirementAn upload can be much larger than the preview surface.

Move this operation into a job and set zencodec::ResourceLimits before constructing the decoder. The maximum is inclusive: width × height equal to the limit is accepted. The config describes the codec; the job describes this request.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use std::borrow::Cow;
use zenbitmaps::QoiDecoderConfig;
use zencodec::ResourceLimits;
use zencodec::decode::{Decode, DecodeJob, DecodeOutput, DecoderConfig};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn limited(bytes: &[u8], max_pixels: u64) -> Result<DecodeOutput, Error> {
    let config = QoiDecoderConfig::new();
    let limits = ResourceLimits::none().with_max_pixels(max_pixels);
    let image = config
        .job()
        .with_limits(limits)
        .decoder(Cow::Borrowed(bytes), &[])?
        .decode()?;
Return & continuation2 lines
    Ok(image)
}

Signatures from this revision · bodies omitted

Trait methodszencodec::decode::DecodeJob<'a>
pub trait DecodeJob<'a>: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    type Dec: Decode<Error = Self::Error>;

    type StreamDec: StreamingDecode<Error = Self::Error> + Send;

    type AnimationFrameDec: AnimationFrameDecoder<Error = Self::Error> + Send + 'static;

    fn with_limits(self, limits: ResourceLimits) -> Self;

    fn with_pulse(self, pulse: how_far::SharedPulse) -> Self;

    fn decoder(
        self,
        data: Cow<'a, [u8]>,
        preferred: &[PixelDescriptor],
    ) -> Result<Self::Dec, Self::Error>;

    fn dyn_incremental_decoder(
        self,
        preferred: &[PixelDescriptor],
    ) -> Result<Box<super::incremental::DynIncrementalDecoder>, BoxedError>;
}

Selected methods · 23 more in the full contract.

Required methods: with_pulse, with_limits, probe, output_info, decoder, push_decoder, streaming_decoder, animation_frame_decoder. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Inherent methodszencodec::CodecSet
impl CodecSet {
    pub fn with_limits(mut self, limits: ResourceLimits) -> Self;

    pub fn decode_job<'a>(
        &'a self,
        format: ImageFormat,
    ) -> Result<Box<dyn DynDecodeJob<'a> + 'a>, CodecSetError>;

    pub fn encode_job(&self, format: ImageFormat) -> Result<Box<dyn DynEncodeJob>, CodecSetError>;

    pub fn incremental_decoder(
        &self,
        preferred: &[PixelDescriptor],
    ) -> Box<crate::traits::DynIncrementalDecoder>;
}

Selected methods · 45 more in the full contract.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::Decode
pub trait Decode: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    fn decode(self) -> Result<DecodeOutput, Self::Error>;
}

Required methods: decode. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Admission is a requirement of the service. Adding it here keeps unrelated preview calls simple.

Upload policy: pixel limit → Decode job: check before allocation → Pixels or refusal: handle the resultCONSUMERUpload policypixel limitIMPLEMENTORDecode jobcheck before allocationCONSUMERPixels or refusalhandle the result
consumerUpload policypixel limit
implementorDecode jobcheck before allocation
consumerPixels or refusalhandle the result
Upload policy → Decode job → Pixels or refusal
Consumer / 03Executable example

Prepare pixels for display

New requirementThe display path prefers RGBA, while a codec may naturally produce RGB.

Give an ordered preference list and inspect the descriptor actually returned. A preference is negotiation, not permission for lossy conversion. Use the returned color metadata when selecting an explicit conversion.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use std::borrow::Cow;
use zenbitmaps::QoiDecoderConfig;
use zencodec::decode::{Decode, DecodeJob, DecodeOutput, DecoderConfig};
use zenpixels::PixelDescriptor;
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn preference(bytes: &[u8]) -> Result<DecodeOutput, Error> {
    let config = QoiDecoderConfig::new();
    let preferred = &[PixelDescriptor::RGBA8_SRGB, PixelDescriptor::RGB8_SRGB];
    let image = config
        .job()
        .decoder(Cow::Borrowed(bytes), preferred)?
        .decode()?;
    let actual = image.descriptor();
Return & continuation3 lines
    assert!(actual.bytes_per_pixel() >= 3);
    Ok(image)
}

Require the display layout with zenpixels-convert

This complete example registers JpegDecoderConfig and takes JPEG bytes. PixelBufferConvertExt::convert_to allocates BGRA8 sRGB pixels or returns a conversion error. Keep the returned residual orientation for presentation. CodecSet and the pinned color-aware conversion path require the unpublished sources used by the native JPEG harness.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup11 lines
use zencodec::{CodecSet, Orientation, ResourceLimits, decode::BoxedError};
use zenjpeg::JpegDecoderConfig;
use zenpixels::{PixelBuffer, PixelDescriptor};
use zenpixels_convert::PixelBufferConvertExt;

pub(crate) fn decode_for_display(
    bytes: &[u8],
) -> Result<(PixelBuffer, Orientation), BoxedError> {
    let codecs = CodecSet::new()
        .with_limits(ResourceLimits::for_untrusted_input().with_max_pixels(4_000_000))
        .with_decoder(JpegDecoderConfig::new());
    let decoded = codecs.decode(bytes)?;
    let orientation = decoded.info().orientation;
    let bgra = decoded
        .into_buffer()
        .convert_to(PixelDescriptor::BGRA8_SRGB)?;
Return & continuation4 lines
    // Storage conversion does not apply orientation. The display owns that step.
    // An unsupported color transform is an error, never a request to relabel.
    Ok((bgra, orientation))
}

Signatures from this revision · bodies omitted

Inherent methodszencodec::CodecSet
impl CodecSet {
    pub fn probe<'a>(&'a self, data: &'a [u8]) -> Result<ImageInfo, CodecSetError>;

    pub fn decode<'a>(&'a self, data: &'a [u8]) -> Result<DecodeOutput, CodecSetError>;

    pub fn decode_preferring<'a>(
        &'a self,
        data: &'a [u8],
        preferred: &[PixelDescriptor],
    ) -> Result<DecodeOutput, CodecSetError>;

    pub fn encode(
        &self,
        format: ImageFormat,
        pixels: PixelSlice<'_>,
    ) -> Result<EncodeOutput, CodecSetError>;

    pub fn encode_with(
        &self,
        format: ImageFormat,
        fidelity: Fidelity,
        pixels: PixelSlice<'_>,
    ) -> Result<EncodeOutput, CodecSetError>;

    pub fn transcode(
        &self,
        input: &[u8],
        target: ImageFormat,
        fidelity: Fidelity,
        metadata: MetadataPolicy,
        color: ColorEmitPolicy,
    ) -> Result<EncodeOutput, CodecSetError>;
}

Selected methods · 43 more in the full contract.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::DecodeJob<'a>
pub trait DecodeJob<'a>: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    type Dec: Decode<Error = Self::Error>;

    type StreamDec: StreamingDecode<Error = Self::Error> + Send;

    type AnimationFrameDec: AnimationFrameDecoder<Error = Self::Error> + Send + 'static;

    fn with_limits(self, limits: ResourceLimits) -> Self;

    fn with_pulse(self, pulse: how_far::SharedPulse) -> Self;

    fn decoder(
        self,
        data: Cow<'a, [u8]>,
        preferred: &[PixelDescriptor],
    ) -> Result<Self::Dec, Self::Error>;

    fn dyn_incremental_decoder(
        self,
        preferred: &[PixelDescriptor],
    ) -> Result<Box<super::incremental::DynIncrementalDecoder>, BoxedError>;
}

Selected methods · 23 more in the full contract.

Required methods: with_pulse, with_limits, probe, output_info, decoder, push_decoder, streaming_decoder, animation_frame_decoder. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

A display buffer has concrete layout and color requirements. They belong at the display boundary, not in a hidden codec conversion.

Rank layouts: RGBA then RGB → Negotiate: preserve sample meaning → Inspect descriptor: convert explicitlyCONSUMERRank layoutsRGBA then RGBIMPLEMENTORNegotiatepreserve sample meaningCONSUMERInspect descriptorconvert explicitly
consumerRank layoutsRGBA then RGB
implementorNegotiatepreserve sample meaning
consumerInspect descriptorconvert explicitly
Rank layouts → Negotiate → Inspect descriptor
3 of 5 in this journeyNext: Publish a derivative
Consumer / 04Executable example

Publish a derivative

New requirementThe gallery stores a derived image and needs an explicit metadata policy.

Carry the decoded metadata into an encode job with a retention policy. Web chooses the framework’s web-oriented retention rules. The target format can only carry metadata that it supports.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use zenbitmaps::QoiEncoderConfig;
use zencodec::MetadataPolicy;
use zencodec::decode::DecodeOutput;
use zencodec::encode::{EncodeJob, EncodeOutput, EncoderConfig};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn retained(image: &DecodeOutput) -> Result<EncodeOutput, Error> {
    let config = QoiEncoderConfig::new();
    let output = config
        .job()
        .with_metadata_policy(image.metadata(), MetadataPolicy::Web)
        .encode(image.pixels())?;
Return & continuation2 lines
    Ok(output)
}

Configure a registry job for this derivative

CodecSet returns a boxed DynEncodeJob. Its set_* methods mutate that job; typed EncodeJob uses with_* builders.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup9 lines
use zencodec::decode::DecodeOutput;
use zencodec::encode::EncodeOutput;
use zencodec::{CodecSet, ImageFormat, MetadataPolicy};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn registered_derivative(
    codecs: &CodecSet,
    image: &DecodeOutput,
) -> Result<EncodeOutput, Error> {
    let mut job = codecs.encode_job(ImageFormat::Qoi)?;
    job.set_metadata_policy(image.metadata(), MetadataPolicy::Web);
    let output = job.encode(image.pixels())?;
Return & continuation3 lines
    // Dyn jobs use setters; typed jobs use consuming with_* builders.
    Ok(output)
}

Signatures from this revision · bodies omitted

Trait methodszencodec::encode::EncodeJob
pub trait EncodeJob: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    type Enc: Sized + 'static;

    type AnimationFrameEnc: Sized + Send + 'static;

    fn with_metadata_policy(self, meta: Metadata, policy: crate::MetadataPolicy) -> Self;

    fn encode(self, pixels: PixelSlice<'_>) -> Result<EncodeOutput, Self::Error>
    where
        Self::Enc: Encoder<Error = Self::Error>;

    fn encoder(self) -> Result<Self::Enc, Self::Error>;
}

Selected methods · 20 more in the full contract.

Required methods: with_pulse, with_limits, with_metadata, encoder, animation_frame_encoder. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methodszencodec::encode::DynEncodeJob
pub trait DynEncodeJob {
    fn set_metadata_policy(&mut self, meta: Metadata, policy: crate::MetadataPolicy);

    fn encode(self: Box<Self>, pixels: PixelSlice<'_>) -> Result<EncodeOutput, BoxedError>;
}

Selected methods · 14 more in the full contract.

Required methods: set_pulse, set_limits, set_policy, set_metadata, set_canvas_size, set_loop_count, extensions, extensions_mut, into_encoder, into_animation_frame_encoder. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Publishing a derivative introduces a new policy decision. It deserves an explicit line rather than silent metadata inheritance.

Pixels + metadata: decoded result → Retention policy: MetadataPolicy::Web → Encode job: target format supportCONSUMERPixels + metadatadecoded resultCONSUMERRetention policyMetadataPolicy::WebIMPLEMENTOREncode jobtarget format support
consumerPixels + metadatadecoded result
consumerRetention policyMetadataPolicy::Web
implementorEncode jobtarget format support
Pixels + metadata → Retention policy → Encode job
Consumer / 05Current contract

The gallery starts moving

New requirementA still photo, animation, and video can share a container family but need different playback contracts.

Use image animation for composited canvas frames and durations. Use zencodec-media for packets, tracks, clocks, configuration changes, and owned frame handles. A video packet may produce zero, one, or several frames later.

Signatures from this revision · bodies omitted

Trait methodszencodec::decode::AnimationFrameDecoder
pub trait AnimationFrameDecoder: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    fn render_next_frame(
        &mut self,
        pulse: Option<&dyn crate::Pulse>,
    ) -> Result<Option<AnimationFrame<'_>>, Self::Error>;

    fn render_next_frame_owned(
        &mut self,
        pulse: Option<&dyn crate::Pulse>,
    ) -> Result<Option<OwnedAnimationFrame>, Self::Error>;
}

Selected methods · 5 more in the full contract.

Required methods: wrap_sink_error, info, render_next_frame, render_next_frame_to_sink. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methodszencodec_media::session::VideoDecoder
pub trait VideoDecoder {
    type Error: core::error::Error + Send + Sync + 'static;

    fn configure(
        &mut self,
        config: &CodecConfig,
        pulse: Option<&dyn zencodec::Pulse>,
    ) -> Result<(), SessionError<Self::Error>>;

    fn send(
        &mut self,
        packet: &mut Packet,
        pulse: Option<&dyn zencodec::Pulse>,
    ) -> Result<SendStatus, SessionError<Self::Error>>;

    fn receive(
        &mut self,
        pulse: Option<&dyn zencodec::Pulse>,
    ) -> Result<Receive<Frame>, SessionError<Self::Error>>;

    fn end_input(&mut self) -> Result<(), SessionError<Self::Error>>;

    fn reset(&mut self) -> Result<(), SessionError<Self::Error>>;
}

Selected methods · 1 more in the full contract.

Required methods: configure, send, receive, end_input, reset, stream_info. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Playback introduces time and retained references. Treating it as a sequence of independent image decodes loses those facts.

Container: track + timestamp → Video session: send / receive → Frame handle: presentation clockADAPTERContainertrack + timestampIMPLEMENTORVideo sessionsend / receiveCONSUMERFrame handlepresentation clock
adapterContainertrack + timestamp
implementorVideo sessionsend / receive
consumerFrame handlepresentation clock
Container → Video session → Frame handle
5 of 5 in this journeyExplore another perspective
Adapter / 01Executable example

Drive the decoder you have

New requirementA decoder exposes IncrementalDecode. Today, the input is a slice.

Give the driver a decoder, an input slice, and a row sink. Pass None for no extra borrowed Pulse; any control retained by the decoder’s job still applies. The driver offers bytes at the requested offset; the decoder reports a consumed prefix and an event. The sink supplies destination storage.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use zenbitmaps::QoiDecoderConfig;
use zencodec::decode::{DecodeJob, DecoderConfig};
use zencodec_io::{CollectSink, drive_from_slice};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn from_slice(bytes: &[u8]) -> Result<CollectSink, Error> {
    let mut decoder = QoiDecoderConfig::new().job().dyn_incremental_decoder(&[])?;
    let mut sink = CollectSink::new();
    // None adds no per-call control; the decoder retains its job control.
    drive_from_slice(decoder.as_mut(), bytes, &mut sink, None, 4096)?;
Return & continuation2 lines
    Ok(sink)
}

Keep format detection inside the same driver path

CodecSet::incremental_decoder clones the registered templates into an owned decoder and detects from the offered prefix.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup6 lines
use zencodec::CodecSet;
use zencodec_io::{CollectSink, drive_from_slice};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn registered_incremental(codecs: &CodecSet, bytes: &[u8]) -> Result<CollectSink, Error> {
    let mut sink = CollectSink::new();
    let mut decoder = codecs.incremental_decoder(&[]);
    // None preserves any SharedPulse already configured on the set's jobs.
    drive_from_slice(decoder.as_mut(), bytes, &mut sink, None, 4096)?;
Return & continuation3 lines
    // Detection happens as the driver supplies the initial bytes.
    Ok(sink)
}

Signatures from this revision · bodies omitted

Functionzencodec_io::drive_from_slice
pub fn drive_from_slice<D>(
    decoder: &mut D,
    bytes: &[u8],
    sink: &mut dyn DecodeRowSink,
    pulse: Option<&dyn Pulse>,
    window: usize,
) -> Result<(OutputInfo, DriveStats), DriveError<D::Error, core::convert::Infallible>>
where
    D: IncrementalDecode + ?Sized;

Selected methods · 3 more in the full contract.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::DecodeRowSink
pub trait DecodeRowSink {
    fn begin(
        &mut self,
        _width: u32,
        _height: u32,
        _descriptor: PixelDescriptor,
    ) -> Result<(), SinkError>;

    fn provide_next_buffer(
        &mut self,
        y: u32,
        height: u32,
        width: u32,
        descriptor: PixelDescriptor,
    ) -> Result<PixelSliceMut<'_>, SinkError>;

    fn finish(&mut self) -> Result<(), SinkError>;
}

Selected methods · 3 more in the full contract.

Required methods: provide_next_buffer. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::IncrementalDecode
pub trait IncrementalDecode {
    type Error: core::error::Error + Send + Sync + 'static;

    fn step(
        &mut self,
        input: Input<'_>,
        sink: &mut dyn DecodeRowSink,
        pulse: Option<&dyn crate::Pulse>,
    ) -> Result<Progress, StepError<Self::Error>>;

    fn requested(&self) -> &[InputRequest];

    fn info(&self) -> Option<&ImageInfo>;

    fn take_details(&mut self) -> Option<crate::decode::DecodeDetails>;

    fn access_pattern(&self) -> crate::source::AccessPattern;
}

Selected methods · 1 more in the full contract.

Required methods: step, requested, info. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

This is the small bridge between a codec protocol and an application buffer. You do not need an async runtime for resident input.

Slice driver: owns input offer → step(input, sink, None): consumes a prefix → CollectSink: owns full outputADAPTERSlice driverowns input offerIMPLEMENTORstep(input, sink, None)consumes a prefixCONSUMERCollectSinkowns full output
adapterSlice driverowns input offer
implementorstep(input, sink, None)consumes a prefix
consumerCollectSinkowns full output
Slice driver → step(input, sink, None) → CollectSink
Adapter / 02Executable example

Read the archive from disk

New requirementThe gallery opens a local file instead of loading every encoded byte up front.

Imports & setup above constructs the decoder and CollectSink. This sink owns the full raster; the function returns it only when the driver succeeds, preserving typed error causes on failure. FileReadAt::new(file)? supplies native positioned file reads on Unix and Windows; resident sources keep a direct slice path.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup9 lines
use zenbitmaps::QoiDecoderConfig;
use zencodec::decode::{DecodeJob, DecoderConfig};
use zencodec::source::ReadAt;
use zencodec_io::{CollectSink, DriveOptions, drive_from_read_at};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn positioned<S: ReadAt>(bytes: &mut S) -> Result<CollectSink, Error> {
    let mut decoder = QoiDecoderConfig::new().job().dyn_incremental_decoder(&[])?;
    let mut sink = CollectSink::new();
    // Pass Some(pulse) to add a full borrowed reporting/cancellation control.
    drive_from_read_at(
        decoder.as_mut(),
        bytes,
        &mut sink,
        None,
        DriveOptions::default(),
    )?;
Return & continuation2 lines
    Ok(sink)
}

Signatures from this revision · bodies omitted

Functionzencodec_io::drive_from_read_at
pub fn drive_from_read_at<D, S>(
    decoder: &mut D,
    source: &mut S,
    sink: &mut dyn DecodeRowSink,
    pulse: Option<&dyn Pulse>,
    opts: DriveOptions,
) -> Result<(OutputInfo, DriveStats), DriveError<D::Error, S::Error>>
where
    D: IncrementalDecode + ?Sized,
    S: ReadAt + ?Sized;

Selected methods · 3 more in the full contract.

Read defaults, imports and full contract ↗
Inherent methods · zencodec-io feature: stdzencodec_io::FileReadAt
impl FileReadAt {
    pub fn new(file: std::fs::File) -> Result<Self, StdSourceError>;

    pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, StdSourceError>;
}
Read defaults, imports and full contract ↗
Trait methodszencodec::source::ReadAt
pub trait ReadAt {
    type Error: core::error::Error + Send + Sync + 'static;

    fn len(&mut self) -> Result<u64, Self::Error>;

    fn len_hint(&self) -> Option<u64>;

    fn read_exact_at(&mut self, offset: u64, out: &mut [u8]) -> Result<(), Self::Error>;

    fn read_at(&mut self, offset: u64, out: &mut [u8]) -> Result<usize, Self::Error>;

    fn read_many_at(&mut self, reqs: &mut [(u64, &mut [u8])]) -> Result<(), Self::Error>;

    fn as_slice(&self) -> Option<&[u8]>;
}

Selected methods · 1 more in the full contract.

Required methods: len, read_exact_at. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

The codec names offsets. The file adapter can satisfy them without manufacturing a generic seek cursor at every boundary.

FileReadAt: positioned disk reads → ReadAt driver: buffer and reuse → Same decoder: same row contractADAPTERFileReadAtpositioned disk readsADAPTERReadAt driverbuffer and reuseIMPLEMENTORSame decodersame row contract
adapterFileReadAtpositioned disk reads
adapterReadAt driverbuffer and reuse
implementorSame decodersame row contract
FileReadAt → ReadAt driver → Same decoder
Adapter / 03Executable example

Zoom into remote content

New requirementA viewport needs several independent ranges, and each remote request has latency.

Expose known independent dependencies in one read_many_at call. The transport may overlap requests and merge nearby ranges, subject to admitted byte and descriptor budgets. Keep immutable object identity with the cache.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use zencodec::source::ReadAt;
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn batch<S: ReadAt>(
    source: &mut S,
    first: &mut [u8],
    second: &mut [u8],
) -> Result<(), Error> {
    source.read_many_at(&mut [(0, first), (8192, second)])?;
Return & continuation2 lines
    Ok(())
}

Signatures from this revision · bodies omitted

Trait methodszencodec::source::ReadAt
pub trait ReadAt {
    type Error: core::error::Error + Send + Sync + 'static;

    fn len(&mut self) -> Result<u64, Self::Error>;

    fn len_hint(&self) -> Option<u64>;

    fn read_exact_at(&mut self, offset: u64, out: &mut [u8]) -> Result<(), Self::Error>;

    fn read_at(&mut self, offset: u64, out: &mut [u8]) -> Result<usize, Self::Error>;

    fn read_many_at(&mut self, reqs: &mut [(u64, &mut [u8])]) -> Result<(), Self::Error>;

    fn as_slice(&self) -> Option<&[u8]>;
}

Selected methods · 1 more in the full contract.

Required methods: len, read_exact_at. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methods · zencodec-io feature: tokiozencodec_io::tokio_io::AsyncSource
pub trait AsyncSource: Send {
    fn read_at(
        &mut self,
        offset: u64,
        out: &mut [u8],
    ) -> impl Future<Output = Result<usize, BoxedError>> + Send;

    fn read_ranges(
        &mut self,
        reqs: &mut [(u64, &mut [u8])],
    ) -> impl Future<Output = Result<(), BoxedError>> + Send;

    fn len_hint(&self) -> Option<u64>;

    fn len(&mut self) -> impl Future<Output = Result<u64, BoxedError>> + Send;
}

Required methods: read_at, len. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

The reused IMI simulator shows the trade: batching overlaps round trips; coalescing reduces requests but transfers gap bytes. A budget can intentionally choose the slower plan.

Viewport demand: known dependencies → Batch + admission: bytes / identities → Range transport: shared bandwidthCONSUMERViewport demandknown dependenciesADAPTERBatch + admissionbytes / identitiesADAPTERRange transportshared bandwidth
consumerViewport demandknown dependencies
adapterBatch + admissionbytes / identities
adapterRange transportshared bandwidth
Viewport demand → Batch + admission → Range transport
3 of 5 in this journeyNext: Let the network wait
Adapter / 04Executable example

Let the network wait

New requirementRemote bytes arrive asynchronously while codec work still needs CPU time.

An AsyncSource supplies input, a Compute implementation schedules codec work, and decode_async connects them. The runnable fixture uses BytesSource and Inline so it is deterministic; a service supplies its actual source and execution policy.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup15 lines
use std::sync::Arc;
use zenbitmaps::QoiDecoderConfig;
use zencodec::decode::DecoderConfig;
use zencodec_io::tokio_io::{BytesSource, Inline, decode_async};
use zencodec_io::{CollectSink, DriveOptions};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub async fn asynchronous(bytes: Arc<[u8]>) -> Result<CollectSink, Error> {
    let decoder = QoiDecoderConfig::new()
        .job()
        .into_incremental_decoder(&[])?;
    let sink = CollectSink::new();
    // BytesSource is the deterministic stand-in for an application's AsyncSource.
    let mut source = BytesSource(bytes);
    let compute = Inline;
    let (_, sink) = decode_async(
        decoder,
        sink,
        &mut source,
        compute,
        DriveOptions::default(),
        None,
    )
    .await?;
Return & continuation2 lines
    Ok(sink)
}

Signatures from this revision · bodies omitted

Function · zencodec-io feature: tokiozencodec_io::tokio_io::decode_async
pub fn decode_async<'a, D, S, Src, C>(
    decoder: D,
    sink: S,
    source: &'a mut Src,
    compute: C,
    opts: DriveOptions,
    pulse: Option<SharedPulse>,
) -> impl Future<Output = Result<(OutputInfo, S), AsyncDriveError<D::Error>>> + Send + 'a
where
    D: IncrementalDecode + Send + 'static,
    S: DecodeRowSink + Send + 'static,
    Src: AsyncSource + ?Sized,
    C: Compute + 'a;

Selected methods · 11 more in the full contract.

Read defaults, imports and full contract ↗
Trait methods · zencodec-io feature: tokiozencodec_io::tokio_io::AsyncSource
pub trait AsyncSource: Send {
    fn read_at(
        &mut self,
        offset: u64,
        out: &mut [u8],
    ) -> impl Future<Output = Result<usize, BoxedError>> + Send;

    fn read_ranges(
        &mut self,
        reqs: &mut [(u64, &mut [u8])],
    ) -> impl Future<Output = Result<(), BoxedError>> + Send;

    fn len_hint(&self) -> Option<u64>;

    fn len(&mut self) -> impl Future<Output = Result<u64, BoxedError>> + Send;
}

Required methods: read_at, len. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methods · zencodec-io feature: tokiozencodec_io::tokio_io::Compute
pub trait Compute: Send + Sync {
    fn spawn(&self, job: Job);

    fn runs_inline(&self) -> bool;
}

Required methods: spawn. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Waiting for bytes and doing CPU work are different operations. The adapter decides where each runs; the codec keeps its synchronous step contract.

AsyncSource: await missing bytes → Compute: schedule codec work → Decoder + sink: resume from stateADAPTERAsyncSourceawait missing bytesADAPTERComputeschedule codec workIMPLEMENTORDecoder + sinkresume from state
adapterAsyncSourceawait missing bytes
adapterComputeschedule codec work
implementorDecoder + sinkresume from state
AsyncSource → Compute → Decoder + sink
4 of 5 in this journeyNext: A client stops reading
Adapter / 05Executable example

A client stops reading

New requirementThe output socket stalls. The encoder may still own a large uncompressed image.

Choose the waiting policy from storage lifetimes. A buffered encode should finish and release raw state into independently admitted encoded storage before the writer stalls. A verified sequential configuration can instead couple production to a bounded output queue.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup15 lines
use std::sync::Arc;
use zenbitmaps::{QoiDecoderConfig, QoiEncoderConfig};
use zencodec::decode::DecoderConfig;
use zencodec::encode::{EncodeJob, EncodeOutput, EncoderConfig};
use zencodec_io::tokio_io::{BytesSource, Inline, decode_async};
use zencodec_io::{DriveOptions, EncoderSink};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub async fn finish_before_write(bytes: Arc<[u8]>) -> Result<EncodeOutput, Error> {
    let decoder = QoiDecoderConfig::new()
        .job()
        .into_incremental_decoder(&[])?;
    let encoder = QoiEncoderConfig::new().job().encoder()?;
    let mut source = BytesSource(bytes);
    let compute = Inline;
    let sink = EncoderSink::new(encoder);
    let (_, sink) = decode_async(
        decoder,
        sink,
        &mut source,
        compute,
        DriveOptions::default(),
        None,
    )
    .await?;
    let output = sink.finish()?;
    // The encoder is consumed before the application awaits its output writer.
Return & continuation2 lines
    Ok(output)
}

Signatures from this revision · bodies omitted

Inherent methodszencodec_io::EncoderSink<E>
impl<E: Encoder> EncoderSink<E>
where
    E::Error: Send + Sync, {
    pub fn new(encoder: E) -> Self;

    pub fn finish(mut self) -> Result<EncodeOutput, EncoderSinkError<E::Error>>;
}

Selected methods · 8 more in the full contract.

Read defaults, imports and full contract ↗
Function · zencodec-io feature: tokiozencodec_io::tokio_io::decode_async
pub fn decode_async<'a, D, S, Src, C>(
    decoder: D,
    sink: S,
    source: &'a mut Src,
    compute: C,
    opts: DriveOptions,
    pulse: Option<SharedPulse>,
) -> impl Future<Output = Result<(OutputInfo, S), AsyncDriveError<D::Error>>> + Send + 'a
where
    D: IncrementalDecode + Send + 'static,
    S: DecodeRowSink + Send + 'static,
    Src: AsyncSource + ?Sized,
    C: Compute + 'a;

Selected methods · 11 more in the full contract.

Read defaults, imports and full contract ↗
Trait methodszencodec::encode::Encoder
pub trait Encoder: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    fn begin(
        &mut self,
        width: u32,
        height: u32,
        descriptor: zenpixels::PixelDescriptor,
    ) -> Result<crate::encode::OutputTiming, Self::Error>;

    fn push_rows(&mut self, _rows: PixelSlice<'_>) -> Result<(), Self::Error>;

    fn pending_output(&self) -> &[u8];

    fn consume_output(&mut self, n: usize);

    fn finish(self) -> Result<EncodeOutput, Self::Error>;
}

Selected methods · 10 more in the full contract.

Required methods: reject, encode. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

The ownership test holds 786432 raw bytes at the first writer stall in the current driver control, versus zero after finish-first. Both controls report streaming output and emit the same 4255 bytes.

Encoder state: raw + intermediates → Finish / release: admitted encoded storage → Output writer: may stay pendingIMPLEMENTOREncoder stateraw + intermediatesADAPTERFinish / releaseadmitted encoded storageCONSUMEROutput writermay stay pending
implementorEncoder stateraw + intermediates
adapterFinish / releaseadmitted encoded storage
consumerOutput writermay stay pending
Encoder state → Finish / release → Output writer
5 of 5 in this journeyExplore another perspective
Implementor / 01Executable example

Connect an existing core

New requirementYour core already decodes a complete image correctly.

Wrap a DecodeJob with WholeFileIncremental and the fused error/termination adapter. The same driver ecosystem can now serve it. The fixture wraps a real QOI job so the bridge is checked against the native path.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup8 lines
use zenbitmaps::QoiDecoderConfig;
use zencodec::decode::{DecoderConfig, erase_incremental};
use zencodec::helpers::WholeFileIncremental;
use zencodec_io::{CollectSink, drive_from_slice};
type Error = Box<dyn std::error::Error + Send + Sync>;

pub fn bridge(bytes: &[u8]) -> Result<CollectSink, Error> {
    let job = QoiDecoderConfig::new().job();
    let mut decoder = erase_incremental(WholeFileIncremental::new(job, &[]));
Return & continuation4 lines
    let mut sink = CollectSink::new();
    drive_from_slice(decoder.as_mut(), bytes, &mut sink, None, 3)?;
    Ok(sink)
}

Signatures from this revision · bodies omitted

Inherent methodszencodec::helpers::WholeFileIncremental<J>
impl<J> WholeFileIncremental<J>
where
    J: for<'a> DecodeJob<'a> + Send + 'static, {
    pub fn new(job: J, preferred: &[PixelDescriptor]) -> Self;
}

Selected methods · 1 more in the full contract.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::DecodeJob<'a>
pub trait DecodeJob<'a>: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    type Dec: Decode<Error = Self::Error>;

    type StreamDec: StreamingDecode<Error = Self::Error> + Send;

    type AnimationFrameDec: AnimationFrameDecoder<Error = Self::Error> + Send + 'static;

    fn with_limits(self, limits: ResourceLimits) -> Self;

    fn with_pulse(self, pulse: how_far::SharedPulse) -> Self;

    fn decoder(
        self,
        data: Cow<'a, [u8]>,
        preferred: &[PixelDescriptor],
    ) -> Result<Self::Dec, Self::Error>;

    fn dyn_incremental_decoder(
        self,
        preferred: &[PixelDescriptor],
    ) -> Result<Box<super::incremental::DynIncrementalDecoder>, BoxedError>;
}

Selected methods · 23 more in the full contract.

Required methods: with_pulse, with_limits, probe, output_info, decoder, push_decoder, streaming_decoder, animation_frame_decoder. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Integration does not require rewriting a correct codec core into a state machine before the first usable result.

Input driver: collect encoded input → WholeFileIncremental: existing push decode → Final rows: arrive at the endADAPTERInput drivercollect encoded inputIMPLEMENTORWholeFileIncrementalexisting push decodeCONSUMERFinal rowsarrive at the end
adapterInput drivercollect encoded input
implementorWholeFileIncrementalexisting push decode
consumerFinal rowsarrive at the end
Input driver → WholeFileIncremental → Final rows
Implementor / 02Current contract

Produce the first useful rows

New requirementWaiting for the complete upload is now the bottleneck, and the codec core can resume.

Implement IncrementalDecode with explicit parsing state. Request the contiguous bytes needed for one useful unit; consume only a prefix. Return Need with requests, Rows after writing final rows, or Done after finishing the sink. Check cancellation and truncation.

Signatures from this revision · bodies omitted

Trait methodszencodec::decode::IncrementalDecode
pub trait IncrementalDecode {
    type Error: core::error::Error + Send + Sync + 'static;

    fn step(
        &mut self,
        input: Input<'_>,
        sink: &mut dyn DecodeRowSink,
        pulse: Option<&dyn crate::Pulse>,
    ) -> Result<Progress, StepError<Self::Error>>;

    fn requested(&self) -> &[InputRequest];

    fn info(&self) -> Option<&ImageInfo>;

    fn take_details(&mut self) -> Option<crate::decode::DecodeDetails>;

    fn access_pattern(&self) -> crate::source::AccessPattern;
}

Selected methods · 1 more in the full contract.

Required methods: step, requested, info. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::DecodeRowSink
pub trait DecodeRowSink {
    fn begin(
        &mut self,
        _width: u32,
        _height: u32,
        _descriptor: PixelDescriptor,
    ) -> Result<(), SinkError>;

    fn provide_next_buffer(
        &mut self,
        y: u32,
        height: u32,
        width: u32,
        descriptor: PixelDescriptor,
    ) -> Result<PixelSliceMut<'_>, SinkError>;

    fn finish(&mut self) -> Result<(), SinkError>;
}

Selected methods · 3 more in the full contract.

Required methods: provide_next_buffer. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Resumability earns its complexity when it shortens time to useful work or reduces retained input. Frame iteration alone does not prove byte-incremental decoding.

Need bytes?: request or fail at EOF → Offer a span: offset + total length → Write final rows: provide_next_bufferIMPLEMENTORNeed bytes?request or fail at EOFADAPTEROffer a spanoffset + total lengthIMPLEMENTORWrite final rowsprovide_next_buffer
implementorNeed bytes?request or fail at EOF
adapterOffer a spanoffset + total length
implementorWrite final rowsprovide_next_buffer
Need bytes? → Offer a span → Write final rows
Implementor / 03Current contract

Describe this encode honestly

New requirementA caller wants to send encoded bytes before the last row arrives.

Implement begin, push_rows, pending_output, consume_output, and finish. Decide timing from the actual configuration and image. Streaming output promises bounded row lag; AT_FINISH keeps output hidden until finalization. Refuse row encoding when it is unsupported.

Signatures from this revision · bodies omitted

Trait methodszencodec::encode::Encoder
pub trait Encoder: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    fn begin(
        &mut self,
        width: u32,
        height: u32,
        descriptor: zenpixels::PixelDescriptor,
    ) -> Result<crate::encode::OutputTiming, Self::Error>;

    fn push_rows(&mut self, _rows: PixelSlice<'_>) -> Result<(), Self::Error>;

    fn pending_output(&self) -> &[u8];

    fn consume_output(&mut self, n: usize);

    fn finish(self) -> Result<EncodeOutput, Self::Error>;
}

Selected methods · 10 more in the full contract.

Required methods: reject, encode. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methodszencodec::encode::EncoderConfig
pub trait EncoderConfig: Clone + Send + Sync {
    type Error: core::error::Error + Send + Sync + 'static;

    type Job: EncodeJob<Error = Self::Error>;

    fn format() -> ImageFormat;

    fn supported_descriptors() -> &'static [PixelDescriptor];

    fn capabilities() -> &'static EncodeCapabilities;

    fn job(self) -> Self::Job;

    fn encode(self, pixels: PixelSlice<'_>) -> Result<EncodeOutput, Self::Error>
    where
        Self: Sized,
        <Self::Job as EncodeJob>::Enc: Encoder<Error = Self::Error>;
}

Selected methods · 16 more in the full contract.

Required methods: format, supported_descriptors, job. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Progressive coding, optimized tables, and a sequential configuration can have different storage and output behavior inside one codec.

Requested settings: do not silently change → begin declaration: stream / finish / refuse → Drain output: partial writes allowedCONSUMERRequested settingsdo not silently changeIMPLEMENTORbegin declarationstream / finish / refuseADAPTERDrain outputpartial writes allowed
consumerRequested settingsdo not silently change
implementorbegin declarationstream / finish / refuse
adapterDrain outputpartial writes allowed
Requested settings → begin declaration → Drain output
Implementor / 04Current contract

Prove every advertised path

New requirementThe adapter now has several paths that must produce the same pixels.

Run zencodec-testkit::check_all with the actual encode/decode configurations. Repeat streaming honesty checks for a configuration that streams when the default does not. Compare row, incremental, and one-shot pixels; exercise partial drains, cancellation, limits, and error termination.

Signatures from this revision · bodies omitted

Functionzencodec_testkit::check_all
pub fn check_all<E, D>(enc: E, dec: D) -> Conformance
where
    E: EncoderConfig,
    D: DecoderConfig,
    <E::Job as EncodeJob>::Enc: Encoder<Error = E::Error>,
    <E::Job as EncodeJob>::AnimationFrameEnc: AnimationFrameEncoder;

Selected methods · 63 more in the full contract.

Read defaults, imports and full contract ↗
Trait methodszencodec::encode::EncoderConfig
pub trait EncoderConfig: Clone + Send + Sync {
    type Error: core::error::Error + Send + Sync + 'static;

    type Job: EncodeJob<Error = Self::Error>;

    fn format() -> ImageFormat;

    fn supported_descriptors() -> &'static [PixelDescriptor];

    fn capabilities() -> &'static EncodeCapabilities;

    fn job(self) -> Self::Job;

    fn encode(self, pixels: PixelSlice<'_>) -> Result<EncodeOutput, Self::Error>
    where
        Self: Sized,
        <Self::Job as EncodeJob>::Enc: Encoder<Error = Self::Error>;
}

Selected methods · 16 more in the full contract.

Required methods: format, supported_descriptors, job. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::DecoderConfig
pub trait DecoderConfig: Clone + Send + Sync {
    type Error: core::error::Error + Send + Sync + 'static;

    type Job<'a>: DecodeJob<'a, Error = Self::Error> + 'static;

    fn decode(self, data: &[u8]) -> Result<DecodeOutput, Self::Error>
    where
        Self: Sized;

    fn probe(&self, data: &[u8]) -> Result<ImageInfo, Self::Error>
    where
        Self: Sized;

    fn job<'a>(self) -> Self::Job<'a>;
}

Selected methods · 4 more in the full contract.

Required methods: formats, supported_descriptors, job. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

A capability flag is a promise to a format-agnostic caller. A test of only the default configuration cannot prove a different mode.

Capability claim: true and false branches → Adversarial driver: chunks / stalls / limits → Exact result: pixels + metadata policyIMPLEMENTORCapability claimtrue and false branchesADAPTERAdversarial driverchunks / stalls / limitsCONSUMERExact resultpixels + metadata policy
implementorCapability claimtrue and false branches
adapterAdversarial driverchunks / stalls / limits
consumerExact resultpixels + metadata policy
Capability claim → Adversarial driver → Exact result
Implementor / 05Current contract

Share the model with video

New requirementThe same family carries stills and interdependent timed frames.

Implement packet/frame sessions separately from demuxing. send may require receive first; end_input drains delayed frames; reset discards queued work while returned frame handles stay valid. Muxers declare forward output and any final patches.

Signatures from this revision · bodies omitted

Trait methodszencodec_media::session::VideoDecoder
pub trait VideoDecoder {
    type Error: core::error::Error + Send + Sync + 'static;

    fn configure(
        &mut self,
        config: &CodecConfig,
        pulse: Option<&dyn zencodec::Pulse>,
    ) -> Result<(), SessionError<Self::Error>>;

    fn send(
        &mut self,
        packet: &mut Packet,
        pulse: Option<&dyn zencodec::Pulse>,
    ) -> Result<SendStatus, SessionError<Self::Error>>;

    fn receive(
        &mut self,
        pulse: Option<&dyn zencodec::Pulse>,
    ) -> Result<Receive<Frame>, SessionError<Self::Error>>;

    fn end_input(&mut self) -> Result<(), SessionError<Self::Error>>;

    fn reset(&mut self) -> Result<(), SessionError<Self::Error>>;
}

Selected methods · 1 more in the full contract.

Required methods: configure, send, receive, end_input, reset, stream_info. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Trait methodszencodec::decode::AnimationFrameDecoder
pub trait AnimationFrameDecoder: Sized {
    type Error: core::error::Error + Send + Sync + 'static;

    fn render_next_frame(
        &mut self,
        pulse: Option<&dyn crate::Pulse>,
    ) -> Result<Option<AnimationFrame<'_>>, Self::Error>;

    fn render_next_frame_owned(
        &mut self,
        pulse: Option<&dyn crate::Pulse>,
    ) -> Result<Option<OwnedAnimationFrame>, Self::Error>;
}

Selected methods · 5 more in the full contract.

Required methods: wrap_sink_error, info, render_next_frame, render_next_frame_to_sink. Other methods have defaults; read them before relying on their behavior.

Read defaults, imports and full contract ↗
Why add this?

Reference pictures, presentation order, and configuration epochs are codec facts. HTTP waits, cache generations, and global admission are adapter facts.

Demux + epoch: packet identity → send ↔ receive: references + delay → Owned frame: valid after resetADAPTERDemux + epochpacket identityIMPLEMENTORsend ↔ receivereferences + delayCONSUMEROwned framevalid after reset
adapterDemux + epochpacket identity
implementorsend ↔ receivereferences + delay
consumerOwned framevalid after reset
Demux + epoch → send ↔ receive → Owned frame
5 of 5 in this journeyExplore another perspective
The lab / reuse the simulator

Would another abstraction help?

Explore results from the reused IMI transport model. These are calculated network costs, with exact byte checks—not measured WAN timings.

Two visits to one viewport

16 independent 4 KiB ranges, separated by 4 KiB gaps. Repeat the same view. One shared 12.5 MB/s link.

The budgeted planner admits at most 64 KiB per view. Cached extents are logical accounting, not allocated memory.

Read policy → completion cost

Calculated transport results for the selected settings
PolicySimulated msBytes fetchedRead rounds

Download all 32 results ↓ Method & limitations ↗
A different question / when to wait

A small output queue can pin a large image.

Byte availability and storage release are different contracts. CPU fairness remains useful in both paths.

Choose where output waits from storage ownershipStart by checking whether this configuration has bounded raw and intermediate state and whether upstream input can pause. If yes, use a bounded output queue and wait. If no, admit encoded staging, finish the encoder to release raw state, then wait on the writer. If staging cannot be admitted, queue or refuse the job before starting.Bounded raw state?Upstream can pause?VERIFY THIS CONFIGURATIONYesNo / unknownBound the output queueWait without accumulating raw inputAdmit encoded stagingFinish → release raw → writeNo capacity? Queue or refuse before starting.Both paths still need CPU fairness, cancellation, and a bound on admitted jobs.
Decision / this configurationBounded raw state?
Upstream can pause?

Choose one path:

If yes / sequential pathBound the output queueWait without accumulating raw input.
If no or unknown / buffered pathAdmit encoded stagingFinish → release raw → write.
No capacity? Queue or refuse before starting.

Both paths need CPU fairness, cancellation, and a bound on admitted jobs.

Output policy flowchart · proposed scheduling distinction, not a new public capability flag.
Qualify the configuration. Couple encoding to a slow writer only when raw/intermediate storage is bounded and the upstream can pause. Otherwise, finish and release into independently admitted encoded storage; cap staging and concurrent jobs. A caller-owned full raster still counts. A memory cap may require refusal; it does not guarantee successful bounded completion.
Read the native QOI ownership control and its limits ↗
Policy / implemented calls and proposed coordination

One request, one resolver.

Implemented JPEG calls and private resolver experiments are marked separately below. All run in the qualification harness. Private prototype types are not exported dependencies; use the implemented examples for application code.

Stored request: required effects + preferences → Operation facts: qualified alternatives → Check requirements: offer feasible choicesCONSUMERStored requestrequired effects + preferencesIMPLEMENTOROperation factsqualified alternativesADAPTERCheck requirementsoffer feasible choices
consumerStored requestrequired effects + preferences
implementorOperation factsqualified alternatives
adapterCheck requirementsoffer feasible choices
Stored request → Operation facts → Check requirements
No feasible choice? Refuse with the failed requirements before calling application code. Estimates cannot certify a hard cap.

1. The gallery needs reliable metadata handling.

A profile binds requirements and preferences once. The consumer makes one call. The native QOI fixture refuses supplied rights retention because it has no EXIF carrier; missing metadata is not invented. Both concrete and CodecSet paths use the same stored request.

Private prototype · Rust
Source ↗
MoreNative tests ↗
Imports & setup16 lines
use crate::{
    native::{Report, Request, RunResult},
    policy::Profile,
};
use std::{convert::Infallible, io::Write};
use zencodec::{ImageFormat, Metadata};
use zenpixels::PixelSlice;

pub(crate) fn derivative(
    pixels: PixelSlice<'_>,
    metadata: &Metadata,
    writer: &mut impl Write,
) -> RunResult<Report, Infallible> {
    // This native fixture supports QOI/BMP, with no EXIF carrier.
    // Required supplied rights therefore cause a refusal before encoding.
    let request = Request::new(ImageFormat::Qoi, Profile::web_exif()).via_registry();
    let report = request.encode(pixels, metadata, writer)?;
Return & continuation3 lines
    // Success means bytes were accepted by Write, not durable publication.
    Ok(report)
}

2. Publication gains an account-specific rule.

Add one callback when the application has a conditional decision. A missing account rule returns its own traced error; a disabled account deliberately declines; enabled accounts delegate to the stored preset. None of these answers can weaken required effects.

Private prototype · Rust
Source ↗
MoreNative tests ↗
Imports & setup25 lines
use crate::{
    native::{Report, Request, RunResult},
    policy::{Decision, Decline, Profile, Question},
};
use std::{error::Error, fmt, io::Write};
use zencodec::{At, ImageFormat, Metadata};
use zenpixels::PixelSlice;

#[derive(Debug)]
pub(crate) struct AccountRuleUnavailable;
impl fmt::Display for AccountRuleUnavailable {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("account publication rule unavailable")
    }
}
impl Error for AccountRuleUnavailable {}

pub(crate) fn account_derivative(
    pixels: PixelSlice<'_>,
    metadata: &Metadata,
    writer: &mut impl Write,
    may_publish: Option<bool>,
) -> RunResult<Report, AccountRuleUnavailable> {
    // The request's required EXIF effects remain in force.
    let request = Request::new(ImageFormat::Qoi, Profile::web_exif()).via_registry();
    let mut resolver = |_: Question<'_>| -> Result<Decision, At<AccountRuleUnavailable>> {
        match may_publish {
            None => Err(At::wrap(AccountRuleUnavailable).at_named("account publication rule")),
            Some(false) => Ok(Decision::reject(Decline::ApplicationRule)),
            Some(true) => Ok(Decision::UsePreset),
        }
    };
    let report = request.encode_with(pixels, metadata, &mut resolver, writer)?;
Return & continuation3 lines
    // An account refusal, a missing rule, and a native error remain distinct.
    Ok(report)
}

Signatures from this revision · bodies omitted

Trait methodsprototype::PolicyResolver (private)
pub(crate) trait PolicyResolver {
    type Error: Error + Send + Sync + 'static;

    fn resolve(&mut self, question: Question<'_>) -> Result<Decision, At<Self::Error>>;
}
Read defaults, imports and full contract ↗
Inherent methodsprototype::Request (private)
impl Request {
    pub fn new(format: ImageFormat, profile: Profile) -> Self;

    pub fn via_registry(mut self) -> Self;

    pub fn encode(
        self,
        pixels: PixelSlice<'_>,
        metadata: &Metadata,
        writer: &mut impl Write,
    ) -> RunResult<Report, Infallible>;

    pub fn encode_with<R: PolicyResolver + ?Sized>(
        self,
        pixels: PixelSlice<'_>,
        metadata: &Metadata,
        resolver: &mut R,
        writer: &mut impl Write,
    ) -> RunResult<Report, R::Error>;
}
Read defaults, imports and full contract ↗
Choose / delegate: or traced refusal / error → Reserve + execute: recheck source and capacity → Result + decisions: cause, trace, partial progressCONSUMERChoose / delegateor traced refusal / errorADAPTERReserve + executerecheck source and capacityCONSUMERResult + decisionscause, trace, partial progress
consumerChoose / delegateor traced refusal / error
adapterReserve + executerecheck source and capacity
consumerResult + decisionscause, trace, partial progress
Choose / delegate → Reserve + execute → Result + decisions

3. A JPEG upload brings real metadata obligations.

The gallery now preserves JPEG scan data while removing descriptive metadata. The native rewrite below retains color and orientation; it does not decode or re-encode pixels. The plan borrows unchanged chunks, exposes exact output size and reports retained opaque carriers.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup4 lines
use zencodec::{MetadataPolicy, decode::BoxedError, metadata_rewrite::RewriteRequest};

pub(crate) fn derivative(source: &[u8]) -> Result<Vec<u8>, BoxedError> {
    // Actual JPEG rewrite; scan data is borrowed without pixel decoding.
    let request = RewriteRequest::new(source).with_policy(MetadataPolicy::ColorAndRotation);
    let plan = zenjpeg::metadata::rewrite(&request)?;
Return & continuation3 lines
    // This preserves opaque ICC. Success does not certify its privacy.
    Ok(plan.to_vec()?)
}

The preset is zencodec::MetadataPolicy::ColorAndRotation. It is a retention rule; it does not certify opaque data as safe to publish.

Signatures from this revision · bodies omitted

Inherent methodszencodec::metadata_rewrite::Plan
impl<'a> Plan<'a> {
    pub fn report(&self) -> &[Entry];

    pub fn chunks(&self) -> impl Iterator<Item = &[u8]>;

    pub fn output_len(&self) -> usize;

    pub fn to_vec(&self) -> Result<Vec<u8>, Error>;
}

Selected methods · 1 more in the full contract.

Read defaults, imports and full contract ↗
Inherent methodszencodec::metadata_rewrite::RewriteRequest
impl<'a> RewriteRequest<'a> {
    pub fn new(input: &'a [u8]) -> Self;

    pub fn with_policy(mut self, policy: MetadataPolicy) -> Self;
}

Selected methods · 20 more in the full contract.

Read defaults, imports and full contract ↗

4. An account requires retained ICC to be inspected.

Use the public plan report to enforce this narrow requirement today. Action is in zencodec::metadata_rewrite. With no qualified ICC inspector, refuse the retained profile before writing anything. Removing it and assuming sRGB would change the color contract. This check does not certify other carriers or pixels free of PII.

Implemented · main · Rust
Source ↗
MoreNative tests ↗
Imports & setup22 lines
use std::io::Write;
use zencodec::{
    MetadataPolicy,
    decode::BoxedError,
    metadata_rewrite::{Action, RewriteRequest},
};

#[derive(Debug)]
pub(crate) struct UninspectedIcc;
impl std::fmt::Display for UninspectedIcc {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("retained ICC has not been inspected")
    }
}
impl std::error::Error for UninspectedIcc {}

pub(crate) fn checked_derivative(
    source: &[u8],
    writer: &mut dyn Write,
) -> Result<(), BoxedError> {
    let request = RewriteRequest::new(source).with_policy(MetadataPolicy::ColorAndRotation);
    let plan = zenjpeg::metadata::rewrite(&request)?;
    if plan
        .report()
        .iter()
        .any(|entry| entry.action() == Action::OpaqueIcc)
    {
        return Err(UninspectedIcc.into());
    }
    // All checks precede the first write. Writer errors can leave partial output.
    for chunk in plan.chunks() {
        writer.write_all(chunk)?;
    }
Return & continuation2 lines
    Ok(())
}
Adapter design: share this rule across operations

A shared resolver becomes useful when account rules apply to multiple carriers and transcode routes. The private adapter below constructs the question, validates feasible choices and preserves traced errors. Ordinary callers should supply requirements and a preset or callback; they should not construct Prepared, Engine or authorization receipts. An impossible request fails before application code runs.

Private prototype · Rust
Source ↗
MoreNative tests ↗
Imports & setup21 lines
use crate::{
    policy::{Carrier, Context, Engine, PolicyResolver, PolicySet},
    policy_disclosure::{Obligation, Prepared, Privacy, TranscodeRoute},
};
use std::io::Write;
use zencodec::{
    MetadataPolicy,
    decode::BoxedError,
    metadata_rewrite::{Action, RewriteRequest},
};

pub(crate) fn require_inspected_icc<R: PolicyResolver>(
    source: &[u8],
    writer: &mut impl Write,
    resolver: &mut R,
) -> Result<(), BoxedError> {
    // An additional, deliberately narrow requirement: retained ICC must be
    // inspected. Other carrier obligations need their own qualification.
    let request = RewriteRequest::new(source).with_policy(MetadataPolicy::ColorAndRotation);
    let plan = zenjpeg::metadata::rewrite(&request)?;
    let mut engine = Engine::new(PolicySet::default(), 8);
    for entry in plan.report() {
        if entry.action() == Action::OpaqueIcc {
            Prepared::opaque(
                &source[entry.source_range()],
                Carrier::Icc,
                Obligation::UnknownCritical,
                Privacy::OrientationOnlyV1,
                TranscodeRoute::Rewrite,
            )
            .resolve(&mut engine, Context::default(), resolver, None)?;
        }
    }
Return & continuation8 lines
    // No bytes escape before every retained ICC obligation passes. This
    // prototype has no qualified ICC sanitizer, so opaque ICC refuses.
    for chunk in plan.chunks() {
        writer.write_all(chunk)?;
    }
    // A writer error can leave partial output; success is not a disk fsync.
    Ok(())
}

Signatures from this revision · bodies omitted

Inherent methodsprototype::Prepared (private)
impl<'a> Prepared<'a> {
    pub fn opaque(
        bytes: &'a [u8],
        carrier: Carrier,
        obligation: Obligation,
        privacy: Privacy,
        route: TranscodeRoute,
    ) -> Self;

    pub fn xmp(
        bytes: &'a [u8],
        service_available: bool,
        byte_limit: usize,
        obligation: Obligation,
        privacy: Privacy,
        route: TranscodeRoute,
    ) -> Self;

    pub fn resolve<R: PolicyResolver + ?Sized>(
        self,
        engine: &mut Engine,
        context: Context,
        resolver: &mut R,
        stop: Option<&dyn zencodec::Pulse>,
    ) -> Result<Approved<'a>, At<Failure<R::Error>>>;
}

Selected methods · 1 more in the full contract.

Read defaults, imports and full contract ↗

5. A stream reveals information after the first header.

Current zenjpeg can report a header at 4 KiB while retaining a whole-file input requirement. Its snapshot can contain half an ICC profile; full decode can later recover color, XMP and orientation. Even probe_full_metadata inherits the header probe. A complete inventory and a final interpretation need distinct guarantees.

Discover relevant fact: scope + revision + completeness → Suspend affected work: before conversion / begin / write → Resolve typed question: preset, conditional code or errorIMPLEMENTORDiscover relevant factscope + revision + completenessADAPTERSuspend affected workbefore conversion / begin / writeCONSUMERResolve typed questionpreset, conditional code or error
implementorDiscover relevant factscope + revision + completeness
adapterSuspend affected workbefore conversion / begin / write
consumerResolve typed questionpreset, conditional code or error
Discover relevant fact → Suspend affected work → Resolve typed question
Pending or provisional

Admit bounded holding or qualified lookahead. Do not start affected work. If capacity runs out, refuse.

Wait → new facts → resolve again
Final and compatible

Validate the choice and its source, revision and epoch. Resume only the effects that it authorizes.

Adopt → execute → report
Unavailable or incompatible

Offer only executable alternatives that satisfy the request. Otherwise return a traced error before prohibited output.

Refuse → preserve cause and progress

Signatures from this revision · bodies omitted

Inherent methodsprototype::Gate (private)
impl Gate {
    pub fn discover(&mut self, fact: Knowledge) -> Result<bool, At<GateCode>>;

    pub fn resolve<R: PolicyResolver + ?Sized>(
        &mut self,
        engine: &mut Engine,
        resolver: &mut R,
        stop: Option<&dyn zencodec::Pulse>,
    ) -> Result<Receipt, At<ResolveFailure<R::Error>>>;

    pub fn before_effect(&self, receipt: &Receipt) -> Result<(), At<GateCode>>;
}

Selected methods · 15 more in the full contract.

Read defaults, imports and full contract ↗

The suspension signatures are a tested private controller, not a production IncrementalDecode extension. Existing metadata notifications are informational and can arrive after sink calls; an observer alone cannot provide this barrier. The callback runs again for new relevant facts, not for every row, packet or writer stall. Header publication, encoder start, discarded originals and body publication are separate commitments.

For video, bind interpretation to the relevant track/configuration epoch and keep returned frames tied to their original epoch. An independently interpreted segment can admit new facts; that is an authored control here, not qualified native muxer reconfiguration.

Native JPEG results and independent oracle limits ↗ · Opaque carriers, transcode routes and suspension ↗ · Required production barrier #166 ↗

Proposed contract and API removal candidates ↗ · Scenario matrix and qualification gaps ↗ · Measured dispatch and allocation costs ↗

The record / claims you can inspect

Current, measured, proposed.

Protocol support, native incremental behavior, and performance qualification are separate claims. Every result has a scope.

READER / MODEL STUDY

Can a new reader use the API?

Two fresh Sol sessions, sequential site packets, preserved submissions and native grading. Initial coding passes changed from 2/4 to 4/4; this is a small model study, not a human success rate.

NATIVE / JPEG

Late metadata and configuration

Actual progressive/row encode, short-read input, opaque ICC admission and metadata rewrite. Independent ICC/EXIF/pixel checks distinguish inventory from interpretation.

TESTED / PRIVATE

Fallible policy resolver

Configured profiles, scoped choices, traced native causes, shared reservations, EXIF checks, real container packets and retained decision reports. Explicit gaps for native accounting and other metadata domains.

PROPOSED / API REVIEW

A minimal 0.3

Keep the consumer calls and byte/session protocols; review admission, interpretation, ownership and image selection. Concrete removal candidates and native experiment gates.

MEASURED / TESTED

Raw ownership at a stalled writer

786432 versus 0 logical retained raw bytes in a deliberate ownership control around native QOI; all outputs exactly 4255 bytes. Not a measurement of QOI internal memory.

SIMULATED

Remote range policy

32 cases through the reused IMI transport model. Exact returned bytes, shared bandwidth, cached extents. No DNS/TLS, retries, CPU time, cancellation, or concurrent views.

MEASURED

Disk and allocation behavior

Warm and per-file cache-miss reads, heaptrack, and perf. Cold results are mixed; there is no universal positional-read speedup claim.

TESTED / PINNED

Containers, video, and waiting PRs

MP4, WebM, Matroska and IVF transport parity; FFmpeg oracles; native AV1 sessions. WebP/JXL frame APIs remain distinct from byte-fed input. Full zenpipe pipeline remains untested here.

EXPERIMENTAL

Proposed admission contracts

Private candidate range plans, reservations, identity checks, and ordered retirement. No new public limits or scheduling API has been adopted from these experiments.

REPRODUCIBLE

Sources, logs, and raw evidence

Committed commands and pins, linked raw logs, and a hashed evidence archive. Resource wrapper RSS and logical ownership counters are reported separately.

The friction / keep it visible

Complexity has to earn its place.

These steps still need a better contract or more evidence. The guide links the open design question at the point it becomes relevant.

A nostdio-style Read/Seek adapter can help an existing synchronous parser. It does not supply suspension, all-required range plans, immutable object identity, or shared storage admission. Keep that bridge narrow; the current evidence does not justify a second core I/O hierarchy.