Skip to main content

oximedia_transcode/
codec_config.rs

1//! Codec-specific configuration and optimization.
2
3use crate::{QualityMode, QualityPreset, RateControlMode, TuneMode};
4use serde::{Deserialize, Serialize};
5
6/// Codec-specific configuration.
7#[derive(Debug, Clone)]
8pub struct CodecConfig {
9    /// Codec name.
10    pub codec: String,
11    /// Preset (speed/quality tradeoff).
12    pub preset: QualityPreset,
13    /// Tune for specific content.
14    pub tune: Option<TuneMode>,
15    /// Profile.
16    pub profile: Option<String>,
17    /// Level.
18    pub level: Option<String>,
19    /// Rate control mode.
20    pub rate_control: RateControlMode,
21    /// Additional options.
22    pub options: Vec<(String, String)>,
23}
24
25impl CodecConfig {
26    /// Creates a new codec configuration.
27    #[must_use]
28    pub fn new(codec: impl Into<String>) -> Self {
29        Self {
30            codec: codec.into(),
31            preset: QualityPreset::Medium,
32            tune: None,
33            profile: None,
34            level: None,
35            rate_control: RateControlMode::Crf(23),
36            options: Vec::new(),
37        }
38    }
39
40    /// Sets the preset.
41    #[must_use]
42    pub fn preset(mut self, preset: QualityPreset) -> Self {
43        self.preset = preset;
44        self
45    }
46
47    /// Sets the tune mode.
48    #[must_use]
49    pub fn tune(mut self, tune: TuneMode) -> Self {
50        self.tune = Some(tune);
51        self
52    }
53
54    /// Sets the profile.
55    #[must_use]
56    pub fn profile(mut self, profile: impl Into<String>) -> Self {
57        self.profile = Some(profile.into());
58        self
59    }
60
61    /// Sets the level.
62    #[must_use]
63    pub fn level(mut self, level: impl Into<String>) -> Self {
64        self.level = Some(level.into());
65        self
66    }
67
68    /// Sets the rate control mode.
69    #[must_use]
70    pub fn rate_control(mut self, mode: RateControlMode) -> Self {
71        self.rate_control = mode;
72        self
73    }
74
75    /// Adds a custom option.
76    #[must_use]
77    pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
78        self.options.push((key.into(), value.into()));
79        self
80    }
81}
82
83/// H.264/AVC specific configuration.
84#[derive(Debug, Clone)]
85pub struct H264Config {
86    base: CodecConfig,
87}
88
89impl H264Config {
90    /// Creates a new H.264 configuration.
91    #[must_use]
92    pub fn new() -> Self {
93        Self {
94            base: CodecConfig::new("h264"),
95        }
96    }
97
98    /// Sets the profile (baseline, main, high, high10, high422, high444).
99    #[must_use]
100    pub fn profile(mut self, profile: H264Profile) -> Self {
101        self.base.profile = Some(profile.as_str().to_string());
102        self
103    }
104
105    /// Sets the level (e.g., "3.0", "4.0", "5.1").
106    #[must_use]
107    pub fn level(mut self, level: impl Into<String>) -> Self {
108        self.base.level = Some(level.into());
109        self
110    }
111
112    /// Enables cabac entropy coding.
113    #[must_use]
114    pub fn cabac(mut self, enable: bool) -> Self {
115        self.base.options.push((
116            "cabac".to_string(),
117            if enable { "1" } else { "0" }.to_string(),
118        ));
119        self
120    }
121
122    /// Sets the number of reference frames.
123    #[must_use]
124    pub fn refs(mut self, refs: u8) -> Self {
125        self.base
126            .options
127            .push(("refs".to_string(), refs.to_string()));
128        self
129    }
130
131    /// Sets the number of B-frames.
132    #[must_use]
133    pub fn bframes(mut self, bframes: u8) -> Self {
134        self.base
135            .options
136            .push(("bframes".to_string(), bframes.to_string()));
137        self
138    }
139
140    /// Enables 8x8 DCT.
141    #[must_use]
142    pub fn dct8x8(mut self, enable: bool) -> Self {
143        self.base.options.push((
144            "8x8dct".to_string(),
145            if enable { "1" } else { "0" }.to_string(),
146        ));
147        self
148    }
149
150    /// Sets the deblocking filter parameters.
151    #[must_use]
152    pub fn deblock(mut self, alpha: i8, beta: i8) -> Self {
153        self.base
154            .options
155            .push(("deblock".to_string(), format!("{alpha}:{beta}")));
156        self
157    }
158
159    /// Converts to base codec config.
160    #[must_use]
161    pub fn build(self) -> CodecConfig {
162        self.base
163    }
164}
165
166impl Default for H264Config {
167    fn default() -> Self {
168        Self::new()
169    }
170}
171
172/// H.264 profiles.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum H264Profile {
175    /// Baseline profile.
176    Baseline,
177    /// Main profile.
178    Main,
179    /// High profile.
180    High,
181    /// High 10 profile (10-bit).
182    High10,
183    /// High 4:2:2 profile.
184    High422,
185    /// High 4:4:4 profile.
186    High444,
187}
188
189impl H264Profile {
190    #[must_use]
191    fn as_str(self) -> &'static str {
192        match self {
193            Self::Baseline => "baseline",
194            Self::Main => "main",
195            Self::High => "high",
196            Self::High10 => "high10",
197            Self::High422 => "high422",
198            Self::High444 => "high444",
199        }
200    }
201}
202
203/// VP9 specific configuration.
204#[derive(Debug, Clone)]
205pub struct Vp9Config {
206    base: CodecConfig,
207}
208
209impl Vp9Config {
210    /// Creates a new VP9 configuration.
211    #[must_use]
212    pub fn new() -> Self {
213        Self {
214            base: CodecConfig::new("vp9"),
215        }
216    }
217
218    /// Creates a VP9 configuration in CRF (constant quality) mode.
219    ///
220    /// CRF range for VP9 is 0–63; lower values produce higher quality.
221    /// Typical values: 31 (good quality), 33 (balanced), 41 (lower quality).
222    #[must_use]
223    pub fn crf(crf_value: u8) -> Self {
224        let mut cfg = Self::new();
225        cfg.base.rate_control = RateControlMode::Crf(crf_value);
226        cfg
227    }
228
229    /// Sets the CPU used (0-8, lower = slower/better).
230    #[must_use]
231    pub fn cpu_used(mut self, cpu_used: u8) -> Self {
232        self.base
233            .options
234            .push(("cpu-used".to_string(), cpu_used.to_string()));
235        self
236    }
237
238    /// Sets the tile columns for parallel encoding (0–6, value is log2 of column count).
239    #[must_use]
240    pub fn tile_columns(mut self, columns: u8) -> Self {
241        self.base
242            .options
243            .push(("tile-columns".to_string(), columns.to_string()));
244        self
245    }
246
247    /// Sets the tile columns for parallel encoding via builder pattern (0–6).
248    #[must_use]
249    pub fn with_tile_columns(mut self, cols: u8) -> Self {
250        self.base
251            .options
252            .push(("tile-columns".to_string(), cols.to_string()));
253        self
254    }
255
256    /// Sets the tile rows (for parallel encoding).
257    #[must_use]
258    pub fn tile_rows(mut self, rows: u8) -> Self {
259        self.base
260            .options
261            .push(("tile-rows".to_string(), rows.to_string()));
262        self
263    }
264
265    /// Sets the frame parallel encoding.
266    #[must_use]
267    pub fn frame_parallel(mut self, enable: bool) -> Self {
268        self.base.options.push((
269            "frame-parallel".to_string(),
270            if enable { "1" } else { "0" }.to_string(),
271        ));
272        self
273    }
274
275    /// Enables or disables frame-parallel decoding hint via builder pattern.
276    #[must_use]
277    pub fn with_frame_parallel(mut self, enabled: bool) -> Self {
278        self.base.options.push((
279            "frame-parallel".to_string(),
280            if enabled { "1" } else { "0" }.to_string(),
281        ));
282        self
283    }
284
285    /// Sets the auto alt reference frames.
286    #[must_use]
287    pub fn auto_alt_ref(mut self, frames: u8) -> Self {
288        self.base
289            .options
290            .push(("auto-alt-ref".to_string(), frames.to_string()));
291        self
292    }
293
294    /// Sets the lag in frames (0–25).
295    ///
296    /// Larger values allow better quality at the cost of encoding latency.
297    #[must_use]
298    pub fn lag_in_frames(mut self, lag: u32) -> Self {
299        self.base
300            .options
301            .push(("lag-in-frames".to_string(), lag.to_string()));
302        self
303    }
304
305    /// Sets lag in frames via builder pattern (0–25).
306    #[must_use]
307    pub fn with_lag_in_frames(mut self, frames: u8) -> Self {
308        self.base
309            .options
310            .push(("lag-in-frames".to_string(), frames.to_string()));
311        self
312    }
313
314    /// Enables row-based multi-threading.
315    #[must_use]
316    pub fn row_mt(mut self, enable: bool) -> Self {
317        self.base.options.push((
318            "row-mt".to_string(),
319            if enable { "1" } else { "0" }.to_string(),
320        ));
321        self
322    }
323
324    /// Enables or disables row-based multi-threading via builder pattern.
325    #[must_use]
326    pub fn with_row_mt(mut self, enabled: bool) -> Self {
327        self.base.options.push((
328            "row-mt".to_string(),
329            if enabled { "1" } else { "0" }.to_string(),
330        ));
331        self
332    }
333
334    /// Screen content encoding preset.
335    ///
336    /// Optimised for screen recordings: fast cpu-used, tile columns for parallelism,
337    /// and row-mt for throughput. Uses CRF 33 as a balanced starting point.
338    #[must_use]
339    pub fn screen_content() -> Self {
340        let mut cfg = Self::crf(33);
341        cfg.base
342            .options
343            .push(("cpu-used".to_string(), "5".to_string()));
344        cfg.base
345            .options
346            .push(("tile-columns".to_string(), "2".to_string()));
347        cfg.base
348            .options
349            .push(("row-mt".to_string(), "1".to_string()));
350        cfg.base
351            .options
352            .push(("lag-in-frames".to_string(), "0".to_string()));
353        cfg
354    }
355
356    /// Converts to base codec config.
357    #[must_use]
358    pub fn build(self) -> CodecConfig {
359        self.base
360    }
361}
362
363impl Default for Vp9Config {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369/// AV1 specific configuration.
370#[derive(Debug, Clone)]
371pub struct Av1Config {
372    base: CodecConfig,
373}
374
375impl Av1Config {
376    /// Creates a new AV1 configuration.
377    #[must_use]
378    pub fn new() -> Self {
379        Self {
380            base: CodecConfig::new("av1"),
381        }
382    }
383
384    /// Sets the CPU used (0-8, lower = slower/better).
385    #[must_use]
386    pub fn cpu_used(mut self, cpu_used: u8) -> Self {
387        self.base
388            .options
389            .push(("cpu-used".to_string(), cpu_used.to_string()));
390        self
391    }
392
393    /// Sets the tile columns.
394    #[must_use]
395    pub fn tiles(mut self, columns: u8, rows: u8) -> Self {
396        self.base
397            .options
398            .push(("tiles".to_string(), format!("{columns}x{rows}")));
399        self
400    }
401
402    /// Enables row-based multi-threading.
403    #[must_use]
404    pub fn row_mt(mut self, enable: bool) -> Self {
405        self.base.options.push((
406            "row-mt".to_string(),
407            if enable { "1" } else { "0" }.to_string(),
408        ));
409        self
410    }
411
412    /// Sets the usage mode (good, realtime).
413    #[must_use]
414    pub fn usage(mut self, usage: Av1Usage) -> Self {
415        self.base
416            .options
417            .push(("usage".to_string(), usage.as_str().to_string()));
418        self
419    }
420
421    /// Enables film grain synthesis.
422    #[must_use]
423    pub fn enable_film_grain(mut self, enable: bool) -> Self {
424        self.base.options.push((
425            "enable-film-grain".to_string(),
426            if enable { "1" } else { "0" }.to_string(),
427        ));
428        self
429    }
430
431    /// Converts to base codec config.
432    #[must_use]
433    pub fn build(self) -> CodecConfig {
434        self.base
435    }
436}
437
438impl Default for Av1Config {
439    fn default() -> Self {
440        Self::new()
441    }
442}
443
444/// AV1 usage modes.
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub enum Av1Usage {
447    /// Good quality mode.
448    Good,
449    /// Real-time mode.
450    Realtime,
451}
452
453impl Av1Usage {
454    #[must_use]
455    fn as_str(self) -> &'static str {
456        match self {
457            Self::Good => "good",
458            Self::Realtime => "realtime",
459        }
460    }
461}
462
463/// Opus audio codec configuration.
464#[derive(Debug, Clone)]
465pub struct OpusConfig {
466    base: CodecConfig,
467}
468
469impl OpusConfig {
470    /// Creates a new Opus configuration.
471    #[must_use]
472    pub fn new() -> Self {
473        Self {
474            base: CodecConfig::new("opus"),
475        }
476    }
477
478    /// Voice/VOIP optimised preset.
479    ///
480    /// Uses VOIP application mode, maximum complexity, VBR, and forward error
481    /// correction — suitable for speech transmission over lossy networks.
482    #[must_use]
483    pub fn voice() -> Self {
484        Self::new()
485            .application(OpusApplication::Voip)
486            .complexity(10)
487            .vbr(true)
488            .with_fec(true)
489    }
490
491    /// Music streaming preset.
492    ///
493    /// Uses Audio application mode, maximum complexity, VBR, and a 20 ms frame
494    /// duration — optimal balance for music with transparent quality.
495    #[must_use]
496    pub fn music() -> Self {
497        Self::new()
498            .application(OpusApplication::Audio)
499            .complexity(10)
500            .vbr(true)
501            .frame_duration(20.0)
502    }
503
504    /// Full-band (20 Hz–20 kHz) audio preset.
505    ///
506    /// Forces full-band mode with maximum complexity and VBR.
507    #[must_use]
508    pub fn fullband() -> Self {
509        let mut cfg = Self::new()
510            .application(OpusApplication::Audio)
511            .complexity(10)
512            .vbr(true);
513        cfg.base
514            .options
515            .push(("cutoff".to_string(), "20000".to_string()));
516        cfg
517    }
518
519    /// Sets the application type.
520    #[must_use]
521    pub fn application(mut self, app: OpusApplication) -> Self {
522        self.base
523            .options
524            .push(("application".to_string(), app.as_str().to_string()));
525        self
526    }
527
528    /// Sets the complexity (0-10).
529    #[must_use]
530    pub fn complexity(mut self, complexity: u8) -> Self {
531        self.base
532            .options
533            .push(("complexity".to_string(), complexity.to_string()));
534        self
535    }
536
537    /// Sets the frame duration in milliseconds.
538    #[must_use]
539    pub fn frame_duration(mut self, duration_ms: f32) -> Self {
540        self.base
541            .options
542            .push(("frame_duration".to_string(), duration_ms.to_string()));
543        self
544    }
545
546    /// Enables variable bitrate.
547    #[must_use]
548    pub fn vbr(mut self, enable: bool) -> Self {
549        self.base.options.push((
550            "vbr".to_string(),
551            if enable { "on" } else { "off" }.to_string(),
552        ));
553        self
554    }
555
556    /// Enables or disables variable bitrate via builder pattern.
557    #[must_use]
558    pub fn with_vbr(mut self, enabled: bool) -> Self {
559        self.base.options.push((
560            "vbr".to_string(),
561            if enabled { "on" } else { "off" }.to_string(),
562        ));
563        self
564    }
565
566    /// Enables or disables constrained VBR mode.
567    ///
568    /// Constrained VBR limits bitrate peaks while still allowing variation,
569    /// giving better quality than strict CBR with bounded bitrate.
570    #[must_use]
571    pub fn with_constrained_vbr(mut self, enabled: bool) -> Self {
572        self.base.options.push((
573            "cvbr".to_string(),
574            if enabled { "1" } else { "0" }.to_string(),
575        ));
576        self
577    }
578
579    /// Enables or disables Discontinuous Transmission (DTX).
580    ///
581    /// DTX reduces bitrate during silence by sending comfort noise packets,
582    /// useful for VOIP applications where silence is frequent.
583    #[must_use]
584    pub fn with_dtx(mut self, enabled: bool) -> Self {
585        self.base.options.push((
586            "dtx".to_string(),
587            if enabled { "1" } else { "0" }.to_string(),
588        ));
589        self
590    }
591
592    /// Enables or disables in-band Forward Error Correction (FEC).
593    ///
594    /// FEC adds redundant audio data that allows partial recovery from packet
595    /// loss in VoIP scenarios. Increases bitrate slightly.
596    #[must_use]
597    pub fn with_fec(mut self, enabled: bool) -> Self {
598        self.base.options.push((
599            "inband_fec".to_string(),
600            if enabled { "1" } else { "0" }.to_string(),
601        ));
602        self
603    }
604
605    /// Sets the expected packet loss percentage (0–100).
606    ///
607    /// This hint guides the encoder in tuning FEC strength and bitrate
608    /// distribution. Requires FEC to be enabled for full effect.
609    #[must_use]
610    pub fn with_packet_loss_perc(mut self, pct: u8) -> Self {
611        self.base
612            .options
613            .push(("packet_loss_perc".to_string(), pct.to_string()));
614        self
615    }
616
617    /// Converts to base codec config.
618    #[must_use]
619    pub fn build(self) -> CodecConfig {
620        self.base
621    }
622}
623
624impl Default for OpusConfig {
625    fn default() -> Self {
626        Self::new()
627    }
628}
629
630/// Opus application types.
631#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
632pub enum OpusApplication {
633    /// Voice over IP.
634    Voip,
635    /// Audio streaming.
636    Audio,
637    /// Low delay.
638    LowDelay,
639}
640
641impl OpusApplication {
642    #[must_use]
643    fn as_str(self) -> &'static str {
644        match self {
645            Self::Voip => "voip",
646            Self::Audio => "audio",
647            Self::LowDelay => "lowdelay",
648        }
649    }
650}
651
652/// FFV1 lossless codec levels.
653#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
654pub enum Ffv1Level {
655    /// FFV1 Level 1 (older, limited features).
656    Level1,
657    /// FFV1 Level 3 (modern, slice-based, multithreaded).
658    Level3,
659}
660
661impl Ffv1Level {
662    /// Returns the integer level value.
663    #[must_use]
664    pub fn as_u8(self) -> u8 {
665        match self {
666            Self::Level1 => 1,
667            Self::Level3 => 3,
668        }
669    }
670}
671
672/// FFV1 entropy coder selection.
673#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
674pub enum Ffv1Coder {
675    /// Golomb-Rice entropy coding (faster, less compression).
676    GolombRice,
677    /// Range (ANS) entropy coding (better compression, slightly slower).
678    Range,
679}
680
681impl Ffv1Coder {
682    /// Returns the integer coder value used by the encoder.
683    #[must_use]
684    pub fn as_u8(self) -> u8 {
685        match self {
686            Self::GolombRice => 0,
687            Self::Range => 1,
688        }
689    }
690}
691
692/// FFV1 lossless video codec configuration.
693#[derive(Debug, Clone)]
694pub struct Ffv1Config {
695    /// FFV1 level (1 or 3).
696    pub level: Ffv1Level,
697    /// Entropy coder selection.
698    pub coder: Ffv1Coder,
699    /// Number of slices for multithreaded encoding (1, 4, 9, 16, 24).
700    pub slice_count: u8,
701    /// Context model complexity (0=simple, 1=complex/better compression).
702    pub context_model: u8,
703    /// Enable per-slice CRC checksums for error detection.
704    pub checksum: bool,
705}
706
707impl Ffv1Config {
708    /// Creates a new FFV1 configuration with sensible defaults.
709    #[must_use]
710    pub fn new() -> Self {
711        Self {
712            level: Ffv1Level::Level3,
713            coder: Ffv1Coder::Range,
714            slice_count: 4,
715            context_model: 0,
716            checksum: true,
717        }
718    }
719
720    /// Best-compression archival preset.
721    ///
722    /// Uses Level 3, Range coder, 16 slices, complex context model, and checksums.
723    /// Ideal for long-term preservation where file size and integrity matter most.
724    #[must_use]
725    pub fn lossless_archive() -> Self {
726        Self {
727            level: Ffv1Level::Level3,
728            coder: Ffv1Coder::Range,
729            slice_count: 16,
730            context_model: 1,
731            checksum: true,
732        }
733    }
734
735    /// Fastest lossless encoding preset.
736    ///
737    /// Uses Level 1, Golomb-Rice coder, 4 slices, simple context model, no checksums.
738    /// Ideal for fast ingest or intermediate encoding.
739    #[must_use]
740    pub fn lossless_fast() -> Self {
741        Self {
742            level: Ffv1Level::Level1,
743            coder: Ffv1Coder::GolombRice,
744            slice_count: 4,
745            context_model: 0,
746            checksum: false,
747        }
748    }
749
750    /// Sets the number of encoding slices.
751    ///
752    /// Valid values: 1, 4, 9, 16, 24. More slices enable better multithreading.
753    #[must_use]
754    pub fn with_slices(mut self, count: u8) -> Self {
755        self.slice_count = count;
756        self
757    }
758
759    /// Builds a `CodecConfig` from this FFV1 configuration.
760    #[must_use]
761    pub fn build(self) -> CodecConfig {
762        let mut cfg = CodecConfig::new("ffv1");
763        cfg.options
764            .push(("level".to_string(), self.level.as_u8().to_string()));
765        cfg.options
766            .push(("coder".to_string(), self.coder.as_u8().to_string()));
767        cfg.options
768            .push(("slices".to_string(), self.slice_count.to_string()));
769        cfg.options
770            .push(("context".to_string(), self.context_model.to_string()));
771        cfg.options.push((
772            "slicecrc".to_string(),
773            if self.checksum { "1" } else { "0" }.to_string(),
774        ));
775        cfg.rate_control = RateControlMode::Crf(0); // lossless
776        cfg
777    }
778}
779
780impl Default for Ffv1Config {
781    fn default() -> Self {
782        Self::new()
783    }
784}
785
786/// FLAC lossless audio codec configuration.
787#[derive(Debug, Clone)]
788pub struct FlacConfig {
789    /// Compression level (0=fastest, 8=best compression).
790    pub compression_level: u8,
791    /// Block size in samples (256–65535, default 4096).
792    pub block_size: u32,
793    /// Verify decoded output matches input (slower, but ensures correctness).
794    pub verify: bool,
795}
796
797impl FlacConfig {
798    /// Creates a new FLAC configuration with balanced defaults.
799    #[must_use]
800    pub fn new() -> Self {
801        Self {
802            compression_level: 5,
803            block_size: 4096,
804            verify: false,
805        }
806    }
807
808    /// Archival preset — maximum compression with verification.
809    #[must_use]
810    pub fn archival() -> Self {
811        Self {
812            compression_level: 8,
813            block_size: 4096,
814            verify: true,
815        }
816    }
817
818    /// Streaming preset — balanced speed and compression.
819    #[must_use]
820    pub fn streaming() -> Self {
821        Self {
822            compression_level: 4,
823            block_size: 4096,
824            verify: false,
825        }
826    }
827
828    /// Fast preset — fastest encoding, least compression.
829    #[must_use]
830    pub fn fast() -> Self {
831        Self {
832            compression_level: 0,
833            block_size: 4096,
834            verify: false,
835        }
836    }
837
838    /// Builds a `CodecConfig` from this FLAC configuration.
839    #[must_use]
840    pub fn build(self) -> CodecConfig {
841        let mut cfg = CodecConfig::new("flac");
842        cfg.options.push((
843            "compression_level".to_string(),
844            self.compression_level.to_string(),
845        ));
846        cfg.options
847            .push(("blocksize".to_string(), self.block_size.to_string()));
848        cfg.options.push((
849            "lpc_coeff_precision".to_string(),
850            "15".to_string(), // maximum precision for archival quality
851        ));
852        if self.verify {
853            cfg.options.push(("verify".to_string(), "1".to_string()));
854        }
855        cfg.rate_control = RateControlMode::Crf(0); // lossless
856        cfg
857    }
858}
859
860impl Default for FlacConfig {
861    fn default() -> Self {
862        Self::new()
863    }
864}
865
866/// JPEG-XL still image encoding configuration.
867///
868/// Supports both lossless and lossy encoding with advanced options like
869/// progressive decoding, effort level, and photon noise ISO simulation.
870#[derive(Debug, Clone)]
871pub struct JxlConfig {
872    /// Quality level for lossy encoding (1.0 = visually lossless, 100.0 = worst).
873    ///
874    /// Set to `None` for lossless mode.
875    pub quality: Option<f32>,
876    /// Encoding effort (1 = fastest, 10 = best compression).
877    pub effort: JxlEffort,
878    /// Enable progressive decoding (DC first, then AC passes).
879    pub progressive: bool,
880    /// Photon noise ISO equivalent for denoising during encode.
881    ///
882    /// Setting this enables noise modelling: the encoder treats pixel
883    /// noise at the specified ISO level as irrelevant, yielding smaller
884    /// files for high-ISO photographs.
885    pub photon_noise_iso: Option<u32>,
886    /// Number of extra channels (e.g. alpha, depth).
887    pub extra_channels: u8,
888    /// Enable modular mode (better for lossless, graphics, low-complexity images).
889    pub modular: bool,
890    /// Color space: "rgb", "xyb" (perceptual, default for lossy), "gray".
891    pub color_space: JxlColorSpace,
892    /// Bit depth per channel (8, 10, 12, 16, 32).
893    pub bit_depth: u8,
894}
895
896/// JPEG-XL encoding effort (speed/compression tradeoff).
897#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
898pub enum JxlEffort {
899    /// Lightning fast (effort 1).
900    Lightning,
901    /// Thunder (effort 2).
902    Thunder,
903    /// Falcon (effort 3).
904    Falcon,
905    /// Cheetah (effort 4).
906    Cheetah,
907    /// Hare (effort 5).
908    Hare,
909    /// Wombat (effort 6).
910    Wombat,
911    /// Squirrel (effort 7, default).
912    Squirrel,
913    /// Kitten (effort 8).
914    Kitten,
915    /// Tortoise (effort 9).
916    Tortoise,
917    /// Glacier (effort 10, maximum compression).
918    Glacier,
919}
920
921impl JxlEffort {
922    /// Returns the numeric effort level (1-10).
923    #[must_use]
924    pub fn as_u8(self) -> u8 {
925        match self {
926            Self::Lightning => 1,
927            Self::Thunder => 2,
928            Self::Falcon => 3,
929            Self::Cheetah => 4,
930            Self::Hare => 5,
931            Self::Wombat => 6,
932            Self::Squirrel => 7,
933            Self::Kitten => 8,
934            Self::Tortoise => 9,
935            Self::Glacier => 10,
936        }
937    }
938
939    /// Returns the effort name as a string.
940    #[must_use]
941    pub fn as_str(self) -> &'static str {
942        match self {
943            Self::Lightning => "lightning",
944            Self::Thunder => "thunder",
945            Self::Falcon => "falcon",
946            Self::Cheetah => "cheetah",
947            Self::Hare => "hare",
948            Self::Wombat => "wombat",
949            Self::Squirrel => "squirrel",
950            Self::Kitten => "kitten",
951            Self::Tortoise => "tortoise",
952            Self::Glacier => "glacier",
953        }
954    }
955}
956
957/// JPEG-XL color space modes.
958#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
959pub enum JxlColorSpace {
960    /// RGB color space.
961    Rgb,
962    /// XYB perceptual color space (default for lossy).
963    Xyb,
964    /// Grayscale.
965    Gray,
966}
967
968impl JxlColorSpace {
969    /// Returns the color space as a string identifier.
970    #[must_use]
971    pub fn as_str(self) -> &'static str {
972        match self {
973            Self::Rgb => "rgb",
974            Self::Xyb => "xyb",
975            Self::Gray => "gray",
976        }
977    }
978}
979
980impl JxlConfig {
981    /// Creates a new JPEG-XL configuration with balanced defaults (lossy, quality 75).
982    #[must_use]
983    pub fn new() -> Self {
984        Self {
985            quality: Some(75.0),
986            effort: JxlEffort::Squirrel,
987            progressive: false,
988            photon_noise_iso: None,
989            extra_channels: 0,
990            modular: false,
991            color_space: JxlColorSpace::Xyb,
992            bit_depth: 8,
993        }
994    }
995
996    /// Lossless encoding preset.
997    ///
998    /// Uses modular mode with RGB color space for mathematically lossless
999    /// compression. Best for graphics, screenshots, and archival.
1000    #[must_use]
1001    pub fn lossless() -> Self {
1002        Self {
1003            quality: None,
1004            effort: JxlEffort::Tortoise,
1005            progressive: false,
1006            photon_noise_iso: None,
1007            extra_channels: 0,
1008            modular: true,
1009            color_space: JxlColorSpace::Rgb,
1010            bit_depth: 8,
1011        }
1012    }
1013
1014    /// Web delivery preset.
1015    ///
1016    /// Lossy with progressive decoding for fast web rendering.
1017    /// Quality 80 gives excellent visual quality at small file sizes.
1018    #[must_use]
1019    pub fn web() -> Self {
1020        Self {
1021            quality: Some(80.0),
1022            effort: JxlEffort::Squirrel,
1023            progressive: true,
1024            photon_noise_iso: None,
1025            extra_channels: 0,
1026            modular: false,
1027            color_space: JxlColorSpace::Xyb,
1028            bit_depth: 8,
1029        }
1030    }
1031
1032    /// Photography preset.
1033    ///
1034    /// Visually lossless encoding with photon noise modelling at ISO 400.
1035    /// Ideal for camera RAW conversions and photo archives.
1036    #[must_use]
1037    pub fn photography() -> Self {
1038        Self {
1039            quality: Some(90.0),
1040            effort: JxlEffort::Kitten,
1041            progressive: true,
1042            photon_noise_iso: Some(400),
1043            extra_channels: 0,
1044            modular: false,
1045            color_space: JxlColorSpace::Xyb,
1046            bit_depth: 16,
1047        }
1048    }
1049
1050    /// Sets the quality level (1.0 = visually lossless, 100.0 = worst).
1051    #[must_use]
1052    pub fn with_quality(mut self, quality: f32) -> Self {
1053        self.quality = Some(quality);
1054        self
1055    }
1056
1057    /// Sets the encoding effort.
1058    #[must_use]
1059    pub fn with_effort(mut self, effort: JxlEffort) -> Self {
1060        self.effort = effort;
1061        self
1062    }
1063
1064    /// Enables or disables progressive decoding.
1065    #[must_use]
1066    pub fn with_progressive(mut self, progressive: bool) -> Self {
1067        self.progressive = progressive;
1068        self
1069    }
1070
1071    /// Sets the photon noise ISO for noise modelling.
1072    #[must_use]
1073    pub fn with_photon_noise(mut self, iso: u32) -> Self {
1074        self.photon_noise_iso = Some(iso);
1075        self
1076    }
1077
1078    /// Sets the bit depth per channel.
1079    #[must_use]
1080    pub fn with_bit_depth(mut self, depth: u8) -> Self {
1081        self.bit_depth = depth;
1082        self
1083    }
1084
1085    /// Enables modular mode (better for lossless/graphics).
1086    #[must_use]
1087    pub fn with_modular(mut self, modular: bool) -> Self {
1088        self.modular = modular;
1089        self
1090    }
1091
1092    /// Returns `true` if this is a lossless configuration.
1093    #[must_use]
1094    pub fn is_lossless(&self) -> bool {
1095        self.quality.is_none()
1096    }
1097
1098    /// Builds a `CodecConfig` from this JPEG-XL configuration.
1099    #[must_use]
1100    pub fn build(self) -> CodecConfig {
1101        let mut cfg = CodecConfig::new("jxl");
1102
1103        if let Some(q) = self.quality {
1104            cfg.options.push(("quality".to_string(), format!("{q:.1}")));
1105        } else {
1106            cfg.options.push(("lossless".to_string(), "1".to_string()));
1107        }
1108
1109        cfg.options
1110            .push(("effort".to_string(), self.effort.as_u8().to_string()));
1111
1112        if self.progressive {
1113            cfg.options
1114                .push(("progressive".to_string(), "1".to_string()));
1115        }
1116
1117        if let Some(iso) = self.photon_noise_iso {
1118            cfg.options
1119                .push(("photon_noise_iso".to_string(), iso.to_string()));
1120        }
1121
1122        if self.modular {
1123            cfg.options.push(("modular".to_string(), "1".to_string()));
1124        }
1125
1126        cfg.options.push((
1127            "color_space".to_string(),
1128            self.color_space.as_str().to_string(),
1129        ));
1130
1131        cfg.options
1132            .push(("bit_depth".to_string(), self.bit_depth.to_string()));
1133
1134        // Lossless uses CRF 0; lossy uses quality-based CRF approximation
1135        cfg.rate_control = if self.is_lossless() {
1136            RateControlMode::Crf(0)
1137        } else {
1138            // Map quality 1-100 to CRF-like value
1139            let q = self.quality.unwrap_or(75.0);
1140            let crf = ((100.0 - q) * 0.63) as u8;
1141            RateControlMode::Crf(crf)
1142        };
1143
1144        cfg
1145    }
1146}
1147
1148impl Default for JxlConfig {
1149    fn default() -> Self {
1150        Self::new()
1151    }
1152}
1153
1154/// Creates codec configuration from quality mode.
1155#[must_use]
1156pub fn codec_config_from_quality(codec: &str, quality: QualityMode) -> CodecConfig {
1157    let preset = quality.to_preset();
1158    let crf = quality.to_crf();
1159
1160    match codec {
1161        "h264" => H264Config::new()
1162            .profile(H264Profile::High)
1163            .refs(3)
1164            .bframes(3)
1165            .build()
1166            .preset(preset)
1167            .rate_control(RateControlMode::Crf(crf)),
1168        "vp9" => Vp9Config::new()
1169            .cpu_used(preset.cpu_used())
1170            .row_mt(true)
1171            .build()
1172            .preset(preset)
1173            .rate_control(RateControlMode::Crf(crf)),
1174        "av1" => Av1Config::new()
1175            .cpu_used(preset.cpu_used())
1176            .row_mt(true)
1177            .usage(Av1Usage::Good)
1178            .build()
1179            .preset(preset)
1180            .rate_control(RateControlMode::Crf(crf)),
1181        "opus" => OpusConfig::new()
1182            .application(OpusApplication::Audio)
1183            .complexity(10)
1184            .vbr(true)
1185            .build()
1186            .preset(preset),
1187        _ => CodecConfig::new(codec)
1188            .preset(preset)
1189            .rate_control(RateControlMode::Crf(crf)),
1190    }
1191}
1192
1193#[cfg(test)]
1194mod tests {
1195    use super::*;
1196
1197    #[test]
1198    fn test_codec_config_new() {
1199        let config = CodecConfig::new("h264");
1200        assert_eq!(config.codec, "h264");
1201        assert_eq!(config.preset, QualityPreset::Medium);
1202    }
1203
1204    #[test]
1205    fn test_h264_config() {
1206        let config = H264Config::new()
1207            .profile(H264Profile::High)
1208            .level("4.0")
1209            .refs(3)
1210            .bframes(3)
1211            .cabac(true)
1212            .dct8x8(true)
1213            .build();
1214
1215        assert_eq!(config.codec, "h264");
1216        assert_eq!(config.profile, Some("high".to_string()));
1217        assert_eq!(config.level, Some("4.0".to_string()));
1218        assert!(config.options.len() > 0);
1219    }
1220
1221    #[test]
1222    fn test_vp9_config() {
1223        let config = Vp9Config::new()
1224            .cpu_used(4)
1225            .tile_columns(2)
1226            .tile_rows(1)
1227            .row_mt(true)
1228            .build();
1229
1230        assert_eq!(config.codec, "vp9");
1231        assert!(config
1232            .options
1233            .iter()
1234            .any(|(k, v)| k == "cpu-used" && v == "4"));
1235        assert!(config
1236            .options
1237            .iter()
1238            .any(|(k, v)| k == "row-mt" && v == "1"));
1239    }
1240
1241    #[test]
1242    fn test_av1_config() {
1243        let config = Av1Config::new()
1244            .cpu_used(6)
1245            .tiles(4, 2)
1246            .usage(Av1Usage::Good)
1247            .row_mt(true)
1248            .build();
1249
1250        assert_eq!(config.codec, "av1");
1251        assert!(config
1252            .options
1253            .iter()
1254            .any(|(k, v)| k == "cpu-used" && v == "6"));
1255        assert!(config
1256            .options
1257            .iter()
1258            .any(|(k, v)| k == "tiles" && v == "4x2"));
1259    }
1260
1261    #[test]
1262    fn test_opus_config() {
1263        let config = OpusConfig::new()
1264            .application(OpusApplication::Audio)
1265            .complexity(10)
1266            .vbr(true)
1267            .build();
1268
1269        assert_eq!(config.codec, "opus");
1270        assert!(config
1271            .options
1272            .iter()
1273            .any(|(k, v)| k == "application" && v == "audio"));
1274        assert!(config
1275            .options
1276            .iter()
1277            .any(|(k, v)| k == "complexity" && v == "10"));
1278    }
1279
1280    #[test]
1281    fn test_codec_config_from_quality() {
1282        let config = codec_config_from_quality("h264", QualityMode::High);
1283        assert_eq!(config.codec, "h264");
1284        assert_eq!(config.preset, QualityPreset::Slow);
1285        assert_eq!(config.rate_control, RateControlMode::Crf(20));
1286    }
1287
1288    #[test]
1289    fn test_h264_profiles() {
1290        assert_eq!(H264Profile::Baseline.as_str(), "baseline");
1291        assert_eq!(H264Profile::Main.as_str(), "main");
1292        assert_eq!(H264Profile::High.as_str(), "high");
1293    }
1294
1295    #[test]
1296    fn test_av1_usage() {
1297        assert_eq!(Av1Usage::Good.as_str(), "good");
1298        assert_eq!(Av1Usage::Realtime.as_str(), "realtime");
1299    }
1300
1301    #[test]
1302    fn test_opus_application() {
1303        assert_eq!(OpusApplication::Voip.as_str(), "voip");
1304        assert_eq!(OpusApplication::Audio.as_str(), "audio");
1305        assert_eq!(OpusApplication::LowDelay.as_str(), "lowdelay");
1306    }
1307
1308    // ── VP9 CRF and new builder methods ──────────────────────────────────
1309
1310    #[test]
1311    fn test_vp9_crf_mode() {
1312        let config = Vp9Config::crf(33).build();
1313        assert_eq!(config.codec, "vp9");
1314        assert!(
1315            matches!(config.rate_control, RateControlMode::Crf(33)),
1316            "VP9 CRF mode should use Crf(33)"
1317        );
1318    }
1319
1320    #[test]
1321    fn test_vp9_crf_range_boundary() {
1322        // VP9 CRF is valid 0-63; test extremes
1323        let lo = Vp9Config::crf(0).build();
1324        let hi = Vp9Config::crf(63).build();
1325        assert!(matches!(lo.rate_control, RateControlMode::Crf(0)));
1326        assert!(matches!(hi.rate_control, RateControlMode::Crf(63)));
1327    }
1328
1329    #[test]
1330    fn test_vp9_with_tile_columns() {
1331        let config = Vp9Config::new().with_tile_columns(3).build();
1332        assert!(
1333            config
1334                .options
1335                .iter()
1336                .any(|(k, v)| k == "tile-columns" && v == "3"),
1337            "with_tile_columns should set tile-columns option"
1338        );
1339    }
1340
1341    #[test]
1342    fn test_vp9_with_frame_parallel() {
1343        let config_on = Vp9Config::new().with_frame_parallel(true).build();
1344        let config_off = Vp9Config::new().with_frame_parallel(false).build();
1345        assert!(config_on
1346            .options
1347            .iter()
1348            .any(|(k, v)| k == "frame-parallel" && v == "1"));
1349        assert!(config_off
1350            .options
1351            .iter()
1352            .any(|(k, v)| k == "frame-parallel" && v == "0"));
1353    }
1354
1355    #[test]
1356    fn test_vp9_with_lag_in_frames() {
1357        let config = Vp9Config::new().with_lag_in_frames(25).build();
1358        assert!(
1359            config
1360                .options
1361                .iter()
1362                .any(|(k, v)| k == "lag-in-frames" && v == "25"),
1363            "with_lag_in_frames should set lag-in-frames option"
1364        );
1365    }
1366
1367    #[test]
1368    fn test_vp9_with_row_mt() {
1369        let config_on = Vp9Config::new().with_row_mt(true).build();
1370        let config_off = Vp9Config::new().with_row_mt(false).build();
1371        assert!(config_on
1372            .options
1373            .iter()
1374            .any(|(k, v)| k == "row-mt" && v == "1"));
1375        assert!(config_off
1376            .options
1377            .iter()
1378            .any(|(k, v)| k == "row-mt" && v == "0"));
1379    }
1380
1381    #[test]
1382    fn test_vp9_screen_content() {
1383        let config = Vp9Config::screen_content().build();
1384        assert_eq!(config.codec, "vp9");
1385        // Screen content uses CRF mode
1386        assert!(matches!(config.rate_control, RateControlMode::Crf(_)));
1387        // Should have cpu-used set for speed
1388        assert!(config.options.iter().any(|(k, _)| k == "cpu-used"));
1389        // Row-MT should be enabled for throughput
1390        assert!(config
1391            .options
1392            .iter()
1393            .any(|(k, v)| k == "row-mt" && v == "1"));
1394    }
1395
1396    // ── Ffv1Config tests ─────────────────────────────────────────────────
1397
1398    #[test]
1399    fn test_ffv1_config_new_defaults() {
1400        let cfg = Ffv1Config::new();
1401        assert!(matches!(cfg.level, Ffv1Level::Level3));
1402        assert!(matches!(cfg.coder, Ffv1Coder::Range));
1403        assert_eq!(cfg.slice_count, 4);
1404        assert_eq!(cfg.context_model, 0);
1405        assert!(cfg.checksum);
1406    }
1407
1408    #[test]
1409    fn test_ffv1_lossless_archive() {
1410        let cfg = Ffv1Config::lossless_archive();
1411        assert!(matches!(cfg.level, Ffv1Level::Level3));
1412        assert!(matches!(cfg.coder, Ffv1Coder::Range));
1413        assert_eq!(cfg.slice_count, 16);
1414        assert_eq!(cfg.context_model, 1);
1415        assert!(cfg.checksum);
1416    }
1417
1418    #[test]
1419    fn test_ffv1_lossless_fast() {
1420        let cfg = Ffv1Config::lossless_fast();
1421        assert!(matches!(cfg.level, Ffv1Level::Level1));
1422        assert!(matches!(cfg.coder, Ffv1Coder::GolombRice));
1423        assert!(!cfg.checksum);
1424    }
1425
1426    #[test]
1427    fn test_ffv1_with_slices() {
1428        let cfg = Ffv1Config::new().with_slices(9);
1429        assert_eq!(cfg.slice_count, 9);
1430    }
1431
1432    #[test]
1433    fn test_ffv1_build() {
1434        let config = Ffv1Config::new().build();
1435        assert_eq!(config.codec, "ffv1");
1436        assert!(config.options.iter().any(|(k, v)| k == "level" && v == "3"));
1437        assert!(config
1438            .options
1439            .iter()
1440            .any(|(k, v)| k == "slices" && v == "4"));
1441        assert!(config
1442            .options
1443            .iter()
1444            .any(|(k, v)| k == "slicecrc" && v == "1"));
1445    }
1446
1447    #[test]
1448    fn test_ffv1_level_values() {
1449        assert_eq!(Ffv1Level::Level1.as_u8(), 1);
1450        assert_eq!(Ffv1Level::Level3.as_u8(), 3);
1451    }
1452
1453    #[test]
1454    fn test_ffv1_coder_values() {
1455        assert_eq!(Ffv1Coder::GolombRice.as_u8(), 0);
1456        assert_eq!(Ffv1Coder::Range.as_u8(), 1);
1457    }
1458
1459    // ── OpusConfig advanced methods ───────────────────────────────────────
1460
1461    #[test]
1462    fn test_opus_voice_preset() {
1463        let config = OpusConfig::voice().build();
1464        assert_eq!(config.codec, "opus");
1465        assert!(config
1466            .options
1467            .iter()
1468            .any(|(k, v)| k == "application" && v == "voip"));
1469        assert!(config
1470            .options
1471            .iter()
1472            .any(|(k, v)| k == "inband_fec" && v == "1"));
1473    }
1474
1475    #[test]
1476    fn test_opus_music_preset() {
1477        let config = OpusConfig::music().build();
1478        assert_eq!(config.codec, "opus");
1479        assert!(config
1480            .options
1481            .iter()
1482            .any(|(k, v)| k == "application" && v == "audio"));
1483        assert!(config.options.iter().any(|(k, v)| k == "vbr" && v == "on"));
1484    }
1485
1486    #[test]
1487    fn test_opus_fullband_preset() {
1488        let config = OpusConfig::fullband().build();
1489        assert_eq!(config.codec, "opus");
1490        assert!(config.options.iter().any(|(k, _)| k == "cutoff"));
1491    }
1492
1493    #[test]
1494    fn test_opus_with_vbr() {
1495        let on = OpusConfig::new().with_vbr(true).build();
1496        let off = OpusConfig::new().with_vbr(false).build();
1497        assert!(on.options.iter().any(|(k, v)| k == "vbr" && v == "on"));
1498        assert!(off.options.iter().any(|(k, v)| k == "vbr" && v == "off"));
1499    }
1500
1501    #[test]
1502    fn test_opus_with_constrained_vbr() {
1503        let on = OpusConfig::new().with_constrained_vbr(true).build();
1504        let off = OpusConfig::new().with_constrained_vbr(false).build();
1505        assert!(on.options.iter().any(|(k, v)| k == "cvbr" && v == "1"));
1506        assert!(off.options.iter().any(|(k, v)| k == "cvbr" && v == "0"));
1507    }
1508
1509    #[test]
1510    fn test_opus_with_dtx() {
1511        let on = OpusConfig::new().with_dtx(true).build();
1512        let off = OpusConfig::new().with_dtx(false).build();
1513        assert!(on.options.iter().any(|(k, v)| k == "dtx" && v == "1"));
1514        assert!(off.options.iter().any(|(k, v)| k == "dtx" && v == "0"));
1515    }
1516
1517    #[test]
1518    fn test_opus_with_fec() {
1519        let config = OpusConfig::new().with_fec(true).build();
1520        assert!(config
1521            .options
1522            .iter()
1523            .any(|(k, v)| k == "inband_fec" && v == "1"));
1524        let config_off = OpusConfig::new().with_fec(false).build();
1525        assert!(config_off
1526            .options
1527            .iter()
1528            .any(|(k, v)| k == "inband_fec" && v == "0"));
1529    }
1530
1531    #[test]
1532    fn test_opus_with_packet_loss_perc() {
1533        let config = OpusConfig::new().with_packet_loss_perc(10).build();
1534        assert!(
1535            config
1536                .options
1537                .iter()
1538                .any(|(k, v)| k == "packet_loss_perc" && v == "10"),
1539            "packet_loss_perc option should be set"
1540        );
1541    }
1542
1543    // ── FlacConfig tests ─────────────────────────────────────────────────
1544
1545    #[test]
1546    fn test_flac_new_defaults() {
1547        let cfg = FlacConfig::new();
1548        assert_eq!(cfg.compression_level, 5);
1549        assert_eq!(cfg.block_size, 4096);
1550        assert!(!cfg.verify);
1551    }
1552
1553    #[test]
1554    fn test_flac_archival() {
1555        let cfg = FlacConfig::archival();
1556        assert_eq!(cfg.compression_level, 8);
1557        assert!(cfg.verify);
1558    }
1559
1560    #[test]
1561    fn test_flac_streaming() {
1562        let cfg = FlacConfig::streaming();
1563        assert_eq!(cfg.compression_level, 4);
1564        assert!(!cfg.verify);
1565    }
1566
1567    #[test]
1568    fn test_flac_fast() {
1569        let cfg = FlacConfig::fast();
1570        assert_eq!(cfg.compression_level, 0);
1571    }
1572
1573    #[test]
1574    fn test_flac_build() {
1575        let config = FlacConfig::new().build();
1576        assert_eq!(config.codec, "flac");
1577        assert!(
1578            config.options.iter().any(|(k, _)| k == "compression_level"),
1579            "FLAC config should include compression_level"
1580        );
1581    }
1582
1583    #[test]
1584    fn test_flac_archival_build_sets_verify() {
1585        let config = FlacConfig::archival().build();
1586        assert_eq!(config.codec, "flac");
1587        assert!(
1588            config
1589                .options
1590                .iter()
1591                .any(|(k, v)| k == "verify" && v == "1"),
1592            "Archival FLAC should enable verify"
1593        );
1594    }
1595
1596    // ── JxlConfig tests ─────────────────────────────────────────────────
1597
1598    #[test]
1599    fn test_jxl_new_defaults() {
1600        let cfg = JxlConfig::new();
1601        assert_eq!(cfg.quality, Some(75.0));
1602        assert_eq!(cfg.effort, JxlEffort::Squirrel);
1603        assert!(!cfg.progressive);
1604        assert!(cfg.photon_noise_iso.is_none());
1605        assert!(!cfg.modular);
1606        assert_eq!(cfg.color_space, JxlColorSpace::Xyb);
1607        assert_eq!(cfg.bit_depth, 8);
1608        assert!(!cfg.is_lossless());
1609    }
1610
1611    #[test]
1612    fn test_jxl_lossless() {
1613        let cfg = JxlConfig::lossless();
1614        assert!(cfg.is_lossless());
1615        assert!(cfg.quality.is_none());
1616        assert!(cfg.modular);
1617        assert_eq!(cfg.color_space, JxlColorSpace::Rgb);
1618        assert_eq!(cfg.effort, JxlEffort::Tortoise);
1619    }
1620
1621    #[test]
1622    fn test_jxl_web() {
1623        let cfg = JxlConfig::web();
1624        assert_eq!(cfg.quality, Some(80.0));
1625        assert!(cfg.progressive);
1626        assert!(!cfg.is_lossless());
1627    }
1628
1629    #[test]
1630    fn test_jxl_photography() {
1631        let cfg = JxlConfig::photography();
1632        assert_eq!(cfg.quality, Some(90.0));
1633        assert_eq!(cfg.photon_noise_iso, Some(400));
1634        assert_eq!(cfg.bit_depth, 16);
1635        assert!(cfg.progressive);
1636    }
1637
1638    #[test]
1639    fn test_jxl_with_quality() {
1640        let cfg = JxlConfig::new().with_quality(50.0);
1641        assert_eq!(cfg.quality, Some(50.0));
1642    }
1643
1644    #[test]
1645    fn test_jxl_with_effort() {
1646        let cfg = JxlConfig::new().with_effort(JxlEffort::Glacier);
1647        assert_eq!(cfg.effort, JxlEffort::Glacier);
1648    }
1649
1650    #[test]
1651    fn test_jxl_with_progressive() {
1652        let cfg = JxlConfig::new().with_progressive(true);
1653        assert!(cfg.progressive);
1654    }
1655
1656    #[test]
1657    fn test_jxl_with_photon_noise() {
1658        let cfg = JxlConfig::new().with_photon_noise(800);
1659        assert_eq!(cfg.photon_noise_iso, Some(800));
1660    }
1661
1662    #[test]
1663    fn test_jxl_with_bit_depth() {
1664        let cfg = JxlConfig::new().with_bit_depth(16);
1665        assert_eq!(cfg.bit_depth, 16);
1666    }
1667
1668    #[test]
1669    fn test_jxl_with_modular() {
1670        let cfg = JxlConfig::new().with_modular(true);
1671        assert!(cfg.modular);
1672    }
1673
1674    #[test]
1675    fn test_jxl_build_lossy() {
1676        let config = JxlConfig::new().build();
1677        assert_eq!(config.codec, "jxl");
1678        assert!(config.options.iter().any(|(k, _)| k == "quality"));
1679        assert!(config.options.iter().any(|(k, _)| k == "effort"));
1680        assert!(config.options.iter().any(|(k, _)| k == "color_space"));
1681        assert!(config.options.iter().any(|(k, _)| k == "bit_depth"));
1682        // Lossy should not set lossless flag
1683        assert!(!config
1684            .options
1685            .iter()
1686            .any(|(k, v)| k == "lossless" && v == "1"));
1687    }
1688
1689    #[test]
1690    fn test_jxl_build_lossless() {
1691        let config = JxlConfig::lossless().build();
1692        assert_eq!(config.codec, "jxl");
1693        assert!(config
1694            .options
1695            .iter()
1696            .any(|(k, v)| k == "lossless" && v == "1"));
1697        assert!(config
1698            .options
1699            .iter()
1700            .any(|(k, v)| k == "modular" && v == "1"));
1701        assert_eq!(config.rate_control, RateControlMode::Crf(0));
1702    }
1703
1704    #[test]
1705    fn test_jxl_build_progressive() {
1706        let config = JxlConfig::web().build();
1707        assert!(config
1708            .options
1709            .iter()
1710            .any(|(k, v)| k == "progressive" && v == "1"));
1711    }
1712
1713    #[test]
1714    fn test_jxl_build_photon_noise() {
1715        let config = JxlConfig::photography().build();
1716        assert!(config
1717            .options
1718            .iter()
1719            .any(|(k, v)| k == "photon_noise_iso" && v == "400"));
1720    }
1721
1722    #[test]
1723    fn test_jxl_effort_values() {
1724        assert_eq!(JxlEffort::Lightning.as_u8(), 1);
1725        assert_eq!(JxlEffort::Thunder.as_u8(), 2);
1726        assert_eq!(JxlEffort::Falcon.as_u8(), 3);
1727        assert_eq!(JxlEffort::Cheetah.as_u8(), 4);
1728        assert_eq!(JxlEffort::Hare.as_u8(), 5);
1729        assert_eq!(JxlEffort::Wombat.as_u8(), 6);
1730        assert_eq!(JxlEffort::Squirrel.as_u8(), 7);
1731        assert_eq!(JxlEffort::Kitten.as_u8(), 8);
1732        assert_eq!(JxlEffort::Tortoise.as_u8(), 9);
1733        assert_eq!(JxlEffort::Glacier.as_u8(), 10);
1734    }
1735
1736    #[test]
1737    fn test_jxl_effort_names() {
1738        assert_eq!(JxlEffort::Lightning.as_str(), "lightning");
1739        assert_eq!(JxlEffort::Squirrel.as_str(), "squirrel");
1740        assert_eq!(JxlEffort::Glacier.as_str(), "glacier");
1741    }
1742
1743    #[test]
1744    fn test_jxl_color_space_names() {
1745        assert_eq!(JxlColorSpace::Rgb.as_str(), "rgb");
1746        assert_eq!(JxlColorSpace::Xyb.as_str(), "xyb");
1747        assert_eq!(JxlColorSpace::Gray.as_str(), "gray");
1748    }
1749
1750    #[test]
1751    fn test_jxl_default_is_new() {
1752        let cfg = JxlConfig::default();
1753        assert_eq!(cfg.quality, Some(75.0));
1754        assert_eq!(cfg.effort, JxlEffort::Squirrel);
1755    }
1756}