1#[derive(Clone, Copy, Debug, PartialEq)]
8#[non_exhaustive]
9pub enum Quality {
10 ApproxJpegli(f32),
13
14 ApproxMozjpeg(u8),
17
18 ApproxSsim2(f32),
21
22 ApproxButteraugli(f32),
25}
26
27impl Default for Quality {
28 fn default() -> Self {
29 Quality::ApproxJpegli(90.0)
30 }
31}
32
33impl From<f32> for Quality {
34 fn from(q: f32) -> Self {
35 Quality::ApproxJpegli(q)
36 }
37}
38
39impl From<u8> for Quality {
40 fn from(q: u8) -> Self {
41 Quality::ApproxJpegli(q as f32)
42 }
43}
44
45impl From<i32> for Quality {
46 fn from(q: i32) -> Self {
47 Quality::ApproxJpegli(q as f32)
48 }
49}
50
51impl Quality {
52 #[must_use]
54 pub fn to_internal(&self) -> f32 {
55 match self {
56 Quality::ApproxJpegli(q) => *q,
57 Quality::ApproxMozjpeg(q) => mozjpeg_to_internal(*q),
58 Quality::ApproxSsim2(score) => ssim2_to_internal(*score),
59 Quality::ApproxButteraugli(dist) => butteraugli_to_internal(*dist),
60 }
61 }
62
63 #[deprecated(
67 since = "0.5.0",
68 note = "Use Quality::from(f32) or Quality::ApproxJpegli(f32) instead"
69 )]
70 #[must_use]
71 pub fn from_quality(q: f32) -> Self {
72 Quality::ApproxJpegli(q)
73 }
74
75 #[deprecated(since = "0.5.0", note = "Use Quality::ApproxButteraugli(f32) instead")]
79 #[must_use]
80 pub fn from_distance(d: f32) -> Self {
81 Quality::ApproxButteraugli(d)
82 }
83
84 #[deprecated(since = "0.5.0", note = "Use Quality::ApproxJpegli(f32) instead")]
86 #[must_use]
87 #[allow(non_snake_case)] pub fn Traditional(q: f32) -> Self {
89 Quality::ApproxJpegli(q)
90 }
91
92 #[must_use]
96 pub fn to_distance(&self) -> f32 {
97 if let Quality::ApproxButteraugli(d) = self {
99 return *d;
100 }
101 let q = self.to_internal();
103 if q >= 100.0 {
104 0.01
105 } else if q >= 30.0 {
106 0.1 + (100.0 - q) * 0.09
107 } else {
108 53.0 / 3000.0 * q * q - 23.0 / 20.0 * q + 25.0
110 }
111 }
112}
113
114const MOZJPEG_TO_JPEGLI: [(u8, u8); 10] = [
117 (30, 28),
118 (40, 37),
119 (50, 47),
120 (60, 55),
121 (70, 65),
122 (75, 71),
123 (80, 77),
124 (85, 83),
125 (90, 89),
126 (95, 94),
127];
128
129fn mozjpeg_to_internal(q: u8) -> f32 {
130 if q >= 100 {
131 return 100.0;
132 }
133 if q <= 30 {
134 return (q as f32 / 30.0) * 28.0;
136 }
137
138 let mut lower = (30u8, 28u8);
140 let mut upper = (95u8, 94u8);
141
142 for &(moz_q, jpegli_q) in &MOZJPEG_TO_JPEGLI {
143 if moz_q <= q && moz_q > lower.0 {
144 lower = (moz_q, jpegli_q);
145 }
146 if moz_q >= q && moz_q < upper.0 {
147 upper = (moz_q, jpegli_q);
148 }
149 }
150
151 if lower.0 == upper.0 {
152 return lower.1 as f32;
153 }
154
155 let t = (q - lower.0) as f32 / (upper.0 - lower.0) as f32;
157 lower.1 as f32 + t * (upper.1 as f32 - lower.1 as f32)
158}
159
160const SSIM2_TO_JPEGLI: [(u8, u8); 8] = [
163 (70, 55), (75, 65),
165 (80, 73),
166 (85, 80),
167 (88, 85),
168 (90, 88),
169 (93, 92),
170 (95, 95),
171];
172
173fn ssim2_to_internal(score: f32) -> f32 {
174 if score >= 100.0 {
175 return 100.0;
176 }
177 if score <= 70.0 {
178 return (score / 70.0) * 55.0;
179 }
180
181 let q = score as u8;
182 let mut lower = (70u8, 55u8);
183 let mut upper = (95u8, 95u8);
184
185 for &(ssim_score, jpegli_q) in &SSIM2_TO_JPEGLI {
186 if ssim_score <= q && ssim_score > lower.0 {
187 lower = (ssim_score, jpegli_q);
188 }
189 if ssim_score >= q && ssim_score < upper.0 {
190 upper = (ssim_score, jpegli_q);
191 }
192 }
193
194 if lower.0 == upper.0 {
195 return lower.1 as f32;
196 }
197
198 let t = (score - lower.0 as f32) / (upper.0 - lower.0) as f32;
199 lower.1 as f32 + t * (upper.1 as f32 - lower.1 as f32)
200}
201
202const BUTTERAUGLI_TO_JPEGLI: [(f32, f32); 7] = [
205 (0.3, 96.0),
206 (0.5, 93.0),
207 (1.0, 88.0),
208 (1.5, 82.0),
209 (2.0, 76.0),
210 (3.0, 68.0),
211 (5.0, 55.0),
212];
213
214fn butteraugli_to_internal(dist: f32) -> f32 {
215 if dist <= 0.0 {
216 return 100.0;
217 }
218 if dist <= 0.3 {
219 return 96.0 + (0.3 - dist) / 0.3 * 4.0;
220 }
221 if dist >= 5.0 {
222 return 55.0 - (dist - 5.0) * 3.0;
223 }
224
225 let mut lower = (0.3f32, 96.0f32);
226 let mut upper = (5.0f32, 55.0f32);
227
228 for &(ba_dist, jpegli_q) in &BUTTERAUGLI_TO_JPEGLI {
229 if ba_dist <= dist && ba_dist > lower.0 {
230 lower = (ba_dist, jpegli_q);
231 }
232 if ba_dist >= dist && ba_dist < upper.0 {
233 upper = (ba_dist, jpegli_q);
234 }
235 }
236
237 if (lower.0 - upper.0).abs() < 0.001 {
238 return lower.1;
239 }
240
241 let t = (dist - lower.0) / (upper.0 - lower.0);
242 lower.1 + t * (upper.1 - lower.1)
243}
244
245#[derive(Clone, Debug)]
247#[non_exhaustive]
248#[derive(Default)]
249#[allow(clippy::large_enum_variant)] pub enum QuantTableConfig {
251 #[default]
253 Perceptual,
254
255 CustomBase {
259 luma: [f32; 64],
261 cb: [f32; 64],
263 cr: [f32; 64],
265 },
266
267 Exact {
270 luma: [u16; 64],
272 cb: [u16; 64],
274 cr: [u16; 64],
276 },
277}
278
279#[derive(Clone, Debug, Default)]
290#[non_exhaustive]
291#[allow(clippy::large_enum_variant)] pub enum ZeroBiasConfig {
293 #[default]
297 Perceptual,
298
299 Disabled,
302
303 Custom {
306 luma: ([f32; 64], [f32; 64]),
308 cb: ([f32; 64], [f32; 64]),
310 cr: ([f32; 64], [f32; 64]),
312 },
313}
314
315impl QuantTableConfig {
316 #[must_use]
321 pub(crate) fn to_custom_matrices(&self) -> Option<crate::quant::CustomQuantMatrices> {
322 use crate::quant::CustomQuantMatrices;
323
324 match self {
325 QuantTableConfig::Perceptual => None,
326 QuantTableConfig::CustomBase { luma, cb, cr } => {
327 let mut matrix = [0.0f32; 192];
329 matrix[..64].copy_from_slice(luma);
330 matrix[64..128].copy_from_slice(cb);
331 matrix[128..192].copy_from_slice(cr);
332 Some(CustomQuantMatrices::new().with_ycbcr(matrix))
333 }
334 QuantTableConfig::Exact { luma, cb, cr } => {
335 let mut tables = [0u16; 192];
337 tables[..64].copy_from_slice(luma);
338 tables[64..128].copy_from_slice(cb);
339 tables[128..192].copy_from_slice(cr);
340 Some(CustomQuantMatrices::new().with_direct_tables(tables))
341 }
342 }
343 }
344}
345
346impl ZeroBiasConfig {
347 #[must_use]
353 pub(crate) fn to_zero_bias_params(
354 &self,
355 component: usize,
356 distance: f32,
357 ) -> crate::quant::ZeroBiasParams {
358 use crate::quant::ZeroBiasParams;
359
360 match self {
361 ZeroBiasConfig::Perceptual => ZeroBiasParams::for_ycbcr(distance, component),
362 ZeroBiasConfig::Disabled => ZeroBiasParams::default(),
363 ZeroBiasConfig::Custom { luma, cb, cr } => {
364 let (mul, offset) = match component {
365 0 => luma,
366 1 => cb,
367 _ => cr,
368 };
369 ZeroBiasParams {
370 mul: *mul,
371 offset: *offset,
372 }
373 }
374 }
375 }
376}
377
378#[derive(Clone, Copy, Debug, PartialEq, Eq)]
380#[non_exhaustive]
381pub enum ColorMode {
382 YCbCr { subsampling: ChromaSubsampling },
384
385 Xyb { subsampling: XybSubsampling },
388
389 Grayscale,
391}
392
393impl Default for ColorMode {
394 fn default() -> Self {
395 ColorMode::YCbCr {
396 subsampling: ChromaSubsampling::None, }
398 }
399}
400
401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
403#[non_exhaustive]
404pub enum ChromaSubsampling {
405 None,
407 HalfHorizontal,
409 Quarter,
411 HalfVertical,
413}
414
415impl ChromaSubsampling {
416 #[must_use]
418 pub fn to_legacy(&self) -> crate::types::Subsampling {
419 match self {
420 ChromaSubsampling::None => crate::types::Subsampling::S444,
421 ChromaSubsampling::HalfHorizontal => crate::types::Subsampling::S422,
422 ChromaSubsampling::Quarter => crate::types::Subsampling::S420,
423 ChromaSubsampling::HalfVertical => crate::types::Subsampling::S440,
424 }
425 }
426
427 #[must_use]
429 pub const fn h_factor(&self) -> u8 {
430 match self {
431 ChromaSubsampling::None | ChromaSubsampling::HalfVertical => 1,
432 ChromaSubsampling::HalfHorizontal | ChromaSubsampling::Quarter => 2,
433 }
434 }
435
436 #[must_use]
438 pub const fn v_factor(&self) -> u8 {
439 match self {
440 ChromaSubsampling::None | ChromaSubsampling::HalfHorizontal => 1,
441 ChromaSubsampling::HalfVertical | ChromaSubsampling::Quarter => 2,
442 }
443 }
444}
445
446#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
451#[non_exhaustive]
452pub enum XybSubsampling {
453 Full,
455 #[default]
457 BQuarter,
458}
459
460#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
464#[non_exhaustive]
465pub enum DownsamplingMethod {
466 #[default]
468 Box,
469 GammaAware,
471 GammaAwareIterative,
473}
474
475impl DownsamplingMethod {
476 #[must_use]
478 pub fn to_legacy(&self) -> crate::types::ChromaDownsampling {
479 match self {
480 DownsamplingMethod::Box => crate::types::ChromaDownsampling::Box,
481 DownsamplingMethod::GammaAware => crate::types::ChromaDownsampling::GammaAware,
482 DownsamplingMethod::GammaAwareIterative => {
483 crate::types::ChromaDownsampling::GammaAwareIterative
484 }
485 }
486 }
487}
488
489#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496#[non_exhaustive]
497pub enum PixelLayout {
498 Rgb8Srgb,
501 Bgr8Srgb,
503 Rgbx8Srgb,
505 Bgrx8Srgb,
507 Gray8Srgb,
509
510 Rgb16Linear,
513 Rgbx16Linear,
515 Gray16Linear,
517
518 RgbF32Linear,
521 RgbxF32Linear,
523 GrayF32Linear,
525
526 YCbCr8,
529 YCbCrF32,
531}
532
533impl PixelLayout {
534 #[must_use]
536 pub const fn bytes_per_pixel(&self) -> usize {
537 match self {
538 Self::Gray8Srgb => 1,
539 Self::Gray16Linear => 2,
540 Self::Rgb8Srgb | Self::Bgr8Srgb | Self::YCbCr8 => 3,
541 Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::GrayF32Linear => 4,
542 Self::Rgb16Linear => 6,
543 Self::Rgbx16Linear => 8,
544 Self::RgbF32Linear | Self::YCbCrF32 => 12,
545 Self::RgbxF32Linear => 16,
546 }
547 }
548
549 #[must_use]
551 pub const fn channels(&self) -> usize {
552 match self {
553 Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear => 1,
554 Self::Rgb8Srgb
555 | Self::Bgr8Srgb
556 | Self::Rgb16Linear
557 | Self::RgbF32Linear
558 | Self::YCbCr8
559 | Self::YCbCrF32 => 3,
560 Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::Rgbx16Linear | Self::RgbxF32Linear => 4,
561 }
562 }
563
564 #[must_use]
566 pub const fn is_grayscale(&self) -> bool {
567 matches!(
568 self,
569 Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear
570 )
571 }
572
573 #[must_use]
575 pub const fn is_ycbcr(&self) -> bool {
576 matches!(self, Self::YCbCr8 | Self::YCbCrF32)
577 }
578
579 #[must_use]
581 pub const fn is_bgr(&self) -> bool {
582 matches!(self, Self::Bgr8Srgb | Self::Bgrx8Srgb)
583 }
584
585 #[must_use]
587 pub const fn is_float(&self) -> bool {
588 matches!(
589 self,
590 Self::RgbF32Linear | Self::RgbxF32Linear | Self::GrayF32Linear | Self::YCbCrF32
591 )
592 }
593
594 #[must_use]
596 pub const fn is_16bit(&self) -> bool {
597 matches!(
598 self,
599 Self::Rgb16Linear | Self::Rgbx16Linear | Self::Gray16Linear
600 )
601 }
602
603 #[must_use]
605 pub fn to_legacy(&self) -> crate::types::PixelFormat {
606 match self {
607 Self::Rgb8Srgb => crate::types::PixelFormat::Rgb,
608 Self::Bgr8Srgb => crate::types::PixelFormat::Bgr,
609 Self::Rgbx8Srgb => crate::types::PixelFormat::Rgba,
610 Self::Bgrx8Srgb => crate::types::PixelFormat::Bgrx,
611 Self::Gray8Srgb => crate::types::PixelFormat::Gray,
612 Self::Rgb16Linear => crate::types::PixelFormat::Rgb16,
613 Self::Rgbx16Linear => crate::types::PixelFormat::Rgba16,
614 Self::Gray16Linear => crate::types::PixelFormat::Gray16,
615 Self::RgbF32Linear => crate::types::PixelFormat::RgbF32,
616 Self::RgbxF32Linear => crate::types::PixelFormat::RgbaF32,
617 Self::GrayF32Linear => crate::types::PixelFormat::GrayF32,
618 Self::YCbCr8 | Self::YCbCrF32 => crate::types::PixelFormat::Rgb,
620 }
621 }
622}
623
624#[derive(Clone, Copy, Debug)]
628pub struct YCbCrPlanes<'a> {
629 pub y: &'a [f32],
630 pub y_stride: usize,
631 pub cb: &'a [f32],
632 pub cb_stride: usize,
633 pub cr: &'a [f32],
634 pub cr_stride: usize,
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640
641 #[test]
642 fn test_quality_default() {
643 let q = Quality::default();
644 assert!(matches!(q, Quality::ApproxJpegli(90.0)));
645 }
646
647 #[test]
648 fn test_quality_from() {
649 let q: Quality = 85.0.into();
650 assert!(matches!(q, Quality::ApproxJpegli(85.0)));
651
652 let q: Quality = 75u8.into();
653 assert!(matches!(q, Quality::ApproxJpegli(75.0)));
654 }
655
656 #[test]
657 fn test_pixel_layout_bytes() {
658 assert_eq!(PixelLayout::Rgb8Srgb.bytes_per_pixel(), 3);
659 assert_eq!(PixelLayout::Rgbx8Srgb.bytes_per_pixel(), 4);
660 assert_eq!(PixelLayout::RgbF32Linear.bytes_per_pixel(), 12);
661 assert_eq!(PixelLayout::Gray8Srgb.bytes_per_pixel(), 1);
662 }
663
664 #[test]
665 fn test_chroma_subsampling_factors() {
666 assert_eq!(ChromaSubsampling::None.h_factor(), 1);
667 assert_eq!(ChromaSubsampling::None.v_factor(), 1);
668 assert_eq!(ChromaSubsampling::Quarter.h_factor(), 2);
669 assert_eq!(ChromaSubsampling::Quarter.v_factor(), 2);
670 assert_eq!(ChromaSubsampling::HalfHorizontal.h_factor(), 2);
671 assert_eq!(ChromaSubsampling::HalfHorizontal.v_factor(), 1);
672 }
673}
674
675#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
707#[non_exhaustive]
708#[cfg(feature = "parallel")]
709pub enum ParallelEncoding {
710 #[default]
716 Auto,
717}