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}