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 #[must_use]
67 pub fn to_distance(&self) -> f32 {
68 if let Quality::ApproxButteraugli(d) = self {
70 return *d;
71 }
72 let q = self.to_internal();
74 if q >= 100.0 {
75 0.01
76 } else if q >= 30.0 {
77 0.1 + (100.0 - q) * 0.09
78 } else {
79 53.0 / 3000.0 * q * q - 23.0 / 20.0 * q + 25.0
81 }
82 }
83}
84
85const MOZJPEG_TO_JPEGLI: [(u8, u8); 10] = [
88 (30, 28),
89 (40, 37),
90 (50, 47),
91 (60, 55),
92 (70, 65),
93 (75, 71),
94 (80, 77),
95 (85, 83),
96 (90, 89),
97 (95, 94),
98];
99
100fn mozjpeg_to_internal(q: u8) -> f32 {
101 if q >= 100 {
102 return 100.0;
103 }
104 if q <= 30 {
105 return (q as f32 / 30.0) * 28.0;
107 }
108
109 let mut lower = (30u8, 28u8);
111 let mut upper = (95u8, 94u8);
112
113 for &(moz_q, jpegli_q) in &MOZJPEG_TO_JPEGLI {
114 if moz_q <= q && moz_q > lower.0 {
115 lower = (moz_q, jpegli_q);
116 }
117 if moz_q >= q && moz_q < upper.0 {
118 upper = (moz_q, jpegli_q);
119 }
120 }
121
122 if lower.0 == upper.0 {
123 return lower.1 as f32;
124 }
125
126 let t = (q - lower.0) as f32 / (upper.0 - lower.0) as f32;
128 lower.1 as f32 + t * (upper.1 as f32 - lower.1 as f32)
129}
130
131const SSIM2_TO_JPEGLI: [(u8, u8); 8] = [
134 (70, 55), (75, 65),
136 (80, 73),
137 (85, 80),
138 (88, 85),
139 (90, 88),
140 (93, 92),
141 (95, 95),
142];
143
144fn ssim2_to_internal(score: f32) -> f32 {
145 if score >= 100.0 {
146 return 100.0;
147 }
148 if score <= 70.0 {
149 return (score / 70.0) * 55.0;
150 }
151
152 let q = score as u8;
153 let mut lower = (70u8, 55u8);
154 let mut upper = (95u8, 95u8);
155
156 for &(ssim_score, jpegli_q) in &SSIM2_TO_JPEGLI {
157 if ssim_score <= q && ssim_score > lower.0 {
158 lower = (ssim_score, jpegli_q);
159 }
160 if ssim_score >= q && ssim_score < upper.0 {
161 upper = (ssim_score, jpegli_q);
162 }
163 }
164
165 if lower.0 == upper.0 {
166 return lower.1 as f32;
167 }
168
169 let t = (score - lower.0 as f32) / (upper.0 - lower.0) as f32;
170 lower.1 as f32 + t * (upper.1 as f32 - lower.1 as f32)
171}
172
173const BUTTERAUGLI_TO_JPEGLI: [(f32, f32); 7] = [
176 (0.3, 96.0),
177 (0.5, 93.0),
178 (1.0, 88.0),
179 (1.5, 82.0),
180 (2.0, 76.0),
181 (3.0, 68.0),
182 (5.0, 55.0),
183];
184
185fn butteraugli_to_internal(dist: f32) -> f32 {
186 if dist <= 0.0 {
187 return 100.0;
188 }
189 if dist <= 0.3 {
190 return 96.0 + (0.3 - dist) / 0.3 * 4.0;
191 }
192 if dist >= 5.0 {
193 return 55.0 - (dist - 5.0) * 3.0;
194 }
195
196 let mut lower = (0.3f32, 96.0f32);
197 let mut upper = (5.0f32, 55.0f32);
198
199 for &(ba_dist, jpegli_q) in &BUTTERAUGLI_TO_JPEGLI {
200 if ba_dist <= dist && ba_dist > lower.0 {
201 lower = (ba_dist, jpegli_q);
202 }
203 if ba_dist >= dist && ba_dist < upper.0 {
204 upper = (ba_dist, jpegli_q);
205 }
206 }
207
208 if (lower.0 - upper.0).abs() < 0.001 {
209 return lower.1;
210 }
211
212 let t = (dist - lower.0) / (upper.0 - lower.0);
213 lower.1 + t * (upper.1 - lower.1)
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218#[non_exhaustive]
219pub enum ColorMode {
220 YCbCr { subsampling: ChromaSubsampling },
222
223 Xyb { subsampling: XybSubsampling },
226
227 Grayscale,
229}
230
231impl Default for ColorMode {
232 fn default() -> Self {
233 ColorMode::YCbCr {
234 subsampling: ChromaSubsampling::None, }
236 }
237}
238
239#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241#[non_exhaustive]
242pub enum ChromaSubsampling {
243 None,
245 HalfHorizontal,
247 Quarter,
249 HalfVertical,
251}
252
253impl ChromaSubsampling {
254 #[must_use]
256 pub const fn h_factor(&self) -> u8 {
257 match self {
258 ChromaSubsampling::None | ChromaSubsampling::HalfVertical => 1,
259 ChromaSubsampling::HalfHorizontal | ChromaSubsampling::Quarter => 2,
260 }
261 }
262
263 #[must_use]
265 pub const fn v_factor(&self) -> u8 {
266 match self {
267 ChromaSubsampling::None | ChromaSubsampling::HalfHorizontal => 1,
268 ChromaSubsampling::HalfVertical | ChromaSubsampling::Quarter => 2,
269 }
270 }
271
272 #[must_use]
277 pub const fn h_samp_factor_luma(self) -> u8 {
278 self.h_factor()
279 }
280
281 #[must_use]
286 pub const fn v_samp_factor_luma(self) -> u8 {
287 self.v_factor()
288 }
289
290 #[must_use]
295 pub const fn mcu_size(self) -> usize {
296 match self {
297 ChromaSubsampling::None => 8,
298 ChromaSubsampling::Quarter
299 | ChromaSubsampling::HalfHorizontal
300 | ChromaSubsampling::HalfVertical => 16,
301 }
302 }
303}
304
305#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
310#[non_exhaustive]
311pub enum XybSubsampling {
312 Full,
314 #[default]
316 BQuarter,
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
323#[non_exhaustive]
324pub enum DownsamplingMethod {
325 #[default]
327 Box,
328 GammaAware,
330 GammaAwareIterative,
332}
333
334impl DownsamplingMethod {
335 #[must_use]
337 pub const fn uses_gamma_aware(self) -> bool {
338 matches!(self, Self::GammaAware | Self::GammaAwareIterative)
339 }
340}
341
342#[derive(Clone, Copy, Debug, PartialEq, Eq)]
349#[non_exhaustive]
350pub enum PixelLayout {
351 Rgb8Srgb,
354 Bgr8Srgb,
356 Rgbx8Srgb,
358 Bgrx8Srgb,
360 Gray8Srgb,
362
363 Rgb16Linear,
366 Rgbx16Linear,
368 Gray16Linear,
370
371 RgbF32Linear,
374 RgbxF32Linear,
376 GrayF32Linear,
378
379 YCbCr8,
382 YCbCrF32,
384}
385
386impl PixelLayout {
387 #[must_use]
389 pub const fn bytes_per_pixel(&self) -> usize {
390 match self {
391 Self::Gray8Srgb => 1,
392 Self::Gray16Linear => 2,
393 Self::Rgb8Srgb | Self::Bgr8Srgb | Self::YCbCr8 => 3,
394 Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::GrayF32Linear => 4,
395 Self::Rgb16Linear => 6,
396 Self::Rgbx16Linear => 8,
397 Self::RgbF32Linear | Self::YCbCrF32 => 12,
398 Self::RgbxF32Linear => 16,
399 }
400 }
401
402 #[must_use]
404 pub const fn channels(&self) -> usize {
405 match self {
406 Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear => 1,
407 Self::Rgb8Srgb
408 | Self::Bgr8Srgb
409 | Self::Rgb16Linear
410 | Self::RgbF32Linear
411 | Self::YCbCr8
412 | Self::YCbCrF32 => 3,
413 Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::Rgbx16Linear | Self::RgbxF32Linear => 4,
414 }
415 }
416
417 #[must_use]
419 pub const fn is_grayscale(&self) -> bool {
420 matches!(
421 self,
422 Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear
423 )
424 }
425
426 #[must_use]
428 pub const fn is_ycbcr(&self) -> bool {
429 matches!(self, Self::YCbCr8 | Self::YCbCrF32)
430 }
431
432 #[must_use]
434 pub const fn is_bgr(&self) -> bool {
435 matches!(self, Self::Bgr8Srgb | Self::Bgrx8Srgb)
436 }
437
438 #[must_use]
440 pub const fn is_float(&self) -> bool {
441 matches!(
442 self,
443 Self::RgbF32Linear | Self::RgbxF32Linear | Self::GrayF32Linear | Self::YCbCrF32
444 )
445 }
446
447 #[must_use]
449 pub const fn is_16bit(&self) -> bool {
450 matches!(
451 self,
452 Self::Rgb16Linear | Self::Rgbx16Linear | Self::Gray16Linear
453 )
454 }
455}
456
457#[derive(Clone, Copy, Debug)]
461pub struct YCbCrPlanes<'a> {
462 pub y: &'a [f32],
463 pub y_stride: usize,
464 pub cb: &'a [f32],
465 pub cb_stride: usize,
466 pub cr: &'a [f32],
467 pub cr_stride: usize,
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_quality_default() {
476 let q = Quality::default();
477 assert!(matches!(q, Quality::ApproxJpegli(90.0)));
478 }
479
480 #[test]
481 fn test_quality_from() {
482 let q: Quality = 85.0.into();
483 assert!(matches!(q, Quality::ApproxJpegli(85.0)));
484
485 let q: Quality = 75u8.into();
486 assert!(matches!(q, Quality::ApproxJpegli(75.0)));
487 }
488
489 #[test]
490 fn test_pixel_layout_bytes() {
491 assert_eq!(PixelLayout::Rgb8Srgb.bytes_per_pixel(), 3);
492 assert_eq!(PixelLayout::Rgbx8Srgb.bytes_per_pixel(), 4);
493 assert_eq!(PixelLayout::RgbF32Linear.bytes_per_pixel(), 12);
494 assert_eq!(PixelLayout::Gray8Srgb.bytes_per_pixel(), 1);
495 }
496
497 #[test]
498 fn test_chroma_subsampling_factors() {
499 assert_eq!(ChromaSubsampling::None.h_factor(), 1);
500 assert_eq!(ChromaSubsampling::None.v_factor(), 1);
501 assert_eq!(ChromaSubsampling::Quarter.h_factor(), 2);
502 assert_eq!(ChromaSubsampling::Quarter.v_factor(), 2);
503 assert_eq!(ChromaSubsampling::HalfHorizontal.h_factor(), 2);
504 assert_eq!(ChromaSubsampling::HalfHorizontal.v_factor(), 1);
505 }
506}
507
508#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
540#[non_exhaustive]
541#[cfg(feature = "parallel")]
542pub enum ParallelEncoding {
543 #[default]
549 Auto,
550}