rawshift-image 0.1.1

Still-image decoding, RAW processing, and encoding for rawshift
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
//! Encoder selection and per-implementation configuration.
//!
//! [`EncodeOptions`] mirrors [`DecodeOptions`](super::standard::DecodeOptions):
//! it is an *implementation-keyed* enum — each variant names one output format
//! *and* one backend, and carries that backend's configuration struct. A format
//! that gains an alternative encoder simply gains another variant; there is no
//! generic, implementation-agnostic option set.
//!
//! `EncodeOptions` is `#[non_exhaustive]` so the planned C/C++ encoder backends
//! (libjpeg-turbo, MozJPEG, jpegli, libaom, SVT-AV1) can be added without a
//! breaking change. Their configuration structs are already defined below — see
//! [`MozjpegEncodeConfig`] and friends — so the API surface is stable ahead of
//! the implementations. The libjxl backend ([`LibjxlEncodeConfig`]) is wired up
//! behind the `jxl-encode-libjxl` feature.

#[cfg(feature = "dng-encode")]
use crate::formats::dng_export::DngExportConfig;

use super::standard::StandardFormat;
use crate::core::CodecId;

pub use crate::core::{BitDepth, MetadataEmbedOptions};

/// An output container format produced by an [`EncodeOptions`] variant.
///
/// Distinct from [`StandardFormat`] because it additionally includes `Dng`,
/// which is a RAW format rather than a standard delivery format.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum OutputFormat {
    /// Portable Network Graphics.
    Png,
    /// JPEG.
    Jpeg,
    /// WebP.
    WebP,
    /// AV1 Image File Format.
    Avif,
    /// JPEG XL.
    Jxl,
    /// Adobe Digital Negative.
    Dng,
}

impl OutputFormat {
    /// A short human-readable name.
    pub fn name(self) -> &'static str {
        match self {
            OutputFormat::Png => "PNG",
            OutputFormat::Jpeg => "JPEG",
            OutputFormat::WebP => "WebP",
            OutputFormat::Avif => "AVIF",
            OutputFormat::Jxl => "JPEG XL",
            OutputFormat::Dng => "DNG",
        }
    }

    /// The conventional lowercase file extension (without a leading dot).
    pub fn extension(self) -> &'static str {
        match self {
            OutputFormat::Png => "png",
            OutputFormat::Jpeg => "jpg",
            OutputFormat::WebP => "webp",
            OutputFormat::Avif => "avif",
            OutputFormat::Jxl => "jxl",
            OutputFormat::Dng => "dng",
        }
    }
}

/// Encoder-agnostic options shared by every backend.
///
/// Embedded as the `common` field of each per-implementation config struct so
/// that metadata-embedding and output bit-depth are configured uniformly,
/// independent of the chosen backend.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CommonEncodeOptions {
    /// Which metadata blocks to embed in the output container.
    pub metadata: MetadataEmbedOptions,
    /// Requested output bit depth.
    ///
    /// Encoders that cannot honour the request return
    /// [`EncodeError::UnsupportedBitDepth`](crate::error::EncodeError::UnsupportedBitDepth)
    /// rather than silently degrading.
    pub bit_depth: BitDepth,
}

/// WebP encoding mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum WebPMode {
    /// VP8L lossless compression.
    Lossless,
    /// VP8 lossy compression.
    Lossy,
}

/// Chroma subsampling mode for JPEG encoders.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum JpegSubsampling {
    /// 4:2:0 — chroma at quarter resolution. Smallest files; the usual default.
    #[default]
    Yuv420,
    /// 4:2:2 — chroma at half horizontal resolution.
    Yuv422,
    /// 4:4:4 — full-resolution chroma. Largest files, best chroma fidelity.
    Yuv444,
}

/// Rate-control strategy for AV1-based AVIF encoders.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AvifRateControl {
    /// Constant quality / constant quantizer — quality knob drives file size.
    #[default]
    ConstantQuality,
    /// Constrained quality — quality target with a bitrate ceiling.
    Constrained,
}

// ── Currently-implemented backend configs ─────────────────────────────────────

/// Configuration for the `zune-png` PNG encoder.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ZunePngEncodeConfig {
    /// Encoder-agnostic options (metadata embedding, bit depth).
    ///
    /// PNG honours `BitDepth::Eight` and `BitDepth::Sixteen`.
    pub common: CommonEncodeOptions,
}

/// Configuration for the `jpeg-encoder` (pure-Rust) JPEG encoder.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct JpegEncEncodeConfig {
    /// Encoder-agnostic options. JPEG output is always 8-bit.
    pub common: CommonEncodeOptions,
    /// Quality, `1..=100`. Higher is better quality and larger files (monotonic).
    /// Default: `90`.
    pub quality: u8,
}

impl Default for JpegEncEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            quality: 90,
        }
    }
}

/// Configuration for the `libwebp` WebP encoder.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LibwebpEncodeConfig {
    /// Encoder-agnostic options. WebP output is always 8-bit.
    pub common: CommonEncodeOptions,
    /// Lossy or lossless mode.
    pub mode: WebPMode,
    /// Quality, `0.0..=100.0`. For lossy this is image quality; for lossless it
    /// is the compression effort. Higher quality is larger (lossy). Default: `75.0`.
    pub quality: f32,
    /// Compression method, `0` (fast) to `6` (slowest, best). Default: `4`.
    pub method: u32,
    /// Near-lossless preprocessing, `0..=100` (`100` = off). Lossless mode only.
    /// Default: `100`.
    pub near_lossless: u32,
}

impl LibwebpEncodeConfig {
    /// Lossy encoding with sensible defaults.
    pub fn lossy() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            mode: WebPMode::Lossy,
            quality: 75.0,
            method: 4,
            near_lossless: 100,
        }
    }

    /// Lossless encoding with sensible defaults.
    pub fn lossless() -> Self {
        Self {
            mode: WebPMode::Lossless,
            ..Self::lossy()
        }
    }
}

impl Default for LibwebpEncodeConfig {
    fn default() -> Self {
        Self::lossy()
    }
}

/// Configuration for the `ravif` (rav1e) AVIF encoder.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct RavifEncodeConfig {
    /// Encoder-agnostic options. This backend currently produces 8-bit AVIF.
    pub common: CommonEncodeOptions,
    /// Quality, `0..=100`. Higher is better quality and larger files (monotonic).
    /// Default: `80`.
    pub quality: u8,
    /// Encoding speed, `1` (slowest, best) to `10` (fastest). Default: `6`.
    pub speed: u8,
}

impl Default for RavifEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            quality: 80,
            speed: 6,
        }
    }
}

/// Configuration for the `zune-jpegxl` JPEG XL encoder (`JxlSimpleEncoder`).
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ZuneJxlEncodeConfig {
    /// Encoder-agnostic options. This backend currently produces 8-bit JXL.
    pub common: CommonEncodeOptions,
    /// Quality, `0.0..=100.0` (`0.0` requests lossless). Default: `0.0`.
    pub quality: f32,
    /// Effort, `1..=9` (higher is slower). `JxlSimpleEncoder` may ignore this.
    /// Default: `7`.
    pub effort: u8,
}

impl Default for ZuneJxlEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            quality: 0.0,
            effort: 7,
        }
    }
}

/// Modular vs VarDCT mode for the [`libjxl`](LibjxlEncodeConfig) encoder
/// (`JXL_ENC_FRAME_SETTING_MODULAR`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LibjxlModular {
    /// Let libjxl decide (VarDCT for lossy, modular for lossless). The default.
    #[default]
    Auto,
    /// Force VarDCT — best for photographic, lossy content.
    VarDct,
    /// Force modular — best for lossless, non-photographic, or high-bit-depth content.
    Modular,
}

/// Internal color transform for the [`libjxl`](LibjxlEncodeConfig) encoder
/// (`JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LibjxlColorTransform {
    /// Let libjxl decide (XYB for lossy). The default.
    #[default]
    Auto,
    /// XYB — the JPEG XL perceptual space; best lossy compression.
    Xyb,
    /// No transform — encode the RGB channels directly.
    None,
    /// YCbCr.
    YCbCr,
}

/// Configuration for the **libjxl** JPEG XL encoder (the reference encoder).
///
/// Unlike the `zune-jpegxl` default ([`ZuneJxlEncodeConfig`]), libjxl honours
/// 8- and 16-bit output, mathematically lossless encoding, and the full set of
/// `JxlEncoderFrameSettingId` toggles. Requires the `jxl-encode-libjxl` feature.
///
/// Integer fields default to a `-1` sentinel and `Option` fields to `None`,
/// meaning "leave at libjxl's own default". This is a plain struct (not
/// `#[non_exhaustive]`): construct it with a struct literal plus
/// `..Default::default()`. The two `extra_*_options` vectors are escape hatches
/// that pass raw frame-setting ids through verbatim, so every libjxl toggle is
/// reachable even if it has no dedicated field.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LibjxlEncodeConfig {
    /// Encoder-agnostic options. libjxl honours `BitDepth::Eight` and `BitDepth::Sixteen`.
    pub common: CommonEncodeOptions,
    /// Butteraugli distance: `0.0` is mathematically lossless, `1.0` visually
    /// lossless, up to `25.0` (smaller files, monotonic). Ignored when `quality`
    /// is `Some` or `lossless` is `true`.
    pub distance: f32,
    /// Optional JPEG-style quality, `0.0..=100.0`. When `Some`, libjxl converts
    /// it to a distance and it overrides `distance`.
    pub quality: Option<f32>,
    /// Force bit-exact lossless encoding (overrides `distance`/`quality`).
    pub lossless: bool,
    /// Effort / speed-quality trade-off, `1` (fastest) ..= `10` (slowest, best).
    pub effort: u8,
    /// Brotli effort for the metadata/modular streams, `-1` (default) or `0..=11`.
    pub brotli_effort: i8,
    /// Decoder-speed tier the stream is optimised for, `0` (default, best) ..= `4`.
    pub decoding_speed: u8,
    /// Emit a responsive / progressive stream (sets `RESPONSIVE` + `PROGRESSIVE_DC`).
    pub progressive: bool,
    /// Modular vs VarDCT mode.
    pub modular: LibjxlModular,
    /// Internal color transform.
    pub color_transform: LibjxlColorTransform,
    /// Edge-preserving filter strength, `-1` (default) ..= `3`.
    pub epf: i8,
    /// Gaborish deblocking filter: `None` = default, `Some(false)`/`Some(true)` = off/on.
    pub gaborish: Option<bool>,
    /// Adaptive noise synthesis: `None` = default, `Some(false)`/`Some(true)` = off/on.
    pub noise: Option<bool>,
    /// Dots synthesis: `None` = default, `Some(false)`/`Some(true)` = off/on.
    pub dots: Option<bool>,
    /// Patches synthesis: `None` = default, `Some(false)`/`Some(true)` = off/on.
    pub patches: Option<bool>,
    /// Photon-noise simulation strength in ISO units (`0.0` = off).
    pub photon_noise_iso: f32,
    /// Downsampling factor, `-1` (default), `1`, `2`, `4`, or `8`.
    pub resampling: i8,
    /// Force the BMFF container even when a bare codestream would do.
    pub use_container: bool,
    /// JPEG XL codestream feature level: `-1` (auto), `5`, or `10`.
    pub codestream_level: i8,
    /// Escape hatch: raw `(JxlEncoderFrameSettingId, value)` integer settings
    /// applied verbatim, for any toggle without a dedicated field above.
    pub extra_int_options: Vec<(i32, i64)>,
    /// Escape hatch: raw `(JxlEncoderFrameSettingId, value)` float settings.
    pub extra_float_options: Vec<(i32, f32)>,
}

impl Default for LibjxlEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            distance: 1.0,
            quality: None,
            lossless: false,
            effort: 7,
            brotli_effort: -1,
            decoding_speed: 0,
            progressive: false,
            modular: LibjxlModular::Auto,
            color_transform: LibjxlColorTransform::Auto,
            epf: -1,
            gaborish: None,
            noise: None,
            dots: None,
            patches: None,
            photon_noise_iso: 0.0,
            resampling: -1,
            use_container: false,
            codestream_level: -1,
            extra_int_options: Vec::new(),
            extra_float_options: Vec::new(),
        }
    }
}

// ── Planned backend configs (API surface only — implementations pending) ──────
//
// These structs are defined now so the encode API is feature-complete and
// documented ahead of the C/C++ backend work. They are not yet wired to an
// `EncodeOptions` variant; each lands together with its backend in a follow-up.

/// Configuration for the planned **libjpeg-turbo** JPEG encoder.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LibjpegTurboEncodeConfig {
    /// Encoder-agnostic options. JPEG output is always 8-bit.
    pub common: CommonEncodeOptions,
    /// Quality, `1..=100`. Higher is better quality and larger files (monotonic).
    pub quality: u8,
    /// Emit a progressive (multi-scan) JPEG instead of baseline.
    pub progressive: bool,
    /// Chroma subsampling mode.
    pub subsampling: JpegSubsampling,
}

impl Default for LibjpegTurboEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            quality: 90,
            progressive: false,
            subsampling: JpegSubsampling::Yuv420,
        }
    }
}

/// Configuration for the planned **MozJPEG** JPEG encoder.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MozjpegEncodeConfig {
    /// Encoder-agnostic options. JPEG output is always 8-bit.
    pub common: CommonEncodeOptions,
    /// Quality, `1..=100`. Higher is better quality and larger files (monotonic).
    pub quality: u8,
    /// Emit a progressive JPEG (MozJPEG enables this by default).
    pub progressive: bool,
    /// Enable trellis quantisation — slower, produces smaller files.
    pub trellis: bool,
    /// Chroma subsampling mode.
    pub subsampling: JpegSubsampling,
}

impl Default for MozjpegEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            quality: 85,
            progressive: true,
            trellis: true,
            subsampling: JpegSubsampling::Yuv420,
        }
    }
}

/// Configuration for the planned **jpegli** JPEG encoder.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct JpegliEncodeConfig {
    /// Encoder-agnostic options. JPEG output is always 8-bit.
    pub common: CommonEncodeOptions,
    /// Butteraugli distance: `0.0` is visually lossless; higher values produce
    /// smaller files (monotonic). Used when `quality` is `None`.
    pub distance: f32,
    /// Optional `1..=100` quality; when `Some`, overrides `distance`.
    pub quality: Option<u8>,
    /// Emit a progressive JPEG.
    pub progressive: bool,
    /// Encode in the XYB color space (jpegli high-fidelity mode).
    pub xyb: bool,
    /// Chroma subsampling mode.
    pub subsampling: JpegSubsampling,
}

impl Default for JpegliEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            distance: 1.0,
            quality: None,
            progressive: true,
            xyb: false,
            subsampling: JpegSubsampling::Yuv420,
        }
    }
}

/// Configuration for the planned **libaom** AVIF encoder.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LibaomEncodeConfig {
    /// Encoder-agnostic options. libaom can produce 8/10/12-bit AVIF.
    pub common: CommonEncodeOptions,
    /// Constant-quantiser level, `0..=63`. Lower is better quality and larger
    /// files (monotonic). Used under [`AvifRateControl::ConstantQuality`].
    pub cq_level: u8,
    /// Minimum quantizer, `0..=63`.
    pub min_quantizer: u8,
    /// Maximum quantizer, `0..=63`.
    pub max_quantizer: u8,
    /// Speed/quality trade-off, `0` (slowest, best) to `8` (fastest).
    pub cpu_used: u8,
    /// Rate-control strategy.
    pub rate_control: AvifRateControl,
}

impl Default for LibaomEncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            cq_level: 30,
            min_quantizer: 0,
            max_quantizer: 63,
            cpu_used: 6,
            rate_control: AvifRateControl::ConstantQuality,
        }
    }
}

/// Configuration for the planned **SVT-AV1** AVIF encoder.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SvtAv1EncodeConfig {
    /// Encoder-agnostic options. SVT-AV1 can produce 8/10-bit AVIF.
    pub common: CommonEncodeOptions,
    /// Constant-Rate-Factor, `0..=63`. Lower is better quality and larger files
    /// (monotonic).
    pub crf: u8,
    /// Encoder preset, `0` (slowest, best) to `13` (fastest).
    pub preset: u8,
}

impl Default for SvtAv1EncodeConfig {
    fn default() -> Self {
        Self {
            common: CommonEncodeOptions::default(),
            crf: 35,
            preset: 8,
        }
    }
}

// ── EncodeOptions ─────────────────────────────────────────────────────────────

/// Selects the encoder implementation and carries its configuration.
///
/// Obtain one with [`EncodeOptions::default_for`] for a format's default
/// backend, or with a constructor such as [`EncodeOptions::jpeg`], or by naming
/// a variant directly to pin a specific backend.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EncodeOptions {
    /// PNG via `zune-png` (requires `png-encode`).
    #[cfg(feature = "png-encode")]
    PngZune(ZunePngEncodeConfig),
    /// JPEG via the pure-Rust `jpeg-encoder` (requires `jpeg-encode`).
    #[cfg(feature = "jpeg-encode")]
    JpegJpegEnc(JpegEncEncodeConfig),
    /// WebP via `libwebp` (requires `webp-encode`).
    #[cfg(feature = "webp-encode")]
    WebpLibwebp(LibwebpEncodeConfig),
    /// AVIF via `ravif` / rav1e (requires `avif-encode`).
    #[cfg(feature = "avif-encode")]
    AvifRavif(RavifEncodeConfig),
    /// JPEG XL via `zune-jpegxl` (requires `jxl-encode`).
    #[cfg(feature = "jxl-encode")]
    JxlZune(ZuneJxlEncodeConfig),
    /// JPEG XL via `libjxl`, the reference encoder (requires `jxl-encode-libjxl`).
    #[cfg(feature = "jxl-encode-libjxl")]
    JxlLibjxl(LibjxlEncodeConfig),
    /// DNG via the in-repo encoder (requires `dng-encode`).
    #[cfg(feature = "dng-encode")]
    Dng(DngExportConfig),
}

#[cfg(feature = "png-encode")]
impl Default for EncodeOptions {
    fn default() -> Self {
        Self::png()
    }
}

impl EncodeOptions {
    /// PNG with default configuration.
    #[cfg(feature = "png-encode")]
    pub fn png() -> Self {
        Self::PngZune(ZunePngEncodeConfig::default())
    }

    /// JPEG with default configuration.
    #[cfg(feature = "jpeg-encode")]
    pub fn jpeg() -> Self {
        Self::JpegJpegEnc(JpegEncEncodeConfig::default())
    }

    /// Lossy WebP with default configuration.
    #[cfg(feature = "webp-encode")]
    pub fn webp_lossy() -> Self {
        Self::WebpLibwebp(LibwebpEncodeConfig::lossy())
    }

    /// Lossless WebP with default configuration.
    #[cfg(feature = "webp-encode")]
    pub fn webp_lossless() -> Self {
        Self::WebpLibwebp(LibwebpEncodeConfig::lossless())
    }

    /// AVIF with default configuration.
    #[cfg(feature = "avif-encode")]
    pub fn avif() -> Self {
        Self::AvifRavif(RavifEncodeConfig::default())
    }

    /// JPEG XL with default configuration.
    #[cfg(feature = "jxl-encode")]
    pub fn jxl() -> Self {
        Self::JxlZune(ZuneJxlEncodeConfig::default())
    }

    /// JPEG XL via the libjxl reference encoder, with default configuration.
    #[cfg(feature = "jxl-encode-libjxl")]
    pub fn jxl_libjxl() -> Self {
        Self::JxlLibjxl(LibjxlEncodeConfig::default())
    }

    /// DNG with default configuration.
    #[cfg(feature = "dng-encode")]
    pub fn dng() -> Self {
        Self::Dng(DngExportConfig::default())
    }

    /// The output format this encoder produces.
    pub fn format(&self) -> OutputFormat {
        match self {
            #[cfg(feature = "png-encode")]
            EncodeOptions::PngZune(_) => OutputFormat::Png,
            #[cfg(feature = "jpeg-encode")]
            EncodeOptions::JpegJpegEnc(_) => OutputFormat::Jpeg,
            #[cfg(feature = "webp-encode")]
            EncodeOptions::WebpLibwebp(_) => OutputFormat::WebP,
            #[cfg(feature = "avif-encode")]
            EncodeOptions::AvifRavif(_) => OutputFormat::Avif,
            #[cfg(feature = "jxl-encode")]
            EncodeOptions::JxlZune(_) => OutputFormat::Jxl,
            #[cfg(feature = "jxl-encode-libjxl")]
            EncodeOptions::JxlLibjxl(_) => OutputFormat::Jxl,
            #[cfg(feature = "dng-encode")]
            EncodeOptions::Dng(_) => OutputFormat::Dng,
            // Unreachable: with no encode feature enabled `EncodeOptions` has
            // no variants and no value of it can be constructed.
            #[allow(unreachable_patterns)]
            _ => unreachable!(),
        }
    }

    /// The stable identifier of the selected encoder implementation.
    pub fn codec_id(&self) -> CodecId {
        match self {
            #[cfg(feature = "png-encode")]
            EncodeOptions::PngZune(_) => CodecId::new("png/zune"),
            #[cfg(feature = "jpeg-encode")]
            EncodeOptions::JpegJpegEnc(_) => CodecId::new("jpeg/jpeg-encoder"),
            #[cfg(feature = "webp-encode")]
            EncodeOptions::WebpLibwebp(_) => CodecId::new("webp/libwebp"),
            #[cfg(feature = "avif-encode")]
            EncodeOptions::AvifRavif(_) => CodecId::new("avif/ravif"),
            #[cfg(feature = "jxl-encode")]
            EncodeOptions::JxlZune(_) => CodecId::new("jxl/zune"),
            #[cfg(feature = "jxl-encode-libjxl")]
            EncodeOptions::JxlLibjxl(_) => CodecId::new("jxl/libjxl"),
            #[cfg(feature = "dng-encode")]
            EncodeOptions::Dng(_) => CodecId::new("dng/rawshift"),
            #[allow(unreachable_patterns)]
            _ => unreachable!(),
        }
    }

    /// The encoder-agnostic options (metadata embedding, bit depth) in effect.
    ///
    /// For DNG — which has its own configuration shape — a `CommonEncodeOptions`
    /// is synthesised from [`DngExportConfig`].
    pub fn common(&self) -> CommonEncodeOptions {
        match self {
            #[cfg(feature = "png-encode")]
            EncodeOptions::PngZune(c) => c.common,
            #[cfg(feature = "jpeg-encode")]
            EncodeOptions::JpegJpegEnc(c) => c.common,
            #[cfg(feature = "webp-encode")]
            EncodeOptions::WebpLibwebp(c) => c.common,
            #[cfg(feature = "avif-encode")]
            EncodeOptions::AvifRavif(c) => c.common,
            #[cfg(feature = "jxl-encode")]
            EncodeOptions::JxlZune(c) => c.common,
            #[cfg(feature = "jxl-encode-libjxl")]
            EncodeOptions::JxlLibjxl(c) => c.common,
            #[cfg(feature = "dng-encode")]
            EncodeOptions::Dng(c) => CommonEncodeOptions {
                metadata: MetadataEmbedOptions {
                    embed_exif: c.embed_exif,
                    embed_icc: false,
                    embed_xmp: false,
                },
                bit_depth: BitDepth::Sixteen,
            },
            #[allow(unreachable_patterns)]
            _ => unreachable!(),
        }
    }

    /// The default encoder backend for `format`, with default configuration.
    ///
    /// Returns `None` when no encoder for `format` is compiled in, or when the
    /// format has no encoder. DNG is absent — it is not a [`StandardFormat`];
    /// use [`EncodeOptions::dng`].
    pub fn default_for(format: StandardFormat) -> Option<EncodeOptions> {
        match format {
            #[cfg(feature = "png-encode")]
            StandardFormat::Png => Some(EncodeOptions::png()),
            #[cfg(feature = "jpeg-encode")]
            StandardFormat::Jpeg => Some(EncodeOptions::jpeg()),
            #[cfg(feature = "webp-encode")]
            StandardFormat::WebP => Some(EncodeOptions::webp_lossy()),
            #[cfg(feature = "avif-encode")]
            StandardFormat::Avif => Some(EncodeOptions::avif()),
            #[cfg(feature = "jxl-encode")]
            StandardFormat::Jxl => Some(EncodeOptions::jxl()),
            _ => None,
        }
    }
}

#[cfg(test)]
mod tests {
    #[cfg(any_standard_encode)]
    use super::*;

    #[test]
    #[cfg(feature = "jpeg-encode")]
    fn default_for_jpeg_is_jpeg() {
        let opts = EncodeOptions::default_for(StandardFormat::Jpeg).unwrap();
        assert_eq!(opts.format(), OutputFormat::Jpeg);
        assert_eq!(opts.codec_id().id, "jpeg/jpeg-encoder");
    }

    #[test]
    #[cfg(feature = "png-encode")]
    fn default_for_unencodable_format_is_none() {
        assert!(EncodeOptions::default_for(StandardFormat::Gif).is_none());
    }

    #[test]
    #[cfg(feature = "avif-encode")]
    fn avif_default_quality_and_common() {
        let opts = EncodeOptions::avif();
        assert_eq!(opts.format(), OutputFormat::Avif);
        assert_eq!(opts.common().bit_depth, BitDepth::Sixteen);
    }

    #[test]
    fn output_format_names_and_extensions() {
        assert_eq!(OutputFormat::Jxl.name(), "JPEG XL");
        assert_eq!(OutputFormat::Avif.extension(), "avif");
    }
}