oxideav_avif/lib.rs
1//! AVIF (AV1 Image File Format) — the AV1 profile of HEIF / MIAF, with
2//! the ISOBMFF / HEIF container served by [`oxideav_heif`] and AV1
3//! pixel decode delegated to [`oxideav_av1`].
4//!
5//! # The image-crate API
6//!
7//! The root follows the workspace image-crate contract (see [`api`]):
8//! [`probe`] / [`info`] standalone; [`decode`] / [`decode_with`] /
9//! [`decode_rgb8`] / [`decode_rgba8`] / [`decode_all`] / [`decode_from`]
10//! and [`encode`] / [`encode_rgb8`] / [`encode_rgba8`] / [`encode_to`]
11//! with the default-on `registry` feature (the pixels come from the
12//! AV1 codec crate, which is framework-only). [`AvifImage`] is the
13//! decoded picture — planes + [`PixelFormat`] + [`ColorInfo`] +
14//! [`Metadata`] — with [`AvifImage::to_rgb8`] / [`AvifImage::to_rgba8`];
15//! [`AvifFile`] is the parsed container ([`parse`]).
16//!
17//! The one-screen example needs the default `registry` feature (the
18//! `decode*` / `encode*` paths go through the AV1 codec crate):
19//!
20#![cfg_attr(feature = "registry", doc = "```no_run")]
21#![cfg_attr(not(feature = "registry"), doc = "```ignore")]
22//! # fn main() -> Result<(), oxideav_avif::Error> {
23//! let bytes = std::fs::read("in.avif").map_err(oxideav_avif::Error::from)?;
24//! if oxideav_avif::probe(&bytes) {
25//! let info = oxideav_avif::info(&bytes)?;
26//! let img = oxideav_avif::decode(&bytes)?;
27//! let rgba: Vec<u8> = img.to_rgba8();
28//! let out = oxideav_avif::encode_rgba8(
29//! img.width(),
30//! img.height(),
31//! &rgba,
32//! &oxideav_avif::EncodeOptions::default().with_quality(90),
33//! )?;
34//! assert_eq!((info.width, info.height), (img.width(), img.height()));
35//! std::fs::write("out.avif", out).map_err(oxideav_avif::Error::from)?;
36//! }
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! # Layering
42//!
43//! * **Container (`oxideav-heif`)**: the box reader ([`box_parser`] is
44//! a thin surface over it), the `meta` item model — `hdlr`, `pitm`,
45//! `iinf` + `infe`, `iloc` (every construction method), `iref`,
46//! `iprp` / `ipco` / `ipma`, `idat`, `grpl` — item payload
47//! resolution, the typed `ispe` / `pixi` / `colr` / `pasp` / `clap` /
48//! `irot` / `imir` / `iscl` / `auxC` / `clli` / `amve` / `rloc` /
49//! `lsel` / `a1op` / `a1lx` / `rref` / `crtt` / `mdft` / `udes` /
50//! `altt` properties, the derived-image descriptors, pixel
51//! composition (grid / `iovl` / `iden`, alpha attachment, `clap` /
52//! `irot` / `imir`), the `moov` / `trak` / `stbl` sample tables and
53//! the still-image writer.
54//! * **AVIF profile (this crate)**: [`parse`] / [`parse_header`] build
55//! this crate's [`Meta`] from the container's, keep `av1C` raw for
56//! the AV1 layer, read `mdcv` / `cclv` and the HEIF-extension
57//! properties (`aebr` … `cmin`, the slideshow transition effects,
58//! `prdi`, `sstr`, `txlo`, `elng`, `fnch`, `mskC`) from the raw
59//! property bytes, and enforce the AVIF brand rule; the av1-avif
60//! §2–§8 audits ([`audit_avif_profile_compliance`],
61//! [`audit_alpha_bit_depth`], [`audit_sequence_header_obu`],
62//! [`audit_tone_map`], [`audit_avis_sequence`] …), the `sato`
63//! descriptor / evaluator (§4.2.3) and `tmap` detection (§4.2.2),
64//! the AV1 decode of every coded item with `lsel` / `a1op` layer
65//! selection, the AVIF-side composition rules (av1-avif §4.1
66//! same-depth alpha, nearest-neighbour alpha resize, the `ispe` /
67//! grid trim with AV1's ceiling chroma extents, monochrome overlays
68//! staying monochrome), the `avis` sample handling, the writer's
69//! AVIF layout (brands, item order, property sets, `auxC` URNs) and
70//! the pixel encoder's tile election.
71//!
72//! # Status
73//!
74//! * [`AvifDecoder`]'s `receive_frame` composites the result:
75//! * Grid items (HEIF §6.6.2) — decode each tile via `dimg` iref
76//! and stitch into the declared output rectangle (see [`grid`]).
77//! * Alpha auxiliary — AV1-coded monochrome item referenced via
78//! `auxl` + `auxC` URN (see [`alpha`]). The av1-avif v1.2.0 §4.1
79//! `shall` "AV1 Alpha Image Item shall be encoded with the same
80//! bit depth as the associated master AV1 Image Item" is audited
81//! at the container layer via [`audit_alpha_bit_depth`] /
82//! [`AlphaBitDepthAudit`], surfaced through
83//! [`AvifInfo::alpha_bit_depth_compliance`].
84//! * `irot` / `imir` / `clap` post-transforms (see [`transform`]).
85//! * AVIS image sequences — [`avis::parse_avis`] walks the first
86//! track through the container's sample-table parser and produces a
87//! flat frame-offset list with `(timescale, display_dims, samples,
88//! av1_codec_config, handler, sample_description_types)`.
89//! [`avis::sample_bytes`] resolves a sample's byte slice inside the
90//! source file. The registry-gated decoder
91//! ([`decoder::AvifDecoder::decode_avis_file`]) walks the table end
92//! to end, lifts the `AV1CodecConfigurationRecord` from `stsd` →
93//! `av01` → `av1C`, and fans every sample through a shared
94//! `oxideav_av1` registry decoder so inter-frame state is preserved.
95//! `Decoder::send_packet` dispatches to this path automatically
96//! when the file's brand classification flags it as a sequence. The
97//! av1-avif v1.2.0 §3 `shall`s on an AV1 Image Sequence track
98//! (handler `'pict'`, single `'av01'` sample description, identical
99//! Sequence Header OBUs across samples) are audited at the
100//! container layer via [`audit_avis_sequence`] /
101//! [`AvisSequenceCompliance`].
102//! * `pixi` (HEIF §6.5.6) and `pasp` (HEIF §6.5.4 / ISO/IEC 14496-12
103//! §8.5.2.1.1) are surfaced through [`AvifInfo`] — see
104//! [`AvifInfo::num_channels`], [`AvifInfo::max_bit_depth`],
105//! [`AvifInfo::is_monochrome`] and [`AvifInfo::has_square_pixels`].
106//!
107//! # Encoder
108//!
109//! The **container muxer** is implemented in [`mux`]: [`AvifMuxer`] /
110//! [`AvifGridMuxer`] / [`AvifOverlayMuxer`] / [`encode_still_av1`] emit
111//! a conformant AVIF file around an already-coded AV1 Image Item Data
112//! payload plus its `av1C` record — this crate decides the brands,
113//! item order and property sets, [`oxideav_heif::HeifWriter`]
114//! serialises the `meta` tree and `mdat` — with `ispe` / `pixi` /
115//! `colr` / `pasp` / `clap` / `irot` / `imir` item properties, an
116//! optional AV1-coded alpha auxiliary (`auxC` + `auxl`), and optional
117//! `grid` tiling (`dimg`). The output round-trips through this crate's
118//! own [`parse`] path pixel-consistently (the coded AV1 stream is copied
119//! verbatim).
120//!
121//! Turning raw pixels *into* that AV1 bitstream is the job of the
122//! [`still`] module ([`StillImage`] + [`encode_still`] /
123//! [`encode_still_grid`]): it drives `oxideav_av1`'s conformance-grade
124//! KEY-frame encoder across the full (bit depth × chroma format)
125//! matrix — including RGB(A) via the H.273 identity matrix, alpha
126//! auxiliaries, arbitrary extents (pad + `clap`), and grid tiling —
127//! then wraps the coded payload through the muxer. The registry
128//! [`Encoder`](encoder) surface ([`make_encoder`] / [`AvifEncoder`])
129//! rides the same pipeline: every video frame sent through
130//! `send_frame` yields one complete AVIF file packet. Callers holding
131//! an already-coded payload keep muxing it directly with
132//! [`AvifMuxer`].
133//!
134//! # Standalone vs registry-integrated
135//!
136//! The crate's default-on `registry` Cargo feature pulls in
137//! `oxideav-core` + `oxideav-av1` and exposes the
138//! `oxideav_core::Decoder` trait surface plus the [`register`] entry
139//! point. Disable the feature (`default-features = false`) for an
140//! `oxideav-core`-free build that still exposes:
141//!
142//! * The container parse ([`parser`], [`parse`], [`parse_header`],
143//! [`meta`]; [`box_parser`] over `oxideav_heif::boxes`).
144//! * The AVIS sample-table walker ([`avis::parse_avis`]).
145//! * The grid / overlay / alpha / transform composition entry points
146//! ([`grid`], [`overlay`], [`alpha`], [`transform`]) over the
147//! container's `compose` layer, operating on crate-local
148//! [`image::AvifFrame`] / [`image::AvifPixelFormat`].
149//! * Container-side inspection: [`inspect::inspect`],
150//! [`inspect::AvifInfo`], [`inspect::transforms_for`].
151//! * The CICP signalling helpers ([`cicp`]).
152//!
153//! Standalone callers that want pixel decode must pair this surface
154//! with their own AV1 decoder — the in-tree one ([`oxideav_av1`]) is
155//! pulled in only when `registry` is on.
156
157pub mod alpha;
158pub mod api;
159pub mod avis;
160pub mod box_parser;
161pub mod cicp;
162pub mod derived;
163pub mod error;
164mod frame_bridge;
165pub mod grid;
166pub mod image;
167pub mod inspect;
168pub mod meta;
169pub mod mux;
170pub mod overlay;
171pub mod parser;
172pub mod region;
173pub mod sample_group;
174pub mod transform;
175
176#[cfg(feature = "registry")]
177mod av1_config;
178
179#[cfg(feature = "registry")]
180pub mod api_codec;
181
182#[cfg(feature = "registry")]
183pub mod decoder;
184
185#[cfg(feature = "registry")]
186pub mod encoder;
187
188#[cfg(feature = "registry")]
189pub mod sequence;
190#[cfg(feature = "registry")]
191pub mod signal;
192#[cfg(feature = "registry")]
193pub mod still;
194
195pub use alpha::{composite_alpha, find_alpha_item_id, ALPHA_URN_PREFIX};
196pub use api::{
197 info, probe, AvifImage, ColorInfo, ColorRange, DecodeOptions, EncodeOptions, EncodeSpeed,
198 Frame, ImageInfo, Metadata, PixelFormat, Plane, RgbImage, RgbaImage, StillChroma,
199};
200pub use avis::{
201 audit_avis_profile_compliance, audit_avis_sequence, audit_edit_list, inspect_avis, parse_avis,
202 parse_prft, parse_producer_reference_times, parse_ssix, parse_subsegment_indexes, sample_bytes,
203 AvisInfo, AvisMeta, AvisProfileCompliance, AvisSequenceCompliance, EditListCompliance,
204 EditListEntry, ProducerReferenceTime, Sample, SubsegmentIndex, SubsegmentRange, HANDLER_PICT,
205 NTP_UNIX_EPOCH_OFFSET_SECONDS,
206};
207pub use cicp::{
208 effective_cicp, is_matrix_reserved, is_primaries_reserved, is_transfer_reserved, matrix_name,
209 primaries_name, transfer_name, CicpTriple,
210};
211pub use derived::{
212 audit_alpha_bit_depth, audit_avif_profile_compliance, audit_iden_derivations, audit_pred_brand,
213 audit_sequence_header_obu, audit_tone_map, build_derivation_graph, normalize_full_range_plane,
214 output_dims_from_reconstructed, parse_grpl, reconstructed_dims, resolve_grids,
215 resolve_iden_derivations, resolve_overlays, resolve_tone_maps, transform_chain,
216 AlphaBitDepthAudit, AvifProfile, AvifProfileCompliance, BracketingKind, DerivationGraph,
217 DerivationKind, DerivationNode, DimTransform, EntityGroup, GainMapChannel, GainMapMetadata,
218 GainMapRational, GridResolution, GridTilePlacement, IdenCompliance, IdenResolution,
219 ImageOverlay, Mif1Compliance, OverlayEntry, OverlayPlacement, OverlayResolution,
220 PredBrandCompliance, SampleTransform, SequenceHeaderObuAudit, Token, ToneMapCompliance,
221 ToneMapResolution, MAX_DERIVATION_DEPTH,
222};
223pub use error::{AvifError, Result};
224/// The crate error under the contract's name.
225pub type Error = AvifError;
226pub use grid::{composite_grid, ImageGrid};
227pub use image::{AvifFrame, AvifPixelFormat, AvifPlane};
228pub use inspect::{
229 coded_item_dependencies, derivation_graph, entity_groups, gain_map_metadata, inspect,
230 is_font_item, item_payload_bytes, region_items, region_items_for, text_items, text_items_for,
231 transforms_for, AvifInfo, CodedItemDependencies, ResolvedRegionItem, ResolvedTextItem,
232};
233pub use meta::{
234 A1lx, A1op, Aebr, Afbr, Altt, AuxC, AuxKind, Cclv, Clap, Clli, Cmex, Cmin, Colr, Crtt, Dobr,
235 Elng, Fade, Fnch, Fobr, Imir, IrefEntry, Irot, Iscl, Ispe, ItemInfo, ItemLocation, Lsel, MaskC,
236 Mdcv, Mdft, Meta, Pano, PanoGrid, Pasp, Pixi, Prdi, Property, Rloc, Rref, Splt, Ssld, Sstr,
237 Stpe, Subs, SubsEntry, Tols, Txlo, Udes, Wbbr, Wipe, Zoom, AUX_URN_ALPHA_HEVC,
238 AUX_URN_ALPHA_MPEG, AUX_URN_DEPTH_HEVC, AUX_URN_DEPTH_MPEG, AUX_URN_HDR_GAINMAP,
239 ITEM_TYPE_EXIF, ITEM_TYPE_IDEN, ITEM_TYPE_IOVL, ITEM_TYPE_MIME, ITEM_TYPE_SATO, ITEM_TYPE_TMAP,
240 ITEM_TYPE_URI,
241};
242pub use mux::{
243 encode_still_av1, AvifGridMuxer, AvifMuxer, AvifOverlayMuxer, BurstAlpha, BurstMember,
244 EntityGroupSpec, GridTile, IdentityDerivation, OverlayLayer, ToneMapItem,
245};
246pub use overlay::{composite_overlay, OverlayInput, MAX_OVERLAY_CANVAS_PIXELS};
247pub use parser::{
248 audit_mif1, classify_brands, item_bytes, item_bytes_owned, item_bytes_owned_full,
249 item_bytes_owned_with_idat, item_bytes_with_idat, parse, parse_header, AvifFile, AvifHeader,
250 BrandClass, BRAND_AVIF, BRAND_AVIO, BRAND_AVIS, BRAND_MA1A, BRAND_MA1B, BRAND_MIAF, BRAND_MIF1,
251 BRAND_MSF1, ITEM_TYPE_AV01, ITEM_TYPE_GRID,
252};
253pub use region::{
254 resolve_derived_region_items, DerivedRegionItem, RegionGeometry, RegionItem, ITEM_TYPE_MSKI,
255 ITEM_TYPE_RGAN, REF_TYPE_DRGN,
256};
257pub use sample_group::{
258 parse_csgp, parse_sample_group_descriptions, parse_sample_to_groups, parse_sbgp, parse_sgpd,
259 BracketingEntry, SampleGroupDescription, SampleToGroup, SampleToGroupKind, SampleToGroupRun,
260 VisualEquivalenceEntry,
261};
262pub use transform::{apply_clap, apply_imir, apply_irot, crop_top_left};
263
264#[cfg(feature = "registry")]
265pub use api_codec::{
266 decode, decode_all, decode_all_with, decode_from, decode_rgb8, decode_rgba8, decode_with,
267 encode, encode_all, encode_rgb8, encode_rgba8, encode_to,
268};
269
270#[cfg(feature = "registry")]
271pub use decoder::{
272 make_decoder, AvifDecoder, AvifDecoderOptions, MAX_GRID_CANVAS_PIXELS, MAX_ITEM_DECODES,
273};
274
275#[cfg(feature = "registry")]
276pub use encoder::{
277 make_encoder, quality_to_base_q_idx, AvifEncoder, AvifEncoderOptions, ENCODER_PIXEL_FORMATS,
278};
279
280#[cfg(feature = "registry")]
281pub use sequence::{encode_sequence, SequenceEncodeOptions};
282
283#[cfg(feature = "registry")]
284pub use signal::{color_signal_for, labelled_pixel_format, miaf_default_signal};
285
286#[cfg(feature = "registry")]
287#[allow(deprecated)]
288pub use still::StillEncodeOptions;
289#[cfg(feature = "registry")]
290pub use still::{
291 elect_grid_tiling, encode_still, encode_still_auto, encode_still_burst, encode_still_grid,
292 encode_still_layered, encode_still_overlay, GainMapSpec, OverlayCanvas, OverlayLayerImage,
293 StillImage, StillProperties, GRID_MIN_TILE_DIM, STILL_MAX_CODED_DIM, TILE_LAYOUT_THREADS,
294};
295
296#[cfg(feature = "registry")]
297#[doc(hidden)]
298pub use registry_glue::__oxideav_entry;
299
300/// Public codec id string. Matches the aggregator-crate Cargo feature `avif`.
301pub const CODEC_ID_STR: &str = "avif";
302
303#[cfg(feature = "registry")]
304mod registry_glue {
305 //! Codec registry + AVIF encoder factory. Gated behind `registry`
306 //! because the entire framework integration depends on
307 //! `oxideav_core` + `oxideav_av1`.
308
309 use oxideav_core::{
310 CodecCapabilities, CodecId, CodecInfo, CodecRegistry, ContainerRegistry, Error,
311 RuntimeContext,
312 };
313
314 use crate::decoder::make_decoder;
315 use crate::encoder::make_encoder;
316 use crate::error::AvifError;
317 use crate::CODEC_ID_STR;
318
319 /// Bridge crate-local errors into the framework error type. Mirrors
320 /// the conversion the decoder performs internally so external
321 /// callers using the standalone container API get the same mapping
322 /// when they wrap up their own framework integration.
323 impl From<AvifError> for Error {
324 fn from(e: AvifError) -> Self {
325 match e {
326 AvifError::InvalidData(s) => Error::InvalidData(s),
327 AvifError::Unsupported(s) => Error::Unsupported(s),
328 AvifError::LimitExceeded(s) => Error::ResourceExhausted(s),
329 AvifError::Io(e) => Error::Io(e),
330 }
331 }
332 }
333
334 /// Register the AVIF decoder + encoder factories with a registry.
335 /// The decoder is declared `avif_heif_av1_decode` — we parse the
336 /// HEIF container end to end, hand the AV1 bitstream to
337 /// oxideav-av1, and composite grid / alpha / transform properties
338 /// on the resulting frames. The encoder factory ([`make_encoder`])
339 /// yields the pixel-in AVIF encoder (one complete AVIF file per
340 /// frame; see [`crate::encoder`] for the format/option surface).
341 ///
342 /// The capabilities declare what the encoder truly accepts
343 /// ([`crate::encoder::ENCODER_PIXEL_FORMATS`] — the set a
344 /// gateway's conversion ladder reads) and that it codes both
345 /// losslessly (`q` 0 / `quality` 100, the default) and lossily;
346 /// the option schemas ([`crate::encoder::AvifEncoderOptions`],
347 /// [`crate::decoder::AvifDecoderOptions`]) are attached so the
348 /// framework's advisory `quality` and `threads` reach the codec.
349 pub fn register_codecs(reg: &mut CodecRegistry) {
350 let caps = CodecCapabilities::video("avif_heif_av1_decode")
351 .with_decode()
352 .with_encode()
353 .with_lossy(true)
354 .with_lossless(true)
355 .with_intra_only(true)
356 .with_pixel_formats(crate::encoder::ENCODER_PIXEL_FORMATS.to_vec());
357 reg.register(
358 CodecInfo::new(CodecId::new(CODEC_ID_STR))
359 .capabilities(caps)
360 .decoder(make_decoder)
361 .encoder(make_encoder)
362 .encoder_options::<crate::encoder::AvifEncoderOptions>()
363 .decoder_options::<crate::decoder::AvifDecoderOptions>(),
364 );
365 }
366
367 /// Unified registration entry point: install both the AVIF codec
368 /// factories and the `.avif` / `.avifs` extension hints into a
369 /// [`RuntimeContext`].
370 ///
371 /// Note this does **not** also register the underlying AV1 codec —
372 /// callers that want both in one call should use
373 /// [`register_with_av1`] (which itself only touches the codec
374 /// sub-registry, since neither AVIF nor AV1 have container hooks
375 /// beyond `.avif*`).
376 ///
377 /// This is the preferred entry point for new code — it matches the
378 /// convention every sibling crate now follows. Direct callers that
379 /// only need one of the two sub-registries can keep using
380 /// [`register_codecs`] / [`register_containers`].
381 pub fn register(ctx: &mut RuntimeContext) {
382 register_codecs(&mut ctx.codecs);
383 register_containers(&mut ctx.containers);
384 }
385
386 oxideav_core::register!("avif", register);
387
388 /// Convenience: register AVIF **and** the underlying AV1 codec in
389 /// one call — the historical contract of this entry point,
390 /// restored now that `oxideav-av1`'s clean-room rebuild ships its
391 /// own registry surface again. Callers that only want the AVIF
392 /// factories keep using [`register_codecs`].
393 pub fn register_with_av1(reg: &mut CodecRegistry) {
394 register_codecs(reg);
395 oxideav_av1::registry::register_codecs(reg);
396 }
397
398 /// Register the `.avif` / `.avifs` extensions against the codec id
399 /// `"avif"` so consumers (cli-convert, pipeline output probing) can
400 /// resolve a `.avif` output path through the central
401 /// [`ContainerRegistry`] without a hard-coded extension list.
402 pub fn register_containers(reg: &mut ContainerRegistry) {
403 reg.register_extension("avif", CODEC_ID_STR);
404 reg.register_extension("avifs", CODEC_ID_STR);
405 }
406}
407
408#[cfg(feature = "registry")]
409pub use registry_glue::{register, register_codecs, register_containers, register_with_av1};
410
411#[cfg(all(test, feature = "registry"))]
412mod tests {
413 use super::*;
414 use oxideav_core::{
415 CodecId, CodecParameters, CodecRegistry, ContainerRegistry, Error, Frame, RuntimeContext,
416 };
417
418 #[test]
419 fn register_installs_factories() {
420 let mut reg = CodecRegistry::new();
421 register_codecs(&mut reg);
422 let id = CodecId::new(CODEC_ID_STR);
423 // The encoder factory yields the real pixel-in AVIF encoder:
424 // a registry-resolved encode of an 8x8 gray frame produces a
425 // parseable AVIF file.
426 let mut params = CodecParameters::video(id.clone());
427 params.width = Some(8);
428 params.height = Some(8);
429 params.pixel_format = Some(oxideav_core::PixelFormat::Gray8);
430 let mut enc = reg.first_encoder(¶ms).expect("encoder factory");
431 let frame = Frame::Video(oxideav_core::frame::VideoFrame {
432 pts: Some(0),
433 planes: vec![oxideav_core::frame::VideoPlane {
434 stride: 8,
435 data: (0..64u8).collect(),
436 }],
437 });
438 enc.send_frame(&frame).expect("send_frame");
439 let pkt = enc.receive_packet().expect("receive_packet");
440 crate::parser::parse(&pkt.data).expect("emitted packet is a parseable AVIF file");
441 // A frame without the required parameters is refused with a
442 // precise error, not a panic.
443 let bare = reg
444 .first_encoder(&CodecParameters::video(id))
445 .expect("encoder factory");
446 let mut bare = bare;
447 let empty = Frame::Video(oxideav_core::frame::VideoFrame {
448 pts: Some(0),
449 planes: vec![],
450 });
451 match bare.send_frame(&empty) {
452 Err(Error::InvalidData(msg)) => assert!(msg.contains("width"), "{msg}"),
453 other => panic!("encoder send_frame: expected InvalidData, got {other:?}"),
454 }
455 // Decoder factory succeeds; `send_packet` exercises the HEIF
456 // parse + AV1 decode pipeline.
457 let params = CodecParameters::video(CodecId::new(CODEC_ID_STR));
458 let _ = reg.first_decoder(¶ms).expect("decoder factory");
459 }
460
461 #[test]
462 fn avif_extension_resolves_to_avif_container() {
463 let mut reg = ContainerRegistry::new();
464 register_containers(&mut reg);
465 assert_eq!(reg.container_for_extension("avif"), Some(CODEC_ID_STR));
466 assert_eq!(reg.container_for_extension("AVIF"), Some(CODEC_ID_STR));
467 assert_eq!(reg.container_for_extension("avifs"), Some(CODEC_ID_STR));
468 }
469
470 #[test]
471 fn register_via_runtime_context_installs_codec_factory() {
472 let mut ctx = RuntimeContext::new();
473 register(&mut ctx);
474 let id = CodecId::new(CODEC_ID_STR);
475 let params = CodecParameters::video(id);
476 let _dec = ctx
477 .codecs
478 .first_decoder(¶ms)
479 .expect("avif decoder factory");
480 // The unified entry point also wires the .avif / .avifs
481 // extension hints through the same call.
482 assert_eq!(
483 ctx.containers.container_for_extension("avif"),
484 Some(CODEC_ID_STR)
485 );
486 assert_eq!(
487 ctx.containers.container_for_extension("avifs"),
488 Some(CODEC_ID_STR)
489 );
490 }
491}