1use crate::consts::{
10 quality_to_distance, BASE_QUANT_MATRIX_STD, BASE_QUANT_MATRIX_XYB, BASE_QUANT_MATRIX_YCBCR,
11 DCT_BLOCK_SIZE, GLOBAL_SCALE_XYB, GLOBAL_SCALE_YCBCR,
12};
13use crate::types::ColorSpace;
14
15pub use crate::types::QuantTable;
17
18pub const FREQUENCY_EXPONENT: [f32; DCT_BLOCK_SIZE] = [
23 1.00, 0.51, 0.67, 0.74, 1.00, 1.00, 1.00, 1.00, 0.51, 0.66, 0.69, 0.87, 1.00, 1.00, 1.00, 1.00,
24 0.67, 0.69, 0.84, 0.83, 0.96, 1.00, 1.00, 1.00, 0.74, 0.87, 0.83, 1.00, 1.00, 0.91, 0.91, 1.00,
25 1.00, 1.00, 0.96, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 0.91, 1.00, 1.00, 1.00, 1.00,
26 1.00, 1.00, 1.00, 0.91, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00,
27];
28
29pub const DIST_THRESHOLD: f32 = 1.5;
31
32const DIST_HQ: f32 = 1.0;
34const DIST_LQ: f32 = 3.0;
35
36#[rustfmt::skip]
40pub const ZERO_BIAS_MUL_YCBCR_LQ: [f32; 192] = [
41 0.0000, 0.0568, 0.3880, 0.6190, 0.6190, 0.4490, 0.4490, 0.6187,
43 0.0568, 0.5829, 0.6189, 0.6190, 0.6190, 0.7190, 0.6190, 0.6189,
44 0.3880, 0.6189, 0.6190, 0.6190, 0.6190, 0.6190, 0.6187, 0.6100,
45 0.6190, 0.6190, 0.6190, 0.6190, 0.5890, 0.3839, 0.7160, 0.6190,
46 0.6190, 0.6190, 0.6190, 0.5890, 0.6190, 0.3880, 0.5860, 0.4790,
47 0.4490, 0.7190, 0.6190, 0.3839, 0.3880, 0.6190, 0.6190, 0.6190,
48 0.4490, 0.6190, 0.6187, 0.7160, 0.5860, 0.6190, 0.6204, 0.6190,
49 0.6187, 0.6189, 0.6100, 0.6190, 0.4790, 0.6190, 0.6190, 0.3480,
50 0.0000, 1.1640, 0.9373, 1.1319, 0.8016, 0.9136, 1.1530, 0.9430,
52 1.1640, 0.9188, 0.9160, 1.1980, 1.1830, 0.9758, 0.9430, 0.9430,
53 0.9373, 0.9160, 0.8430, 1.1720, 0.7083, 0.9430, 0.9430, 0.9430,
54 1.1319, 1.1980, 1.1720, 1.1490, 0.8547, 0.9430, 0.9430, 0.9430,
55 0.8016, 1.1830, 0.7083, 0.8547, 0.9430, 0.9430, 0.9430, 0.9430,
56 0.9136, 0.9758, 0.9430, 0.9430, 0.9430, 0.9430, 0.9430, 0.9430,
57 1.1530, 0.9430, 0.9430, 0.9430, 0.9430, 0.9430, 0.9430, 0.9480,
58 0.9430, 0.9430, 0.9430, 0.9430, 0.9430, 0.9430, 0.9480, 0.9430,
59 0.0000, 1.3190, 0.4308, 0.4460, 0.0661, 0.0660, 0.2660, 0.2960,
61 1.3190, 0.3280, 0.3093, 0.0750, 0.0505, 0.1594, 0.3060, 0.2113,
62 0.4308, 0.3093, 0.3060, 0.1182, 0.0500, 0.3060, 0.3915, 0.2426,
63 0.4460, 0.0750, 0.1182, 0.0512, 0.0500, 0.2130, 0.3930, 0.1590,
64 0.0661, 0.0505, 0.0500, 0.0500, 0.3055, 0.3360, 0.5148, 0.5403,
65 0.0660, 0.1594, 0.3060, 0.2130, 0.3360, 0.5060, 0.5874, 0.3060,
66 0.2660, 0.3060, 0.3915, 0.3930, 0.5148, 0.5874, 0.3060, 0.3060,
67 0.2960, 0.2113, 0.2426, 0.1590, 0.5403, 0.3060, 0.3060, 0.3060,
68];
69
70#[rustfmt::skip]
74pub const ZERO_BIAS_MUL_YCBCR_HQ: [f32; 192] = [
75 0.0000, 0.0044, 0.2521, 0.6547, 0.8161, 0.6130, 0.8841, 0.8155,
77 0.0044, 0.6831, 0.6553, 0.6295, 0.7848, 0.7843, 0.8474, 0.7836,
78 0.2521, 0.6553, 0.7834, 0.7829, 0.8161, 0.8072, 0.7743, 0.9242,
79 0.6547, 0.6295, 0.7829, 0.8654, 0.7829, 0.6986, 0.7818, 0.7726,
80 0.8161, 0.7848, 0.8161, 0.7829, 0.7471, 0.7827, 0.7843, 0.7653,
81 0.6130, 0.7843, 0.8072, 0.6986, 0.7827, 0.7848, 0.9508, 0.7653,
82 0.8841, 0.8474, 0.7743, 0.7818, 0.7843, 0.9508, 0.7839, 0.8437,
83 0.8155, 0.7836, 0.9242, 0.7726, 0.7653, 0.7653, 0.8437, 0.7819,
84 0.0000, 1.0816, 1.0556, 1.2876, 1.1554, 1.1567, 1.8851, 0.5488,
86 1.0816, 1.1537, 1.1850, 1.0712, 1.1671, 2.0719, 1.0544, 1.4764,
87 1.0556, 1.1850, 1.2870, 1.1981, 1.8181, 1.2618, 1.0564, 1.1191,
88 1.2876, 1.0712, 1.1981, 1.4753, 2.0609, 1.0564, 1.2645, 1.0564,
89 1.1554, 1.1671, 1.8181, 2.0609, 0.7324, 1.1163, 0.8464, 1.0564,
90 1.1567, 2.0719, 1.2618, 1.0564, 1.1163, 1.0040, 1.0564, 1.0564,
91 1.8851, 1.0544, 1.0564, 1.2645, 0.8464, 1.0564, 1.0564, 1.0564,
92 0.5488, 1.4764, 1.1191, 1.0564, 1.0564, 1.0564, 1.0564, 1.0564,
93 0.0000, 0.5392, 0.6659, 0.8968, 0.6829, 0.6328, 0.5802, 0.4836,
95 0.5392, 0.6746, 0.6760, 0.6102, 0.6015, 0.6958, 0.7327, 0.4897,
96 0.6659, 0.6760, 0.6957, 0.6543, 0.4396, 0.6330, 0.7081, 0.2583,
97 0.8968, 0.6102, 0.6543, 0.5913, 0.6457, 0.5828, 0.5139, 0.3565,
98 0.6829, 0.6015, 0.4396, 0.6457, 0.5633, 0.4263, 0.6371, 0.5949,
99 0.6328, 0.6958, 0.6330, 0.5828, 0.4263, 0.2847, 0.2909, 0.6629,
100 0.5802, 0.7327, 0.7081, 0.5139, 0.6371, 0.2909, 0.6644, 0.6644,
101 0.4836, 0.4897, 0.2583, 0.3565, 0.5949, 0.6629, 0.6644, 0.6644,
102];
103
104pub const ZERO_BIAS_OFFSET_YCBCR_DC: [f32; 3] = [0.0, 0.0, 0.0];
107
108pub const ZERO_BIAS_OFFSET_YCBCR_AC: [f32; 3] = [0.59082, 0.58146, 0.57988];
111
112#[derive(Debug, Clone)]
117pub struct ZeroBiasParams {
118 pub mul: [f32; DCT_BLOCK_SIZE],
120 pub offset: [f32; DCT_BLOCK_SIZE],
122}
123
124impl Default for ZeroBiasParams {
125 fn default() -> Self {
126 Self {
130 mul: [0.0; DCT_BLOCK_SIZE],
131 offset: [0.0; DCT_BLOCK_SIZE],
132 }
133 }
134}
135
136impl ZeroBiasParams {
137 #[must_use]
148 pub fn for_ycbcr(distance: f32, component: usize) -> Self {
149 let c = component.min(2);
150
151 let mix_lq = ((distance - DIST_HQ) / (DIST_LQ - DIST_HQ)).clamp(0.0, 1.0);
153 let mix_hq = 1.0 - mix_lq;
154
155 let mut mul = [0.0f32; DCT_BLOCK_SIZE];
156 let mut offset = [0.0f32; DCT_BLOCK_SIZE];
157
158 for k in 0..DCT_BLOCK_SIZE {
159 let lq = ZERO_BIAS_MUL_YCBCR_LQ[c * DCT_BLOCK_SIZE + k];
160 let hq = ZERO_BIAS_MUL_YCBCR_HQ[c * DCT_BLOCK_SIZE + k];
161 mul[k] = mix_lq * lq + mix_hq * hq;
162
163 offset[k] = if k == 0 {
164 ZERO_BIAS_OFFSET_YCBCR_DC[c]
165 } else {
166 ZERO_BIAS_OFFSET_YCBCR_AC[c]
167 };
168 }
169
170 Self { mul, offset }
171 }
172
173 #[must_use]
177 pub fn for_ycbcr_simple(component: usize) -> Self {
178 let c = component.min(2);
179
180 let mul = [0.0f32; DCT_BLOCK_SIZE]; let mut offset = [0.0f32; DCT_BLOCK_SIZE];
183 for k in 0..DCT_BLOCK_SIZE {
184 offset[k] = if k == 0 {
185 ZERO_BIAS_OFFSET_YCBCR_DC[c]
186 } else {
187 ZERO_BIAS_OFFSET_YCBCR_AC[c]
188 };
189 }
190
191 Self { mul, offset }
192 }
193
194 #[inline]
198 #[must_use]
199 pub fn apply(&self, coeff: f32, k: usize, quant: f32) -> f32 {
200 let threshold = (self.mul[k] + self.offset[k]) * quant;
201 if coeff.abs() < threshold {
202 0.0
203 } else {
204 coeff
205 }
206 }
207}
208
209#[inline]
222#[must_use]
223pub fn distance_to_scale(distance: f32, freq_idx: usize) -> f32 {
224 if distance < DIST_THRESHOLD {
225 return distance;
226 }
227 let exp = FREQUENCY_EXPONENT[freq_idx];
228 let mul = DIST_THRESHOLD.powf(1.0 - exp);
229 (0.5 * distance).max(mul * distance.powf(exp))
230}
231
232#[inline]
234#[must_use]
235pub fn scale_to_distance(scale: f32, freq_idx: usize) -> f32 {
236 if scale < DIST_THRESHOLD {
237 return scale;
238 }
239 let exp = 1.0 / FREQUENCY_EXPONENT[freq_idx];
240 let mul = DIST_THRESHOLD.powf(1.0 - exp);
241 (2.0 * scale).min(mul * scale.powf(exp))
242}
243
244#[must_use]
254pub fn quant_vals_to_distance(
255 y_quant: &QuantTable,
256 cb_quant: &QuantTable,
257 cr_quant: &QuantTable,
258) -> f32 {
259 use crate::consts::{BASE_QUANT_MATRIX_YCBCR, GLOBAL_SCALE_YCBCR};
260
261 const DIST_MAX: f32 = 10000.0;
262 const QUANT_MAX: u16 = 255; let global_scale = GLOBAL_SCALE_YCBCR;
265
266 let mut dist_min = 0.0f32;
267 let mut dist_max = DIST_MAX;
268
269 let quant_tables = [y_quant, cb_quant, cr_quant];
271
272 for (c, quant) in quant_tables.iter().enumerate() {
273 let base_idx = c * DCT_BLOCK_SIZE;
274 let base_qm = &BASE_QUANT_MATRIX_YCBCR[base_idx..base_idx + DCT_BLOCK_SIZE];
275
276 for k in 0..DCT_BLOCK_SIZE {
277 let mut dmin = 0.0f32;
278 let mut dmax = DIST_MAX;
279 let invq = 1.0 / base_qm[k] / global_scale;
280 let qval = quant.values[k];
281
282 if qval > 1 {
283 let scale_min = (qval as f32 - 0.5) * invq;
284 dmin = scale_to_distance(scale_min, k);
285 }
286 if qval < QUANT_MAX {
287 let scale_max = (qval as f32 + 0.5) * invq;
288 dmax = scale_to_distance(scale_max, k);
289 }
290
291 if dmin <= dist_max {
292 dist_min = dist_min.max(dmin);
293 }
294 if dmax >= dist_min {
295 dist_max = dist_max.min(dmax);
296 }
297 }
298 }
299
300 if dist_min == 0.0 {
302 dist_max
303 } else if dist_max >= DIST_MAX {
304 dist_min
305 } else {
306 0.5 * (dist_min + dist_max)
307 }
308}
309
310pub const STD_LUMINANCE_QUANT: [u16; DCT_BLOCK_SIZE] = [
313 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56,
314 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113,
315 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99,
316];
317
318pub const STD_CHROMINANCE_QUANT: [u16; DCT_BLOCK_SIZE] = [
321 17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99,
322 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
323 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
324];
325
326#[derive(Debug, Clone, Copy, PartialEq)]
328#[non_exhaustive]
329pub enum Quality {
330 Traditional(f32),
332 Distance(f32),
335}
336
337impl Default for Quality {
338 fn default() -> Self {
339 Self::Traditional(90.0)
340 }
341}
342
343impl Quality {
344 #[must_use]
346 pub fn from_quality(q: f32) -> Self {
347 Self::Traditional(q.clamp(1.0, 100.0))
348 }
349
350 #[must_use]
352 pub fn from_distance(d: f32) -> Self {
353 Self::Distance(d.max(0.0))
354 }
355
356 #[must_use]
358 pub fn to_distance(self) -> f32 {
359 match self {
360 Self::Traditional(q) => quality_to_distance(q as i32),
361 Self::Distance(d) => d,
362 }
363 }
364
365 #[must_use]
367 pub fn to_quality(self) -> f32 {
368 match self {
369 Self::Traditional(q) => q,
370 Self::Distance(d) => distance_to_quality(d),
371 }
372 }
373
374 #[must_use]
376 pub fn to_linear(self) -> f32 {
377 let d = self.to_distance();
378 if d <= 0.1 {
380 1.0
381 } else {
382 (0.1 / d).min(1.0)
383 }
384 }
385}
386
387fn distance_to_quality(distance: f32) -> f32 {
389 if distance <= 0.0 {
391 100.0
392 } else if distance >= 15.0 {
393 1.0
394 } else {
395 100.0 - (distance * 6.6).min(99.0)
397 }
398}
399
400#[must_use]
409pub fn generate_quant_table(
410 quality: Quality,
411 component: usize,
412 color_space: ColorSpace,
413 use_xyb: bool,
414 is_420: bool,
415) -> QuantTable {
416 let distance = quality.to_distance();
417
418 if use_xyb {
419 generate_xyb_quant_table(distance, component)
420 } else {
421 generate_standard_quant_table(distance, component, color_space, is_420)
422 }
423}
424
425fn generate_xyb_quant_table(distance: f32, component: usize) -> QuantTable {
430 let mut values = [0u16; DCT_BLOCK_SIZE];
431
432 let base_idx = component.min(2) * DCT_BLOCK_SIZE;
434 let base = &BASE_QUANT_MATRIX_XYB[base_idx..base_idx + DCT_BLOCK_SIZE];
435
436 for (i, &base_val) in base.iter().enumerate() {
437 let scale = distance_to_scale(distance, i) * GLOBAL_SCALE_XYB;
439 let q = (base_val * scale).round();
440 values[i] = (q as u16).clamp(1, 255);
442 }
443
444 QuantTable {
445 values,
446 precision: 0, }
448}
449
450fn generate_standard_quant_table(
455 distance: f32,
456 component: usize,
457 color_space: ColorSpace,
458 is_420: bool,
459) -> QuantTable {
460 use crate::consts::{GLOBAL_SCALE_420, K420_RESCALE};
461
462 let mut values = [0u16; DCT_BLOCK_SIZE];
463
464 let (base, mut global_scale) = if color_space == ColorSpace::YCbCr {
466 let base_idx = component.min(2) * DCT_BLOCK_SIZE;
467 (
468 &BASE_QUANT_MATRIX_YCBCR[base_idx..base_idx + DCT_BLOCK_SIZE],
469 GLOBAL_SCALE_YCBCR,
470 )
471 } else {
472 let base_idx = if component == 0 { 0 } else { DCT_BLOCK_SIZE };
474 (
475 &BASE_QUANT_MATRIX_STD[base_idx..base_idx + DCT_BLOCK_SIZE],
476 1.0,
477 )
478 };
479
480 if is_420 && color_space == ColorSpace::YCbCr {
483 global_scale *= GLOBAL_SCALE_420;
484 }
485
486 let is_chroma_420 = is_420 && color_space == ColorSpace::YCbCr && component > 0;
488
489 for (i, &base_val) in base.iter().enumerate() {
490 let mut scale = distance_to_scale(distance, i) * global_scale;
492
493 if is_chroma_420 {
496 scale *= K420_RESCALE[i];
497 }
498
499 let q = (base_val * scale).round();
500 values[i] = (q as u16).clamp(1, 255);
501 }
502
503 QuantTable {
504 values,
505 precision: 0,
506 }
507}
508
509#[must_use]
515pub fn generate_standard_jpeg_table(quality: f32, is_chrominance: bool) -> QuantTable {
516 let base_table = if is_chrominance {
517 &STD_CHROMINANCE_QUANT
518 } else {
519 &STD_LUMINANCE_QUANT
520 };
521
522 let quality = quality.clamp(1.0, 100.0);
524 let scale = if quality < 50.0 {
525 5000.0 / quality
526 } else {
527 200.0 - quality * 2.0
528 };
529
530 let mut values = [0u16; DCT_BLOCK_SIZE];
531 for (i, &base) in base_table.iter().enumerate() {
532 let q = ((base as f32 * scale + 50.0) / 100.0).round();
533 values[i] = (q as u16).clamp(1, 255);
534 }
535
536 QuantTable {
537 values,
538 precision: 0,
539 }
540}
541
542#[inline]
544#[must_use]
545pub fn quantize(coeff: f32, quant: u16) -> i16 {
546 let q = quant as f32;
547 (coeff / q).round() as i16
548}
549
550#[inline]
552#[must_use]
553pub fn dequantize(quantized: i16, quant: u16) -> f32 {
554 quantized as f32 * quant as f32
555}
556
557pub fn quantize_block(
559 coeffs: &[f32; DCT_BLOCK_SIZE],
560 quant: &[u16; DCT_BLOCK_SIZE],
561) -> [i16; DCT_BLOCK_SIZE] {
562 let mut result = [0i16; DCT_BLOCK_SIZE];
563 for i in 0..DCT_BLOCK_SIZE {
564 result[i] = quantize(coeffs[i], quant[i]);
565 }
566 result
567}
568
569#[cfg(debug_assertions)]
581#[allow(dead_code)] static ZERO_BIAS_DEBUG: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
583#[cfg(debug_assertions)]
584#[allow(dead_code)] static ZERO_BIAS_ZEROS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
586
587pub fn quantize_block_with_zero_bias(
588 coeffs: &[f32; DCT_BLOCK_SIZE],
589 quant: &[u16; DCT_BLOCK_SIZE],
590 zero_bias: &ZeroBiasParams,
591 aq_strength: f32,
592) -> [i16; DCT_BLOCK_SIZE] {
593 let mut result = [0i16; DCT_BLOCK_SIZE];
594
595 for k in 0..DCT_BLOCK_SIZE {
596 let q = quant[k] as f32;
597 let qval = coeffs[k] / q;
598
599 let threshold = zero_bias.offset[k] + zero_bias.mul[k] * aq_strength;
604
605 if qval.abs() >= threshold {
606 result[k] = qval.round() as i16;
607 }
608 }
610 result
611}
612
613pub fn quantize_block_compare(
615 coeffs: &[f32; DCT_BLOCK_SIZE],
616 quant: &[u16; DCT_BLOCK_SIZE],
617 zero_bias: &ZeroBiasParams,
618 aq_strength: f32,
619) -> ([i16; DCT_BLOCK_SIZE], usize) {
620 let mut result = [0i16; DCT_BLOCK_SIZE];
621 let mut zeros_from_bias = 0usize;
622 for k in 0..DCT_BLOCK_SIZE {
623 let q = quant[k] as f32;
624 let qval = coeffs[k] / q;
625 let simple_result = qval.round() as i16;
626 let threshold = zero_bias.offset[k] + zero_bias.mul[k] * aq_strength;
627
628 if qval.abs() >= threshold {
629 result[k] = simple_result;
630 } else {
631 if simple_result != 0 {
633 zeros_from_bias += 1;
634 }
635 }
636 }
637 (result, zeros_from_bias)
638}
639
640pub fn dequantize_block(
642 quantized: &[i16; DCT_BLOCK_SIZE],
643 quant: &[u16; DCT_BLOCK_SIZE],
644) -> [f32; DCT_BLOCK_SIZE] {
645 let mut result = [0.0f32; DCT_BLOCK_SIZE];
646 for i in 0..DCT_BLOCK_SIZE {
647 result[i] = dequantize(quantized[i], quant[i]);
648 }
649 result
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn test_quality_conversion() {
658 let q = Quality::from_quality(90.0);
660 let d = q.to_distance();
661 assert!(d > 0.0 && d < 5.0);
662
663 let q2 = Quality::from_distance(1.0);
665 let d2 = q2.to_distance();
666 assert!((d2 - 1.0).abs() < 0.001);
667 }
668
669 #[test]
670 fn test_standard_table_generation() {
671 let table_q50 = generate_standard_jpeg_table(50.0, false);
672 let table_q90 = generate_standard_jpeg_table(90.0, false);
673
674 let sum_q50: u32 = table_q50.values.iter().map(|&v| v as u32).sum();
676 let sum_q90: u32 = table_q90.values.iter().map(|&v| v as u32).sum();
677 assert!(sum_q90 < sum_q50);
678 }
679
680 #[test]
681 fn test_quantize_dequantize() {
682 let coeff = 123.456f32;
683 let quant = 16;
684
685 let quantized = quantize(coeff, quant);
686 let recovered = dequantize(quantized, quant);
687
688 assert!((recovered - coeff).abs() < quant as f32);
690 }
691
692 #[test]
693 fn test_quant_values_in_range() {
694 for q in [10.0, 50.0, 90.0, 100.0] {
696 let table = generate_standard_jpeg_table(q, false);
697 for &v in &table.values {
698 assert!(v >= 1 && v <= 255);
699 }
700 }
701 }
702
703 #[test]
704 fn test_xyb_table_generation() {
705 let table =
706 generate_quant_table(Quality::from_distance(1.0), 0, ColorSpace::Xyb, true, false);
707
708 for &v in &table.values {
710 assert!(v >= 1 && v <= 255);
711 }
712 }
713
714 #[test]
715 fn test_quant_table_comparison() {
716 println!("\n=== Quant Table Comparison (Y channel) ===");
717 println!(
718 "{:>5} {:>8} {:>10} {:>10} {:>8} {:>8}",
719 "Q", "dist", "YCbCr_sum", "XYB_sum", "YCbCr[0]", "XYB[0]"
720 );
721
722 for q in [10, 20, 30, 40, 50, 60, 70, 80, 90] {
723 let quality = Quality::from_quality(q as f32);
724 let distance = quality.to_distance();
725
726 let ycbcr = generate_quant_table(quality, 0, ColorSpace::YCbCr, false, false);
727 let xyb = generate_quant_table(quality, 0, ColorSpace::Xyb, true, false);
728
729 let ycbcr_sum: u32 = ycbcr.values.iter().map(|&x| x as u32).sum();
730 let xyb_sum: u32 = xyb.values.iter().map(|&x| x as u32).sum();
731
732 println!(
733 "{:>5} {:>8.2} {:>10} {:>10} {:>8} {:>8}",
734 q, distance, ycbcr_sum, xyb_sum, ycbcr.values[0], xyb.values[0]
735 );
736 }
737 }
738
739 #[test]
740 fn test_distance_to_scale_linear_region() {
741 for distance in [0.1, 0.5, 1.0, 1.4] {
743 for freq_idx in 0..64 {
744 let scale = distance_to_scale(distance, freq_idx);
745 assert!(
746 (scale - distance).abs() < 1e-6,
747 "Linear region failed: d={}, k={}, scale={}",
748 distance,
749 freq_idx,
750 scale
751 );
752 }
753 }
754 }
755
756 #[test]
757 fn test_distance_to_scale_nonlinear_region() {
758 let distance = 3.0;
760
761 let scale_dc = distance_to_scale(distance, 0);
763 assert!((scale_dc - distance).abs() < 0.1, "DC scale: {}", scale_dc);
765
766 let scale_1 = distance_to_scale(distance, 1);
768 assert!(scale_1 > 1.5 && scale_1 < 3.0, "Index 1 scale: {}", scale_1);
771 }
772
773 #[test]
774 fn test_distance_to_scale_roundtrip() {
775 for distance in [0.5, 1.0, 2.0, 3.0, 5.0, 10.0] {
777 for freq_idx in 0..64 {
778 let scale = distance_to_scale(distance, freq_idx);
779 let recovered = scale_to_distance(scale, freq_idx);
780 assert!(
781 (recovered - distance).abs() < 0.01,
782 "Roundtrip failed: d={}, k={}, scale={}, recovered={}",
783 distance,
784 freq_idx,
785 scale,
786 recovered
787 );
788 }
789 }
790 }
791
792 #[test]
793 fn test_frequency_exponent_values() {
794 assert_eq!(FREQUENCY_EXPONENT.len(), 64);
796
797 assert!((FREQUENCY_EXPONENT[0] - 1.0).abs() < 1e-6);
799
800 assert!(FREQUENCY_EXPONENT[1] < 1.0); assert!(FREQUENCY_EXPONENT[8] < 1.0); assert!((FREQUENCY_EXPONENT[63] - 1.0).abs() < 1e-6);
806 assert!((FREQUENCY_EXPONENT[62] - 1.0).abs() < 1e-6);
807 }
808
809 #[test]
812 fn test_distance_to_scale_cpp_reference() {
813 let test_cases = [
816 (1.0_f32, 0_usize, 1.0_f32),
818 (1.0, 1, 1.0),
819 (1.0, 63, 1.0),
820 (2.0, 0, 2.0), (3.0, 0, 3.0), (5.0, 0, 5.0), ];
825
826 for (distance, freq_idx, expected) in test_cases {
827 let actual = distance_to_scale(distance, freq_idx);
828 assert!(
829 (actual - expected).abs() < 0.01,
830 "Mismatch: distance_to_scale({}, {}) = {}, expected {}",
831 distance,
832 freq_idx,
833 actual,
834 expected
835 );
836 }
837 }
838
839 #[test]
840 fn test_zero_bias_table_sizes() {
841 assert_eq!(ZERO_BIAS_MUL_YCBCR_LQ.len(), 192);
843 assert_eq!(ZERO_BIAS_MUL_YCBCR_HQ.len(), 192);
844 assert_eq!(ZERO_BIAS_OFFSET_YCBCR_DC.len(), 3);
845 assert_eq!(ZERO_BIAS_OFFSET_YCBCR_AC.len(), 3);
846 }
847
848 #[test]
849 fn test_zero_bias_dc_is_zero_in_tables() {
850 for c in 0..3 {
852 assert!(
853 ZERO_BIAS_MUL_YCBCR_LQ[c * 64].abs() < 1e-6,
854 "LQ DC mul for component {} should be 0, got {}",
855 c,
856 ZERO_BIAS_MUL_YCBCR_LQ[c * 64]
857 );
858 assert!(
859 ZERO_BIAS_MUL_YCBCR_HQ[c * 64].abs() < 1e-6,
860 "HQ DC mul for component {} should be 0, got {}",
861 c,
862 ZERO_BIAS_MUL_YCBCR_HQ[c * 64]
863 );
864 }
865 }
866
867 #[test]
868 fn test_zero_bias_params_default() {
869 let params = ZeroBiasParams::default();
870
871 for k in 0..64 {
873 assert!((params.mul[k]).abs() < 1e-6);
874 assert!((params.offset[k]).abs() < 1e-6);
875 }
876 }
877
878 #[test]
879 fn test_zero_bias_for_ycbcr_hq() {
880 let params = ZeroBiasParams::for_ycbcr(0.5, 0);
882
883 assert!((params.mul[1] - ZERO_BIAS_MUL_YCBCR_HQ[1]).abs() < 1e-5);
885 assert!((params.mul[10] - ZERO_BIAS_MUL_YCBCR_HQ[10]).abs() < 1e-5);
886
887 assert!((params.offset[0] - ZERO_BIAS_OFFSET_YCBCR_DC[0]).abs() < 1e-5);
889 assert!((params.offset[1] - ZERO_BIAS_OFFSET_YCBCR_AC[0]).abs() < 1e-5);
890 }
891
892 #[test]
893 fn test_zero_bias_for_ycbcr_lq() {
894 let params = ZeroBiasParams::for_ycbcr(5.0, 0);
896
897 assert!((params.mul[1] - ZERO_BIAS_MUL_YCBCR_LQ[1]).abs() < 1e-5);
899 assert!((params.mul[10] - ZERO_BIAS_MUL_YCBCR_LQ[10]).abs() < 1e-5);
900 }
901
902 #[test]
903 fn test_zero_bias_for_ycbcr_blend() {
904 let params = ZeroBiasParams::for_ycbcr(2.0, 0);
906
907 let hq_val = ZERO_BIAS_MUL_YCBCR_HQ[1];
909 let lq_val = ZERO_BIAS_MUL_YCBCR_LQ[1];
910 let expected = 0.5 * hq_val + 0.5 * lq_val;
911 assert!(
912 (params.mul[1] - expected).abs() < 1e-5,
913 "Expected blend {} (HQ={}, LQ={}), got {}",
914 expected,
915 hq_val,
916 lq_val,
917 params.mul[1]
918 );
919 }
920
921 #[test]
922 fn test_zero_bias_for_ycbcr_all_components() {
923 for c in 0..3 {
925 let params = ZeroBiasParams::for_ycbcr(1.5, c);
926
927 assert!((params.offset[0] - ZERO_BIAS_OFFSET_YCBCR_DC[c]).abs() < 1e-5);
929
930 assert!((params.offset[1] - ZERO_BIAS_OFFSET_YCBCR_AC[c]).abs() < 1e-5);
932 assert!((params.offset[63] - ZERO_BIAS_OFFSET_YCBCR_AC[c]).abs() < 1e-5);
933 }
934 }
935
936 #[test]
937 fn test_zero_bias_apply() {
938 let params = ZeroBiasParams::for_ycbcr(2.0, 0);
939 let quant = 16.0;
940
941 let threshold = (params.mul[1] + params.offset[1]) * quant;
943 let small_coeff = threshold * 0.5;
944 assert!((params.apply(small_coeff, 1, quant)).abs() < 1e-6);
945
946 let large_coeff = threshold * 2.0;
948 assert!((params.apply(large_coeff, 1, quant) - large_coeff).abs() < 1e-6);
949 }
950
951 #[test]
954 fn test_zero_bias_cpp_reference() {
955 let params = ZeroBiasParams::for_ycbcr(2.0, 0);
959 let expected_mul_1 = 0.5 * 0.0568 + 0.5 * 0.0044;
960 assert!(
961 (params.mul[1] - expected_mul_1).abs() < 1e-4,
962 "Y mul[1] at d=2.0: expected {}, got {}",
963 expected_mul_1,
964 params.mul[1]
965 );
966
967 assert!(
969 (params.offset[1] - 0.59082).abs() < 1e-4,
970 "Y offset[1]: expected 0.59082, got {}",
971 params.offset[1]
972 );
973 }
974}