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.
usezenbitmaps::{BmpDecoderConfig, BmpEncoderConfig, QoiDecoderConfig, QoiEncoderConfig};usezencodec::decode::DecodeOutput;usezencodec::{CodecSet, CodecSetError};// Build once at application startup, then share by reference or Arc.pubfngallery_codecs() ->CodecSet {CodecSet::new().with_decoder(QoiDecoderConfig::new()).with_decoder(BmpDecoderConfig::new()).with_encoder(QoiEncoderConfig::new()).with_encoder(BmpEncoderConfig::new())}pubfnregistered_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)}
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.
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.
Required methods: with_pulse, with_limits, with_metadata, encoder, animation_frame_encoder. Other methods have defaults; read them before relying on their behavior.
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.
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.
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)}
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.
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.
usezenbitmaps::{BmpDecoderConfig, BmpEncoderConfig, QoiDecoderConfig, QoiEncoderConfig};usezencodec::decode::DecodeOutput;usezencodec::{CodecSet, CodecSetError};// Build once at application startup, then share by reference or Arc.pubfngallery_codecs() ->CodecSet {CodecSet::new().with_decoder(QoiDecoderConfig::new()).with_decoder(BmpDecoderConfig::new()).with_encoder(QoiEncoderConfig::new()).with_encoder(BmpEncoderConfig::new())}pubfnregistered_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)}
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.
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.
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.
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.
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))}
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.
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.
Required methods: with_pulse, with_limits, with_metadata, encoder, animation_frame_encoder. Other methods have defaults; read them before relying on their behavior.
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.
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.
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.
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.
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.
// Pass Some(pulse) to add a full borrowed reporting/cancellation control.drive_from_read_at( decoder.as_mut(), bytes,&mut sink,None,DriveOptions::default(), )?;
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.
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.
consumerViewport demandknown dependencies
↓
adapterBatch + admissionbytes / identities
↓
adapterRange transportshared bandwidth
Viewport demand → Batch + admission → Range transport
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.
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.
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.
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.
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.
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.
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.
Resumability earns its complexity when it shortens time to useful work or reduces retained input. Frame iteration alone does not prove byte-incremental decoding.
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.
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.
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.
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.
Reference pictures, presentation order, and configuration epochs are codec facts. HTTP waits, cache generations, and global admission are adapter facts.
Byte availability and storage release are different contracts. CPU fairness remains useful in both paths.
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.
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.
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.
usecrate::{native::{Report, Request, RunResult},policy::Profile,};usestd::{convert::Infallible, io::Write};usezencodec::{ImageFormat, Metadata};usezenpixels::PixelSlice;pub(crate) fnderivative( pixels:PixelSlice<'_>, metadata:&Metadata, writer:&mutimplWrite,) ->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.
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.
usezencodec::{MetadataPolicy, decode::BoxedError, metadata_rewrite::RewriteRequest};pub(crate) fnderivative(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.
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.
usestd::io::Write;usezencodec::{MetadataPolicy,decode::BoxedError,metadata_rewrite::{Action, RewriteRequest},};#[derive(Debug)]pub(crate) structUninspectedIcc;implstd::fmt::DisplayforUninspectedIcc {fnfmt(&self, f:&mutstd::fmt::Formatter<'_>) ->std::fmt::Result { f.write_str("retained ICC has not been inspected") }}implstd::error::ErrorforUninspectedIcc {}pub(crate) fnchecked_derivative( source:&[u8], writer:&mutdynWrite,) ->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) {returnErr(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.
usecrate::{policy::{Carrier, Context, Engine, PolicyResolver, PolicySet},policy_disclosure::{Obligation, Prepared, Privacy, TranscodeRoute},};usestd::io::Write;usezencodec::{MetadataPolicy,decode::BoxedError,metadata_rewrite::{Action, RewriteRequest},};pub(crate) fnrequire_inspected_icc<R:PolicyResolver>( source:&[u8], writer:&mutimplWrite, resolver:&mutR,) ->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)?;letmut 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(())}
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.
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.
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.