mozjpeg_rs/
types.rs

1//! Core type definitions for mozjpeg encoder.
2//!
3//! This module defines all the types needed for JPEG encoding,
4//! matching the semantics of mozjpeg's C types but with idiomatic Rust design.
5
6use crate::consts::{DCTSIZE2, MAX_COMPS_IN_SCAN};
7
8// =============================================================================
9// Pixel Density
10// =============================================================================
11
12/// Pixel density unit for JFIF APP0 marker.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14#[repr(u8)]
15pub enum DensityUnit {
16    /// No units - X/Y specify pixel aspect ratio only
17    #[default]
18    None = 0,
19    /// Dots per inch
20    DotsPerInch = 1,
21    /// Dots per centimeter
22    DotsPerCm = 2,
23}
24
25/// Pixel density specification for the JFIF APP0 marker.
26///
27/// This affects how the image is displayed at its "natural" size,
28/// but most software ignores JFIF density in favor of EXIF metadata.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct PixelDensity {
31    /// Density unit
32    pub unit: DensityUnit,
33    /// Horizontal density (or aspect ratio numerator if unit is None)
34    pub x: u16,
35    /// Vertical density (or aspect ratio denominator if unit is None)
36    pub y: u16,
37}
38
39impl Default for PixelDensity {
40    fn default() -> Self {
41        Self {
42            unit: DensityUnit::DotsPerInch,
43            x: 72,
44            y: 72,
45        }
46    }
47}
48
49impl PixelDensity {
50    /// Create density in dots per inch.
51    pub const fn dpi(x: u16, y: u16) -> Self {
52        Self {
53            unit: DensityUnit::DotsPerInch,
54            x,
55            y,
56        }
57    }
58
59    /// Create density in dots per centimeter.
60    pub const fn dpcm(x: u16, y: u16) -> Self {
61        Self {
62            unit: DensityUnit::DotsPerCm,
63            x,
64            y,
65        }
66    }
67
68    /// Create pixel aspect ratio (no physical units).
69    pub const fn aspect_ratio(x: u16, y: u16) -> Self {
70        Self {
71            unit: DensityUnit::None,
72            x,
73            y,
74        }
75    }
76}
77
78// =============================================================================
79// Color Spaces
80// =============================================================================
81
82/// Input color space for the encoder.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84#[repr(u8)]
85pub enum ColorSpace {
86    /// Unknown/unspecified color space
87    #[default]
88    Unknown = 0,
89    /// Grayscale (1 component)
90    Grayscale = 1,
91    /// RGB (3 components, standard order)
92    Rgb = 2,
93    /// YCbCr (3 components)
94    YCbCr = 3,
95    /// CMYK (4 components)
96    Cmyk = 4,
97    /// YCCK (4 components, Y/Cb/Cr/K)
98    Ycck = 5,
99    /// RGB with explicit order (red/green/blue)
100    ExtRgb = 6,
101    /// RGBX (RGB with padding byte)
102    ExtRgbx = 7,
103    /// BGR (blue/green/red)
104    ExtBgr = 8,
105    /// BGRX (BGR with padding byte)
106    ExtBgrx = 9,
107    /// XBGR (padding/blue/green/red)
108    ExtXbgr = 10,
109    /// XRGB (padding/red/green/blue)
110    ExtXrgb = 11,
111    /// RGBA (with alpha)
112    ExtRgba = 12,
113    /// BGRA (with alpha)
114    ExtBgra = 13,
115    /// ABGR (alpha first)
116    ExtAbgr = 14,
117    /// ARGB (alpha first)
118    ExtArgb = 15,
119}
120
121impl ColorSpace {
122    /// Returns the number of components for this color space.
123    pub const fn num_components(self) -> usize {
124        match self {
125            ColorSpace::Unknown => 0,
126            ColorSpace::Grayscale => 1,
127            ColorSpace::Rgb | ColorSpace::YCbCr => 3,
128            ColorSpace::ExtRgb | ColorSpace::ExtBgr => 3,
129            ColorSpace::Cmyk | ColorSpace::Ycck => 4,
130            ColorSpace::ExtRgbx
131            | ColorSpace::ExtBgrx
132            | ColorSpace::ExtXbgr
133            | ColorSpace::ExtXrgb
134            | ColorSpace::ExtRgba
135            | ColorSpace::ExtBgra
136            | ColorSpace::ExtAbgr
137            | ColorSpace::ExtArgb => 4,
138        }
139    }
140
141    /// Returns the bytes per pixel for this color space.
142    pub const fn bytes_per_pixel(self) -> usize {
143        self.num_components()
144    }
145
146    /// Returns true if this is an RGB variant.
147    pub const fn is_rgb_variant(self) -> bool {
148        matches!(
149            self,
150            ColorSpace::Rgb
151                | ColorSpace::ExtRgb
152                | ColorSpace::ExtRgbx
153                | ColorSpace::ExtBgr
154                | ColorSpace::ExtBgrx
155                | ColorSpace::ExtXbgr
156                | ColorSpace::ExtXrgb
157                | ColorSpace::ExtRgba
158                | ColorSpace::ExtBgra
159                | ColorSpace::ExtAbgr
160                | ColorSpace::ExtArgb
161        )
162    }
163}
164
165// =============================================================================
166// Compression Profile
167// =============================================================================
168
169/// Compression profile controlling which mozjpeg features are enabled.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
171#[repr(u32)]
172pub enum CompressionProfile {
173    /// Maximum compression - all mozjpeg features enabled.
174    /// - Progressive mode
175    /// - Trellis quantization
176    /// - Optimized Huffman tables
177    /// - ImageMagick quantization tables (index 3)
178    #[default]
179    MaxCompression = 0x5D083AAD,
180    /// Fastest - libjpeg-turbo defaults, no mozjpeg extensions.
181    /// - Baseline (non-progressive)
182    /// - Standard quantization
183    /// - Pre-computed Huffman tables
184    Fastest = 0x2AEA5CB4,
185}
186
187// =============================================================================
188// DCT Method
189// =============================================================================
190
191/// DCT algorithm selection.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
193#[repr(u8)]
194pub enum DctMethod {
195    /// Accurate integer method (default)
196    #[default]
197    IntSlow = 0,
198    /// Less accurate but faster integer method
199    IntFast = 1,
200    /// Floating-point method
201    Float = 2,
202}
203
204// =============================================================================
205// Sampling Factor / Subsampling
206// =============================================================================
207
208/// Chroma subsampling mode.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
210pub enum Subsampling {
211    /// 4:4:4 - No subsampling (highest quality)
212    #[default]
213    S444,
214    /// 4:2:2 - Horizontal subsampling
215    S422,
216    /// 4:2:0 - Horizontal and vertical subsampling (most common)
217    S420,
218    /// 4:4:0 - Vertical subsampling only
219    S440,
220    /// Grayscale (1 component)
221    Gray,
222}
223
224impl Subsampling {
225    /// Returns (h_samp_factor, v_samp_factor) for luminance component.
226    pub const fn luma_factors(self) -> (u8, u8) {
227        match self {
228            Subsampling::S444 | Subsampling::Gray => (1, 1),
229            Subsampling::S422 => (2, 1),
230            Subsampling::S420 => (2, 2),
231            Subsampling::S440 => (1, 2),
232        }
233    }
234
235    /// Returns (h_samp_factor, v_samp_factor) for chroma components.
236    pub const fn chroma_factors(self) -> (u8, u8) {
237        (1, 1) // Chroma always 1x1 relative to max
238    }
239}
240
241// =============================================================================
242// Scan Info (for progressive JPEG)
243// =============================================================================
244
245/// Describes a single scan in a multi-scan (progressive) JPEG.
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub struct ScanInfo {
248    /// Number of components in this scan (1-4)
249    pub comps_in_scan: u8,
250    /// Component indices for this scan
251    pub component_index: [u8; MAX_COMPS_IN_SCAN],
252    /// Spectral selection start (0 for DC, 1-63 for AC)
253    pub ss: u8,
254    /// Spectral selection end (0 for DC-only, 63 for full AC)
255    pub se: u8,
256    /// Successive approximation high bit
257    pub ah: u8,
258    /// Successive approximation low bit (point transform)
259    pub al: u8,
260}
261
262impl ScanInfo {
263    /// Create a DC-only scan for all components.
264    pub const fn dc_scan(num_components: u8) -> Self {
265        Self {
266            comps_in_scan: num_components,
267            component_index: [0, 1, 2, 3],
268            ss: 0,
269            se: 0,
270            ah: 0,
271            al: 0,
272        }
273    }
274
275    /// Create an AC scan for a single component.
276    pub const fn ac_scan(component: u8, ss: u8, se: u8, ah: u8, al: u8) -> Self {
277        Self {
278            comps_in_scan: 1,
279            component_index: [component, 0, 0, 0],
280            ss,
281            se,
282            ah,
283            al,
284        }
285    }
286
287    /// Returns true if this is a DC-only scan.
288    pub const fn is_dc_scan(&self) -> bool {
289        self.ss == 0 && self.se == 0
290    }
291
292    /// Returns true if this is a refinement scan (successive approximation).
293    pub const fn is_refinement(&self) -> bool {
294        self.ah != 0
295    }
296
297    /// Create a DC scan for a single component.
298    pub const fn dc_scan_single(component: u8) -> Self {
299        Self {
300            comps_in_scan: 1,
301            component_index: [component, 0, 0, 0],
302            ss: 0,
303            se: 0,
304            ah: 0,
305            al: 0,
306        }
307    }
308
309    /// Create a DC scan for two components (e.g., Cb and Cr).
310    pub const fn dc_scan_pair(comp1: u8, comp2: u8) -> Self {
311        Self {
312            comps_in_scan: 2,
313            component_index: [comp1, comp2, 0, 0],
314            ss: 0,
315            se: 0,
316            ah: 0,
317            al: 0,
318        }
319    }
320}
321
322impl Default for ScanInfo {
323    fn default() -> Self {
324        Self::dc_scan(3)
325    }
326}
327
328// =============================================================================
329// Component Info
330// =============================================================================
331
332/// Information about a single image component (color channel).
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub struct ComponentInfo {
335    /// Component identifier (1=Y, 2=Cb, 3=Cr for YCbCr)
336    pub component_id: u8,
337    /// Index in component array
338    pub component_index: u8,
339    /// Horizontal sampling factor (1-4)
340    pub h_samp_factor: u8,
341    /// Vertical sampling factor (1-4)
342    pub v_samp_factor: u8,
343    /// Quantization table index (0-3)
344    pub quant_tbl_no: u8,
345    /// DC Huffman table index (0-3)
346    pub dc_tbl_no: u8,
347    /// AC Huffman table index (0-3)
348    pub ac_tbl_no: u8,
349}
350
351impl Default for ComponentInfo {
352    fn default() -> Self {
353        Self {
354            component_id: 1,
355            component_index: 0,
356            h_samp_factor: 1,
357            v_samp_factor: 1,
358            quant_tbl_no: 0,
359            dc_tbl_no: 0,
360            ac_tbl_no: 0,
361        }
362    }
363}
364
365// =============================================================================
366// Quantization Table
367// =============================================================================
368
369/// A quantization table with 64 coefficients.
370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
371pub struct QuantTable {
372    /// Quantization values in natural (row-major) order
373    pub values: [u16; DCTSIZE2],
374    /// True if this table has been written to the output
375    pub sent: bool,
376}
377
378impl QuantTable {
379    /// Create a new quantization table from values.
380    pub const fn new(values: [u16; DCTSIZE2]) -> Self {
381        Self {
382            values,
383            sent: false,
384        }
385    }
386
387    /// Create from a base table scaled by a quality factor.
388    /// Scale factor is a percentage (100 = use table as-is).
389    pub fn scaled(base: &[u16; DCTSIZE2], scale_factor: u32, force_baseline: bool) -> Self {
390        let mut values = [0u16; DCTSIZE2];
391        for i in 0..DCTSIZE2 {
392            let mut temp = ((base[i] as u32) * scale_factor + 50) / 100;
393            // Clamp to valid range
394            if temp == 0 {
395                temp = 1;
396            }
397            if temp > 32767 {
398                temp = 32767;
399            }
400            if force_baseline && temp > 255 {
401                temp = 255;
402            }
403            values[i] = temp as u16;
404        }
405        Self {
406            values,
407            sent: false,
408        }
409    }
410}
411
412impl Default for QuantTable {
413    fn default() -> Self {
414        Self {
415            values: [16; DCTSIZE2], // Flat table
416            sent: false,
417        }
418    }
419}
420
421// =============================================================================
422// Huffman Table
423// =============================================================================
424
425/// A Huffman coding table.
426#[derive(Debug, Clone, Default, PartialEq, Eq)]
427pub struct HuffmanTable {
428    /// Number of codes of each length (`bits[k]` = # of symbols with k-bit codes).
429    /// `bits[0]` is unused.
430    pub bits: [u8; 17],
431    /// Symbol values in order of increasing code length
432    pub huffval: Vec<u8>,
433    /// True if this table has been written to the output
434    pub sent: bool,
435}
436
437impl HuffmanTable {
438    /// Create a new Huffman table from bits and values.
439    pub fn new(bits: [u8; 17], huffval: Vec<u8>) -> Self {
440        Self {
441            bits,
442            huffval,
443            sent: false,
444        }
445    }
446
447    /// Returns the total number of symbols in this table.
448    pub fn num_symbols(&self) -> usize {
449        self.bits[1..].iter().map(|&b| b as usize).sum()
450    }
451}
452
453// =============================================================================
454// Trellis Configuration
455// =============================================================================
456
457/// Configuration for trellis quantization.
458#[derive(Debug, Clone, Copy, PartialEq)]
459pub struct TrellisConfig {
460    /// Enable trellis quantization for AC coefficients
461    pub enabled: bool,
462    /// Enable trellis quantization for DC coefficients
463    pub dc_enabled: bool,
464    /// Optimize for sequences of EOB
465    pub eob_opt: bool,
466    /// Use perceptual lambda weighting table
467    pub use_lambda_weight_tbl: bool,
468    /// Consider scan order in trellis optimization
469    pub use_scans_in_trellis: bool,
470    /// Optimize quantization table in trellis loop
471    pub q_opt: bool,
472    /// Lambda log scale parameter 1
473    pub lambda_log_scale1: f32,
474    /// Lambda log scale parameter 2
475    pub lambda_log_scale2: f32,
476    /// Frequency split point for spectral selection
477    pub freq_split: i32,
478    /// Number of trellis optimization loops
479    pub num_loops: i32,
480    /// DC delta weight for vertical gradient consideration
481    pub delta_dc_weight: f32,
482}
483
484impl Default for TrellisConfig {
485    fn default() -> Self {
486        Self {
487            enabled: true,
488            dc_enabled: true,
489            eob_opt: true,
490            use_lambda_weight_tbl: true,
491            use_scans_in_trellis: false,
492            q_opt: false,
493            lambda_log_scale1: crate::consts::DEFAULT_LAMBDA_LOG_SCALE1,
494            lambda_log_scale2: crate::consts::DEFAULT_LAMBDA_LOG_SCALE2,
495            freq_split: crate::consts::DEFAULT_TRELLIS_FREQ_SPLIT,
496            num_loops: crate::consts::DEFAULT_TRELLIS_NUM_LOOPS,
497            delta_dc_weight: crate::consts::DEFAULT_TRELLIS_DELTA_DC_WEIGHT,
498        }
499    }
500}
501
502impl TrellisConfig {
503    /// Configuration with trellis disabled (fastest mode).
504    pub const fn disabled() -> Self {
505        Self {
506            enabled: false,
507            dc_enabled: false,
508            eob_opt: false,
509            use_lambda_weight_tbl: false,
510            use_scans_in_trellis: false,
511            q_opt: false,
512            lambda_log_scale1: 14.75,
513            lambda_log_scale2: 16.5,
514            freq_split: 8,
515            num_loops: 1,
516            delta_dc_weight: 0.0,
517        }
518    }
519
520    /// Preset that favors smaller file sizes over quality.
521    ///
522    /// Uses lower lambda values which makes the trellis algorithm more aggressive
523    /// about zeroing coefficients, resulting in smaller files at the cost of some
524    /// quality loss.
525    ///
526    /// Lambda = 2^scale1 / (2^scale2 + norm). Lower lambda = more aggressive zeroing.
527    pub fn favor_size() -> Self {
528        Self {
529            lambda_log_scale1: 14.0, // Lower = less distortion penalty
530            lambda_log_scale2: 17.0, // Higher = smaller lambda
531            ..Self::default()
532        }
533    }
534
535    /// Preset that favors quality over file size.
536    ///
537    /// Uses higher lambda values which makes the trellis algorithm more conservative,
538    /// preserving more coefficients for better quality at the cost of larger files.
539    ///
540    /// Lambda = 2^scale1 / (2^scale2 + norm). Higher lambda = more conservative.
541    pub fn favor_quality() -> Self {
542        Self {
543            lambda_log_scale1: 15.5, // Higher = more distortion penalty
544            lambda_log_scale2: 16.0, // Lower = larger lambda
545            ..Self::default()
546        }
547    }
548
549    /// Set the lambda log scale parameters directly.
550    ///
551    /// These control the rate-distortion tradeoff in trellis quantization:
552    /// - `scale1`: Controls rate penalty (higher = smaller files, default 14.75)
553    /// - `scale2`: Controls distortion sensitivity (higher = better quality, default 16.5)
554    ///
555    /// The effective lambda is: `2^scale1 / (2^scale2 + block_norm)`
556    pub fn lambda_scales(mut self, scale1: f32, scale2: f32) -> Self {
557        self.lambda_log_scale1 = scale1;
558        self.lambda_log_scale2 = scale2;
559        self
560    }
561
562    /// Adjust rate-distortion balance with a simple factor.
563    ///
564    /// - `factor > 1.0`: Favor quality (higher lambda, more conservative)
565    /// - `factor < 1.0`: Favor smaller files (lower lambda, more aggressive)
566    /// - `factor = 1.0`: Default behavior
567    ///
568    /// The factor multiplies the effective lambda value logarithmically.
569    pub fn rd_factor(mut self, factor: f32) -> Self {
570        // Adjust scale1 by log2 of the factor
571        // factor=2.0 adds 1.0 to scale1 (doubles lambda → more quality)
572        // factor=0.5 subtracts 1.0 from scale1 (halves lambda → smaller files)
573        self.lambda_log_scale1 = 14.75 + factor.log2();
574        self
575    }
576
577    /// Enable or disable AC coefficient trellis optimization.
578    pub fn ac_trellis(mut self, enabled: bool) -> Self {
579        self.enabled = enabled;
580        self
581    }
582
583    /// Enable or disable DC coefficient trellis optimization.
584    pub fn dc_trellis(mut self, enabled: bool) -> Self {
585        self.dc_enabled = enabled;
586        self
587    }
588
589    /// Enable or disable EOB run optimization.
590    pub fn eob_optimization(mut self, enabled: bool) -> Self {
591        self.eob_opt = enabled;
592        self
593    }
594}
595
596// =============================================================================
597// DCT Block Types
598// =============================================================================
599
600/// A single 8x8 block of DCT coefficients.
601pub type DctBlock = [i16; DCTSIZE2];
602
603/// A single 8x8 block of pixel samples.
604pub type SampleBlock = [u8; DCTSIZE2];
605
606/// A single 8x8 block of floating-point values.
607pub type FloatBlock = [f32; DCTSIZE2];
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612
613    #[test]
614    fn test_colorspace_components() {
615        assert_eq!(ColorSpace::Grayscale.num_components(), 1);
616        assert_eq!(ColorSpace::Rgb.num_components(), 3);
617        assert_eq!(ColorSpace::YCbCr.num_components(), 3);
618        assert_eq!(ColorSpace::Cmyk.num_components(), 4);
619        assert_eq!(ColorSpace::ExtRgba.num_components(), 4);
620    }
621
622    #[test]
623    fn test_subsampling_factors() {
624        assert_eq!(Subsampling::S444.luma_factors(), (1, 1));
625        assert_eq!(Subsampling::S422.luma_factors(), (2, 1));
626        assert_eq!(Subsampling::S420.luma_factors(), (2, 2));
627        assert_eq!(Subsampling::S440.luma_factors(), (1, 2));
628    }
629
630    #[test]
631    fn test_scan_info() {
632        let dc = ScanInfo::dc_scan(3);
633        assert!(dc.is_dc_scan());
634        assert!(!dc.is_refinement());
635
636        let ac = ScanInfo::ac_scan(0, 1, 63, 0, 0);
637        assert!(!ac.is_dc_scan());
638        assert!(!ac.is_refinement());
639
640        let refine = ScanInfo::ac_scan(0, 1, 63, 1, 0);
641        assert!(refine.is_refinement());
642    }
643
644    #[test]
645    fn test_quant_table_scaling() {
646        let base = [16u16; DCTSIZE2];
647
648        // 100% scale should give same values
649        let scaled = QuantTable::scaled(&base, 100, false);
650        assert_eq!(scaled.values, base);
651
652        // 200% scale should double
653        let scaled = QuantTable::scaled(&base, 200, false);
654        assert_eq!(scaled.values[0], 32);
655
656        // 50% scale should halve
657        let scaled = QuantTable::scaled(&base, 50, false);
658        assert_eq!(scaled.values[0], 8);
659
660        // Force baseline should clamp to 255
661        let high = [1000u16; DCTSIZE2];
662        let scaled = QuantTable::scaled(&high, 100, true);
663        assert_eq!(scaled.values[0], 255);
664    }
665
666    #[test]
667    fn test_trellis_config_defaults() {
668        let config = TrellisConfig::default();
669        assert!(config.enabled);
670        assert!(config.dc_enabled);
671        assert_eq!(config.lambda_log_scale1, 14.75);
672        assert_eq!(config.num_loops, 1);
673
674        let disabled = TrellisConfig::disabled();
675        assert!(!disabled.enabled);
676    }
677
678    #[test]
679    fn test_trellis_config_presets() {
680        let favor_size = TrellisConfig::favor_size();
681        assert!(favor_size.enabled);
682        assert!(favor_size.lambda_log_scale1 < 14.75); // Lower = more aggressive
683
684        let favor_quality = TrellisConfig::favor_quality();
685        assert!(favor_quality.enabled);
686        assert!(favor_quality.lambda_log_scale1 > 14.75); // Higher = more conservative
687    }
688
689    #[test]
690    fn test_trellis_config_builder() {
691        let config = TrellisConfig::default()
692            .lambda_scales(15.0, 17.0)
693            .ac_trellis(true)
694            .dc_trellis(false)
695            .eob_optimization(false);
696
697        assert_eq!(config.lambda_log_scale1, 15.0);
698        assert_eq!(config.lambda_log_scale2, 17.0);
699        assert!(config.enabled);
700        assert!(!config.dc_enabled);
701        assert!(!config.eob_opt);
702    }
703
704    #[test]
705    fn test_trellis_rd_factor() {
706        // factor=1.0 should give default scale1
707        let config = TrellisConfig::default().rd_factor(1.0);
708        assert!((config.lambda_log_scale1 - 14.75).abs() < 0.01);
709
710        // factor=2.0 should add 1.0 to scale1
711        let config = TrellisConfig::default().rd_factor(2.0);
712        assert!((config.lambda_log_scale1 - 15.75).abs() < 0.01);
713
714        // factor=0.5 should subtract 1.0 from scale1
715        let config = TrellisConfig::default().rd_factor(0.5);
716        assert!((config.lambda_log_scale1 - 13.75).abs() < 0.01);
717    }
718}