Skip to main content

rawshift_image/formats/
export.rs

1//! Encoder selection and per-implementation configuration.
2//!
3//! [`EncodeOptions`] mirrors [`DecodeOptions`](super::standard::DecodeOptions):
4//! it is an *implementation-keyed* enum — each variant names one output format
5//! *and* one backend, and carries that backend's configuration struct. A format
6//! that gains an alternative encoder simply gains another variant; there is no
7//! generic, implementation-agnostic option set.
8//!
9//! `EncodeOptions` is `#[non_exhaustive]` so the planned C/C++ encoder backends
10//! (libjpeg-turbo, MozJPEG, jpegli, libaom, SVT-AV1, libjxl) can be added
11//! without a breaking change. Their configuration structs are already defined
12//! below — see [`MozjpegEncodeConfig`] and friends — so the API surface is
13//! stable ahead of the implementations.
14
15#[cfg(feature = "dng-encode")]
16use crate::formats::dng_export::DngExportConfig;
17
18use super::standard::StandardFormat;
19use crate::core::CodecId;
20
21pub use crate::core::{BitDepth, MetadataEmbedOptions};
22
23/// An output container format produced by an [`EncodeOptions`] variant.
24///
25/// Distinct from [`StandardFormat`] because it additionally includes `Dng`,
26/// which is a RAW format rather than a standard delivery format.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum OutputFormat {
30    /// Portable Network Graphics.
31    Png,
32    /// JPEG.
33    Jpeg,
34    /// WebP.
35    WebP,
36    /// AV1 Image File Format.
37    Avif,
38    /// JPEG XL.
39    Jxl,
40    /// Adobe Digital Negative.
41    Dng,
42}
43
44impl OutputFormat {
45    /// A short human-readable name.
46    pub fn name(self) -> &'static str {
47        match self {
48            OutputFormat::Png => "PNG",
49            OutputFormat::Jpeg => "JPEG",
50            OutputFormat::WebP => "WebP",
51            OutputFormat::Avif => "AVIF",
52            OutputFormat::Jxl => "JPEG XL",
53            OutputFormat::Dng => "DNG",
54        }
55    }
56
57    /// The conventional lowercase file extension (without a leading dot).
58    pub fn extension(self) -> &'static str {
59        match self {
60            OutputFormat::Png => "png",
61            OutputFormat::Jpeg => "jpg",
62            OutputFormat::WebP => "webp",
63            OutputFormat::Avif => "avif",
64            OutputFormat::Jxl => "jxl",
65            OutputFormat::Dng => "dng",
66        }
67    }
68}
69
70/// Encoder-agnostic options shared by every backend.
71///
72/// Embedded as the `common` field of each per-implementation config struct so
73/// that metadata-embedding and output bit-depth are configured uniformly,
74/// independent of the chosen backend.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub struct CommonEncodeOptions {
78    /// Which metadata blocks to embed in the output container.
79    pub metadata: MetadataEmbedOptions,
80    /// Requested output bit depth.
81    ///
82    /// Encoders that cannot honour the request return
83    /// [`EncodeError::UnsupportedBitDepth`](crate::error::EncodeError::UnsupportedBitDepth)
84    /// rather than silently degrading.
85    pub bit_depth: BitDepth,
86}
87
88/// WebP encoding mode.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub enum WebPMode {
92    /// VP8L lossless compression.
93    Lossless,
94    /// VP8 lossy compression.
95    Lossy,
96}
97
98/// Chroma subsampling mode for JPEG encoders.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub enum JpegSubsampling {
102    /// 4:2:0 — chroma at quarter resolution. Smallest files; the usual default.
103    #[default]
104    Yuv420,
105    /// 4:2:2 — chroma at half horizontal resolution.
106    Yuv422,
107    /// 4:4:4 — full-resolution chroma. Largest files, best chroma fidelity.
108    Yuv444,
109}
110
111/// Rate-control strategy for AV1-based AVIF encoders.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114pub enum AvifRateControl {
115    /// Constant quality / constant quantizer — quality knob drives file size.
116    #[default]
117    ConstantQuality,
118    /// Constrained quality — quality target with a bitrate ceiling.
119    Constrained,
120}
121
122// ── Currently-implemented backend configs ─────────────────────────────────────
123
124/// Configuration for the `zune-png` PNG encoder.
125#[derive(Debug, Clone, PartialEq, Eq, Default)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127pub struct ZunePngEncodeConfig {
128    /// Encoder-agnostic options (metadata embedding, bit depth).
129    ///
130    /// PNG honours `BitDepth::Eight` and `BitDepth::Sixteen`.
131    pub common: CommonEncodeOptions,
132}
133
134/// Configuration for the `jpeg-encoder` (pure-Rust) JPEG encoder.
135#[derive(Debug, Clone, PartialEq, Eq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub struct JpegEncEncodeConfig {
138    /// Encoder-agnostic options. JPEG output is always 8-bit.
139    pub common: CommonEncodeOptions,
140    /// Quality, `1..=100`. Higher is better quality and larger files (monotonic).
141    /// Default: `90`.
142    pub quality: u8,
143}
144
145impl Default for JpegEncEncodeConfig {
146    fn default() -> Self {
147        Self {
148            common: CommonEncodeOptions::default(),
149            quality: 90,
150        }
151    }
152}
153
154/// Configuration for the `libwebp` WebP encoder.
155#[derive(Debug, Clone, PartialEq)]
156#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
157pub struct LibwebpEncodeConfig {
158    /// Encoder-agnostic options. WebP output is always 8-bit.
159    pub common: CommonEncodeOptions,
160    /// Lossy or lossless mode.
161    pub mode: WebPMode,
162    /// Quality, `0.0..=100.0`. For lossy this is image quality; for lossless it
163    /// is the compression effort. Higher quality is larger (lossy). Default: `75.0`.
164    pub quality: f32,
165    /// Compression method, `0` (fast) to `6` (slowest, best). Default: `4`.
166    pub method: u32,
167    /// Near-lossless preprocessing, `0..=100` (`100` = off). Lossless mode only.
168    /// Default: `100`.
169    pub near_lossless: u32,
170}
171
172impl LibwebpEncodeConfig {
173    /// Lossy encoding with sensible defaults.
174    pub fn lossy() -> Self {
175        Self {
176            common: CommonEncodeOptions::default(),
177            mode: WebPMode::Lossy,
178            quality: 75.0,
179            method: 4,
180            near_lossless: 100,
181        }
182    }
183
184    /// Lossless encoding with sensible defaults.
185    pub fn lossless() -> Self {
186        Self {
187            mode: WebPMode::Lossless,
188            ..Self::lossy()
189        }
190    }
191}
192
193impl Default for LibwebpEncodeConfig {
194    fn default() -> Self {
195        Self::lossy()
196    }
197}
198
199/// Configuration for the `ravif` (rav1e) AVIF encoder.
200#[derive(Debug, Clone, PartialEq, Eq)]
201#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
202pub struct RavifEncodeConfig {
203    /// Encoder-agnostic options. This backend currently produces 8-bit AVIF.
204    pub common: CommonEncodeOptions,
205    /// Quality, `0..=100`. Higher is better quality and larger files (monotonic).
206    /// Default: `80`.
207    pub quality: u8,
208    /// Encoding speed, `1` (slowest, best) to `10` (fastest). Default: `6`.
209    pub speed: u8,
210}
211
212impl Default for RavifEncodeConfig {
213    fn default() -> Self {
214        Self {
215            common: CommonEncodeOptions::default(),
216            quality: 80,
217            speed: 6,
218        }
219    }
220}
221
222/// Configuration for the `zune-jpegxl` JPEG XL encoder (`JxlSimpleEncoder`).
223#[derive(Debug, Clone, PartialEq)]
224#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
225pub struct ZuneJxlEncodeConfig {
226    /// Encoder-agnostic options. This backend currently produces 8-bit JXL.
227    pub common: CommonEncodeOptions,
228    /// Quality, `0.0..=100.0` (`0.0` requests lossless). Default: `0.0`.
229    pub quality: f32,
230    /// Effort, `1..=9` (higher is slower). `JxlSimpleEncoder` may ignore this.
231    /// Default: `7`.
232    pub effort: u8,
233}
234
235impl Default for ZuneJxlEncodeConfig {
236    fn default() -> Self {
237        Self {
238            common: CommonEncodeOptions::default(),
239            quality: 0.0,
240            effort: 7,
241        }
242    }
243}
244
245// ── Planned backend configs (API surface only — implementations pending) ──────
246//
247// These structs are defined now so the encode API is feature-complete and
248// documented ahead of the C/C++ backend work. They are not yet wired to an
249// `EncodeOptions` variant; each lands together with its backend in a follow-up.
250
251/// Configuration for the planned **libjpeg-turbo** JPEG encoder.
252#[derive(Debug, Clone, PartialEq, Eq)]
253#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
254pub struct LibjpegTurboEncodeConfig {
255    /// Encoder-agnostic options. JPEG output is always 8-bit.
256    pub common: CommonEncodeOptions,
257    /// Quality, `1..=100`. Higher is better quality and larger files (monotonic).
258    pub quality: u8,
259    /// Emit a progressive (multi-scan) JPEG instead of baseline.
260    pub progressive: bool,
261    /// Chroma subsampling mode.
262    pub subsampling: JpegSubsampling,
263}
264
265impl Default for LibjpegTurboEncodeConfig {
266    fn default() -> Self {
267        Self {
268            common: CommonEncodeOptions::default(),
269            quality: 90,
270            progressive: false,
271            subsampling: JpegSubsampling::Yuv420,
272        }
273    }
274}
275
276/// Configuration for the planned **MozJPEG** JPEG encoder.
277#[derive(Debug, Clone, PartialEq, Eq)]
278#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
279pub struct MozjpegEncodeConfig {
280    /// Encoder-agnostic options. JPEG output is always 8-bit.
281    pub common: CommonEncodeOptions,
282    /// Quality, `1..=100`. Higher is better quality and larger files (monotonic).
283    pub quality: u8,
284    /// Emit a progressive JPEG (MozJPEG enables this by default).
285    pub progressive: bool,
286    /// Enable trellis quantisation — slower, produces smaller files.
287    pub trellis: bool,
288    /// Chroma subsampling mode.
289    pub subsampling: JpegSubsampling,
290}
291
292impl Default for MozjpegEncodeConfig {
293    fn default() -> Self {
294        Self {
295            common: CommonEncodeOptions::default(),
296            quality: 85,
297            progressive: true,
298            trellis: true,
299            subsampling: JpegSubsampling::Yuv420,
300        }
301    }
302}
303
304/// Configuration for the planned **jpegli** JPEG encoder.
305#[derive(Debug, Clone, PartialEq)]
306#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
307pub struct JpegliEncodeConfig {
308    /// Encoder-agnostic options. JPEG output is always 8-bit.
309    pub common: CommonEncodeOptions,
310    /// Butteraugli distance: `0.0` is visually lossless; higher values produce
311    /// smaller files (monotonic). Used when `quality` is `None`.
312    pub distance: f32,
313    /// Optional `1..=100` quality; when `Some`, overrides `distance`.
314    pub quality: Option<u8>,
315    /// Emit a progressive JPEG.
316    pub progressive: bool,
317    /// Encode in the XYB color space (jpegli high-fidelity mode).
318    pub xyb: bool,
319    /// Chroma subsampling mode.
320    pub subsampling: JpegSubsampling,
321}
322
323impl Default for JpegliEncodeConfig {
324    fn default() -> Self {
325        Self {
326            common: CommonEncodeOptions::default(),
327            distance: 1.0,
328            quality: None,
329            progressive: true,
330            xyb: false,
331            subsampling: JpegSubsampling::Yuv420,
332        }
333    }
334}
335
336/// Configuration for the planned **libaom** AVIF encoder.
337#[derive(Debug, Clone, PartialEq, Eq)]
338#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
339pub struct LibaomEncodeConfig {
340    /// Encoder-agnostic options. libaom can produce 8/10/12-bit AVIF.
341    pub common: CommonEncodeOptions,
342    /// Constant-quantiser level, `0..=63`. Lower is better quality and larger
343    /// files (monotonic). Used under [`AvifRateControl::ConstantQuality`].
344    pub cq_level: u8,
345    /// Minimum quantizer, `0..=63`.
346    pub min_quantizer: u8,
347    /// Maximum quantizer, `0..=63`.
348    pub max_quantizer: u8,
349    /// Speed/quality trade-off, `0` (slowest, best) to `8` (fastest).
350    pub cpu_used: u8,
351    /// Rate-control strategy.
352    pub rate_control: AvifRateControl,
353}
354
355impl Default for LibaomEncodeConfig {
356    fn default() -> Self {
357        Self {
358            common: CommonEncodeOptions::default(),
359            cq_level: 30,
360            min_quantizer: 0,
361            max_quantizer: 63,
362            cpu_used: 6,
363            rate_control: AvifRateControl::ConstantQuality,
364        }
365    }
366}
367
368/// Configuration for the planned **SVT-AV1** AVIF encoder.
369#[derive(Debug, Clone, PartialEq, Eq)]
370#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
371pub struct SvtAv1EncodeConfig {
372    /// Encoder-agnostic options. SVT-AV1 can produce 8/10-bit AVIF.
373    pub common: CommonEncodeOptions,
374    /// Constant-Rate-Factor, `0..=63`. Lower is better quality and larger files
375    /// (monotonic).
376    pub crf: u8,
377    /// Encoder preset, `0` (slowest, best) to `13` (fastest).
378    pub preset: u8,
379}
380
381impl Default for SvtAv1EncodeConfig {
382    fn default() -> Self {
383        Self {
384            common: CommonEncodeOptions::default(),
385            crf: 35,
386            preset: 8,
387        }
388    }
389}
390
391/// Configuration for the planned **libjxl** JPEG XL encoder.
392#[derive(Debug, Clone, PartialEq)]
393#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
394pub struct LibjxlEncodeConfig {
395    /// Encoder-agnostic options. libjxl can produce up to 16-bit JXL.
396    pub common: CommonEncodeOptions,
397    /// Butteraugli distance: `0.0` is mathematically lossless; higher values
398    /// produce smaller files (monotonic).
399    pub distance: f32,
400    /// Effort, `1..=10` (higher is slower, better compression).
401    pub effort: u8,
402    /// Emit a responsive / progressive JPEG XL stream.
403    pub progressive: bool,
404}
405
406impl Default for LibjxlEncodeConfig {
407    fn default() -> Self {
408        Self {
409            common: CommonEncodeOptions::default(),
410            distance: 1.0,
411            effort: 7,
412            progressive: false,
413        }
414    }
415}
416
417// ── EncodeOptions ─────────────────────────────────────────────────────────────
418
419/// Selects the encoder implementation and carries its configuration.
420///
421/// Obtain one with [`EncodeOptions::default_for`] for a format's default
422/// backend, or with a constructor such as [`EncodeOptions::jpeg`], or by naming
423/// a variant directly to pin a specific backend.
424#[derive(Debug, Clone, PartialEq)]
425#[non_exhaustive]
426#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
427pub enum EncodeOptions {
428    /// PNG via `zune-png` (requires `png-encode`).
429    #[cfg(feature = "png-encode")]
430    PngZune(ZunePngEncodeConfig),
431    /// JPEG via the pure-Rust `jpeg-encoder` (requires `jpeg-encode`).
432    #[cfg(feature = "jpeg-encode")]
433    JpegJpegEnc(JpegEncEncodeConfig),
434    /// WebP via `libwebp` (requires `webp-encode`).
435    #[cfg(feature = "webp-encode")]
436    WebpLibwebp(LibwebpEncodeConfig),
437    /// AVIF via `ravif` / rav1e (requires `avif-encode`).
438    #[cfg(feature = "avif-encode")]
439    AvifRavif(RavifEncodeConfig),
440    /// JPEG XL via `zune-jpegxl` (requires `jxl-encode`).
441    #[cfg(feature = "jxl-encode")]
442    JxlZune(ZuneJxlEncodeConfig),
443    /// DNG via the in-repo encoder (requires `dng-encode`).
444    #[cfg(feature = "dng-encode")]
445    Dng(DngExportConfig),
446}
447
448#[cfg(feature = "png-encode")]
449impl Default for EncodeOptions {
450    fn default() -> Self {
451        Self::png()
452    }
453}
454
455impl EncodeOptions {
456    /// PNG with default configuration.
457    #[cfg(feature = "png-encode")]
458    pub fn png() -> Self {
459        Self::PngZune(ZunePngEncodeConfig::default())
460    }
461
462    /// JPEG with default configuration.
463    #[cfg(feature = "jpeg-encode")]
464    pub fn jpeg() -> Self {
465        Self::JpegJpegEnc(JpegEncEncodeConfig::default())
466    }
467
468    /// Lossy WebP with default configuration.
469    #[cfg(feature = "webp-encode")]
470    pub fn webp_lossy() -> Self {
471        Self::WebpLibwebp(LibwebpEncodeConfig::lossy())
472    }
473
474    /// Lossless WebP with default configuration.
475    #[cfg(feature = "webp-encode")]
476    pub fn webp_lossless() -> Self {
477        Self::WebpLibwebp(LibwebpEncodeConfig::lossless())
478    }
479
480    /// AVIF with default configuration.
481    #[cfg(feature = "avif-encode")]
482    pub fn avif() -> Self {
483        Self::AvifRavif(RavifEncodeConfig::default())
484    }
485
486    /// JPEG XL with default configuration.
487    #[cfg(feature = "jxl-encode")]
488    pub fn jxl() -> Self {
489        Self::JxlZune(ZuneJxlEncodeConfig::default())
490    }
491
492    /// DNG with default configuration.
493    #[cfg(feature = "dng-encode")]
494    pub fn dng() -> Self {
495        Self::Dng(DngExportConfig::default())
496    }
497
498    /// The output format this encoder produces.
499    pub fn format(&self) -> OutputFormat {
500        match self {
501            #[cfg(feature = "png-encode")]
502            EncodeOptions::PngZune(_) => OutputFormat::Png,
503            #[cfg(feature = "jpeg-encode")]
504            EncodeOptions::JpegJpegEnc(_) => OutputFormat::Jpeg,
505            #[cfg(feature = "webp-encode")]
506            EncodeOptions::WebpLibwebp(_) => OutputFormat::WebP,
507            #[cfg(feature = "avif-encode")]
508            EncodeOptions::AvifRavif(_) => OutputFormat::Avif,
509            #[cfg(feature = "jxl-encode")]
510            EncodeOptions::JxlZune(_) => OutputFormat::Jxl,
511            #[cfg(feature = "dng-encode")]
512            EncodeOptions::Dng(_) => OutputFormat::Dng,
513            // Unreachable: with no encode feature enabled `EncodeOptions` has
514            // no variants and no value of it can be constructed.
515            #[allow(unreachable_patterns)]
516            _ => unreachable!(),
517        }
518    }
519
520    /// The stable identifier of the selected encoder implementation.
521    pub fn codec_id(&self) -> CodecId {
522        match self {
523            #[cfg(feature = "png-encode")]
524            EncodeOptions::PngZune(_) => CodecId::new("png/zune"),
525            #[cfg(feature = "jpeg-encode")]
526            EncodeOptions::JpegJpegEnc(_) => CodecId::new("jpeg/jpeg-encoder"),
527            #[cfg(feature = "webp-encode")]
528            EncodeOptions::WebpLibwebp(_) => CodecId::new("webp/libwebp"),
529            #[cfg(feature = "avif-encode")]
530            EncodeOptions::AvifRavif(_) => CodecId::new("avif/ravif"),
531            #[cfg(feature = "jxl-encode")]
532            EncodeOptions::JxlZune(_) => CodecId::new("jxl/zune"),
533            #[cfg(feature = "dng-encode")]
534            EncodeOptions::Dng(_) => CodecId::new("dng/rawshift"),
535            #[allow(unreachable_patterns)]
536            _ => unreachable!(),
537        }
538    }
539
540    /// The encoder-agnostic options (metadata embedding, bit depth) in effect.
541    ///
542    /// For DNG — which has its own configuration shape — a `CommonEncodeOptions`
543    /// is synthesised from [`DngExportConfig`].
544    pub fn common(&self) -> CommonEncodeOptions {
545        match self {
546            #[cfg(feature = "png-encode")]
547            EncodeOptions::PngZune(c) => c.common,
548            #[cfg(feature = "jpeg-encode")]
549            EncodeOptions::JpegJpegEnc(c) => c.common,
550            #[cfg(feature = "webp-encode")]
551            EncodeOptions::WebpLibwebp(c) => c.common,
552            #[cfg(feature = "avif-encode")]
553            EncodeOptions::AvifRavif(c) => c.common,
554            #[cfg(feature = "jxl-encode")]
555            EncodeOptions::JxlZune(c) => c.common,
556            #[cfg(feature = "dng-encode")]
557            EncodeOptions::Dng(c) => CommonEncodeOptions {
558                metadata: MetadataEmbedOptions {
559                    embed_exif: c.embed_exif,
560                    embed_icc: false,
561                    embed_xmp: false,
562                },
563                bit_depth: BitDepth::Sixteen,
564            },
565            #[allow(unreachable_patterns)]
566            _ => unreachable!(),
567        }
568    }
569
570    /// The default encoder backend for `format`, with default configuration.
571    ///
572    /// Returns `None` when no encoder for `format` is compiled in, or when the
573    /// format has no encoder. DNG is absent — it is not a [`StandardFormat`];
574    /// use [`EncodeOptions::dng`].
575    pub fn default_for(format: StandardFormat) -> Option<EncodeOptions> {
576        match format {
577            #[cfg(feature = "png-encode")]
578            StandardFormat::Png => Some(EncodeOptions::png()),
579            #[cfg(feature = "jpeg-encode")]
580            StandardFormat::Jpeg => Some(EncodeOptions::jpeg()),
581            #[cfg(feature = "webp-encode")]
582            StandardFormat::WebP => Some(EncodeOptions::webp_lossy()),
583            #[cfg(feature = "avif-encode")]
584            StandardFormat::Avif => Some(EncodeOptions::avif()),
585            #[cfg(feature = "jxl-encode")]
586            StandardFormat::Jxl => Some(EncodeOptions::jxl()),
587            _ => None,
588        }
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    #[cfg(any_standard_encode)]
595    use super::*;
596
597    #[test]
598    #[cfg(feature = "jpeg-encode")]
599    fn default_for_jpeg_is_jpeg() {
600        let opts = EncodeOptions::default_for(StandardFormat::Jpeg).unwrap();
601        assert_eq!(opts.format(), OutputFormat::Jpeg);
602        assert_eq!(opts.codec_id().id, "jpeg/jpeg-encoder");
603    }
604
605    #[test]
606    #[cfg(feature = "png-encode")]
607    fn default_for_unencodable_format_is_none() {
608        assert!(EncodeOptions::default_for(StandardFormat::Gif).is_none());
609    }
610
611    #[test]
612    #[cfg(feature = "avif-encode")]
613    fn avif_default_quality_and_common() {
614        let opts = EncodeOptions::avif();
615        assert_eq!(opts.format(), OutputFormat::Avif);
616        assert_eq!(opts.common().bit_depth, BitDepth::Sixteen);
617    }
618
619    #[test]
620    fn output_format_names_and_extensions() {
621        assert_eq!(OutputFormat::Jxl.name(), "JPEG XL");
622        assert_eq!(OutputFormat::Avif.extension(), "avif");
623    }
624}