jpegli/
quant.rs

1//! Quantization tables and quality settings.
2//!
3//! This module provides:
4//! - Standard JPEG quantization tables
5//! - jpegli's enhanced quantization matrices
6//! - Quality parameter handling (traditional and butteraugli distance)
7//! - Adaptive quantization support
8
9use 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
15// Re-export QuantTable from types
16pub use crate::types::QuantTable;
17
18/// Per-frequency scaling exponents for non-linear quality scaling.
19/// Low frequencies (top-left) use lower exponents for more aggressive scaling,
20/// while high frequencies (bottom-right) use 1.0 for linear scaling.
21/// From C++ jpegli quant.cc
22pub 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
29/// Distance threshold where non-linear scaling kicks in.
30pub const DIST_THRESHOLD: f32 = 1.5;
31
32/// Distance thresholds for zero-bias blending between HQ and LQ tables.
33const DIST_HQ: f32 = 1.0;
34const DIST_LQ: f32 = 3.0;
35
36/// Zero-bias multiplier table for YCbCr at low quality (distance >= 3.0).
37/// 3 components × 64 coefficients = 192 values.
38/// From C++ jpegli quant.cc kZeroBiasMulYCbCrLQ
39#[rustfmt::skip]
40pub const ZERO_BIAS_MUL_YCBCR_LQ: [f32; 192] = [
41    // c = 0 (Y)
42    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    // c = 1 (Cb)
51    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    // c = 2 (Cr)
60    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/// Zero-bias multiplier table for YCbCr at high quality (distance <= 1.0).
71/// 3 components × 64 coefficients = 192 values.
72/// From C++ jpegli quant.cc kZeroBiasMulYCbCrHQ
73#[rustfmt::skip]
74pub const ZERO_BIAS_MUL_YCBCR_HQ: [f32; 192] = [
75    // c = 0 (Y)
76    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    // c = 1 (Cb)
85    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    // c = 2 (Cr)
94    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
104/// Zero-bias offset for DC coefficients (per component).
105/// From C++ jpegli quant.cc kZeroBiasOffsetYCbCrDC
106pub const ZERO_BIAS_OFFSET_YCBCR_DC: [f32; 3] = [0.0, 0.0, 0.0];
107
108/// Zero-bias offset for AC coefficients (per component).
109/// From C++ jpegli quant.cc kZeroBiasOffsetYCbCrAC
110pub const ZERO_BIAS_OFFSET_YCBCR_AC: [f32; 3] = [0.59082, 0.58146, 0.57988];
111
112/// Zero-bias parameters for a single DCT block.
113///
114/// Zero-bias controls how coefficients are rounded toward zero during quantization.
115/// A higher multiplier means more aggressive zeroing of small coefficients.
116#[derive(Debug, Clone)]
117pub struct ZeroBiasParams {
118    /// Multiplier per coefficient (64 values)
119    pub mul: [f32; DCT_BLOCK_SIZE],
120    /// Offset per coefficient (64 values)
121    pub offset: [f32; DCT_BLOCK_SIZE],
122}
123
124impl Default for ZeroBiasParams {
125    fn default() -> Self {
126        // Default: all zeros (matches C++ when adaptive quantization is disabled)
127        // When AQ is off, C++ sets zero_bias_mul and zero_bias_offset to 0
128        // This means no coefficients are biased toward zero based on threshold
129        Self {
130            mul: [0.0; DCT_BLOCK_SIZE],
131            offset: [0.0; DCT_BLOCK_SIZE],
132        }
133    }
134}
135
136impl ZeroBiasParams {
137    /// Compute zero-bias parameters for YCbCr color space.
138    ///
139    /// Blends between HQ and LQ tables based on butteraugli distance.
140    /// - distance <= 1.0: Use HQ table
141    /// - distance >= 3.0: Use LQ table
142    /// - 1.0 < distance < 3.0: Linear blend
143    ///
144    /// # Arguments
145    /// * `distance` - Butteraugli distance (quality parameter)
146    /// * `component` - Component index (0=Y, 1=Cb, 2=Cr)
147    #[must_use]
148    pub fn for_ycbcr(distance: f32, component: usize) -> Self {
149        let c = component.min(2);
150
151        // Compute blend factor
152        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    /// Compute zero-bias parameters for non-adaptive quantization (simpler default).
174    ///
175    /// For YCbCr, applies only the offsets without the multiplier blending.
176    #[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]; // Not used in simple mode
181
182        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    /// Apply zero-bias to a coefficient before quantization.
195    ///
196    /// This adjusts the rounding behavior to favor zeroing small coefficients.
197    #[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/// Converts butteraugli distance to a per-frequency scale factor.
210///
211/// This implements jpegli's non-linear quality scaling. At low distances
212/// (high quality), scaling is linear. Above DIST_THRESHOLD, scaling becomes
213/// non-linear based on the frequency-dependent exponent.
214///
215/// # Arguments
216/// * `distance` - Butteraugli distance (quality parameter)
217/// * `freq_idx` - DCT frequency index (0-63, in zigzag order)
218///
219/// # Returns
220/// Scale factor for quantization
221#[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/// Inverse of distance_to_scale - converts scale back to distance.
233#[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/// Infers the butteraugli distance from quantization table values.
245///
246/// This matches C++ jpegli's `QuantValsToDistance` function.
247/// It finds the distance that would produce the given quant tables
248/// when using jpegli's quantization formula.
249///
250/// This is used to compute zero-bias parameters appropriate for the
251/// actual quant values, rather than the input distance (which may differ
252/// at extreme quality levels where values are clamped to 1).
253#[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; // baseline JPEG
263
264    let global_scale = GLOBAL_SCALE_YCBCR;
265
266    let mut dist_min = 0.0f32;
267    let mut dist_max = DIST_MAX;
268
269    // Process all three components
270    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    // Return the appropriate distance
301    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
310/// Standard JPEG luminance quantization table.
311/// From ITU-T T.81 (1992) K.1
312pub 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
318/// Standard JPEG chrominance quantization table.
319/// From ITU-T T.81 (1992) K.2
320pub 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/// Quality representation that can be either traditional (1-100) or butteraugli distance.
327#[derive(Debug, Clone, Copy, PartialEq)]
328#[non_exhaustive]
329pub enum Quality {
330    /// Traditional JPEG quality (1-100, where 100 is best)
331    Traditional(f32),
332    /// Butteraugli distance (0.0 = lossless, higher = more compression)
333    /// Typical values: 0.5 = very high quality, 1.0 = high, 2.0 = medium
334    Distance(f32),
335}
336
337impl Default for Quality {
338    fn default() -> Self {
339        Self::Traditional(90.0)
340    }
341}
342
343impl Quality {
344    /// Creates a quality setting from traditional JPEG quality (1-100).
345    #[must_use]
346    pub fn from_quality(q: f32) -> Self {
347        Self::Traditional(q.clamp(1.0, 100.0))
348    }
349
350    /// Creates a quality setting from butteraugli distance.
351    #[must_use]
352    pub fn from_distance(d: f32) -> Self {
353        Self::Distance(d.max(0.0))
354    }
355
356    /// Converts to butteraugli distance.
357    #[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    /// Converts to traditional quality (approximate).
366    #[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    /// Converts to linear quality (0.0-1.0 where 1.0 is best).
375    #[must_use]
376    pub fn to_linear(self) -> f32 {
377        let d = self.to_distance();
378        // Approximate inverse of linear_quality_to_distance
379        if d <= 0.1 {
380            1.0
381        } else {
382            (0.1 / d).min(1.0)
383        }
384    }
385}
386
387/// Converts butteraugli distance to approximate traditional quality.
388fn distance_to_quality(distance: f32) -> f32 {
389    // Approximate inverse of quality_to_distance
390    if distance <= 0.0 {
391        100.0
392    } else if distance >= 15.0 {
393        1.0
394    } else {
395        // This is a rough approximation
396        100.0 - (distance * 6.6).min(99.0)
397    }
398}
399
400/// Generates a quantization table for the given quality and component.
401///
402/// # Arguments
403/// * `quality` - Quality setting
404/// * `component` - Component index (0 = Y/luma, 1+ = chroma)
405/// * `color_space` - Color space being used
406/// * `use_xyb` - Whether to use XYB-optimized tables
407/// * `is_420` - Whether 4:2:0 chroma subsampling is used (applies quality compensation)
408#[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
425/// Generates a quantization table using jpegli's XYB-optimized matrices.
426///
427/// Uses per-frequency non-linear scaling via `distance_to_scale()` for
428/// better quality at the same file size compared to linear scaling.
429fn generate_xyb_quant_table(distance: f32, component: usize) -> QuantTable {
430    let mut values = [0u16; DCT_BLOCK_SIZE];
431
432    // Select the appropriate base matrix row
433    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        // Apply per-frequency non-linear scaling
438        let scale = distance_to_scale(distance, i) * GLOBAL_SCALE_XYB;
439        let q = (base_val * scale).round();
440        // Clamp to valid quantization values (1-255 for baseline)
441        values[i] = (q as u16).clamp(1, 255);
442    }
443
444    QuantTable {
445        values,
446        precision: 0, // 8-bit for baseline
447    }
448}
449
450/// Generates a quantization table using standard or YCbCr matrices.
451///
452/// Uses per-frequency non-linear scaling via `distance_to_scale()` for
453/// better quality at the same file size compared to linear scaling.
454fn 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    // Choose base matrix based on color space
465    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        // Use standard JPEG tables
473        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    // Apply 4:2:0 global quality compensation (like C++ jpegli)
481    // This makes quant tables 22% larger for ALL components
482    if is_420 && color_space == ColorSpace::YCbCr {
483        global_scale *= GLOBAL_SCALE_420;
484    }
485
486    // Check if we need per-frequency chroma rescale for 4:2:0
487    let is_chroma_420 = is_420 && color_space == ColorSpace::YCbCr && component > 0;
488
489    for (i, &base_val) in base.iter().enumerate() {
490        // Apply per-frequency non-linear scaling
491        let mut scale = distance_to_scale(distance, i) * global_scale;
492
493        // Apply additional per-frequency rescale for chroma in 4:2:0 mode
494        // This reduces chroma quantization to preserve color fidelity
495        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/// Generates a standard JPEG quantization table scaled by quality factor.
510///
511/// # Arguments
512/// * `quality` - Quality 1-100 (100 = best)
513/// * `is_chrominance` - True for Cb/Cr tables, false for Y
514#[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    // Standard JPEG quality scaling
523    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/// Quantizes a DCT coefficient using the given quantization value.
543#[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/// Dequantizes a coefficient.
551#[inline]
552#[must_use]
553pub fn dequantize(quantized: i16, quant: u16) -> f32 {
554    quantized as f32 * quant as f32
555}
556
557/// Quantizes a block of DCT coefficients.
558pub 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/// Quantizes a block of DCT coefficients with zero-biasing.
570///
571/// This matches C++ jpegli's quantization behavior where small coefficients
572/// are biased toward zero to improve compression.
573///
574/// The threshold is: `offset + mul * aq_strength`
575/// - If `|coeff/quant| >= threshold`: round normally
576/// - Else: set to 0
577///
578/// For non-adaptive quantization, use aq_strength = 0.0
579/// Counter for debugging zero-bias effectiveness
580#[cfg(debug_assertions)]
581#[allow(dead_code)] // Used for manual debugging - print stats at end of encoding
582static ZERO_BIAS_DEBUG: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
583#[cfg(debug_assertions)]
584#[allow(dead_code)] // Used for manual debugging - print stats at end of encoding
585static 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        // Note on scaling: C++ DCT uses 1/64 scaling total, Rust uses 1/8.
600        // C++ compensates by using quant_mul = 8/quant in quantization.
601        // Net effect: both produce qval = dct_normalized / quant for storage.
602        // For threshold comparison, both compare |qval| vs threshold directly.
603        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        // else result[k] stays 0
609    }
610    result
611}
612
613/// Alternative: compare with simple quantization
614pub 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            // Would have been non-zero without zero-biasing
632            if simple_result != 0 {
633                zeros_from_bias += 1;
634            }
635        }
636    }
637    (result, zeros_from_bias)
638}
639
640/// Dequantizes a block of coefficients.
641pub 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        // Traditional quality 90 should give reasonable distance
659        let q = Quality::from_quality(90.0);
660        let d = q.to_distance();
661        assert!(d > 0.0 && d < 5.0);
662
663        // Distance 1.0 should round-trip approximately
664        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        // Higher quality should have smaller quantization values
675        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        // Should be within one quantization step
689        assert!((recovered - coeff).abs() < quant as f32);
690    }
691
692    #[test]
693    fn test_quant_values_in_range() {
694        // All generated tables should have values in [1, 255] for baseline
695        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        // All values should be valid
709        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        // Below DIST_THRESHOLD (1.5), scaling should be linear
742        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        // Above DIST_THRESHOLD, scaling should be non-linear for some frequencies
759        let distance = 3.0;
760
761        // DC coefficient (index 0) has exponent 1.0 - should be close to linear
762        let scale_dc = distance_to_scale(distance, 0);
763        // The formula with exp=1.0: max(0.5*d, d^1.0) = max(1.5, 3.0) = 3.0
764        assert!((scale_dc - distance).abs() < 0.1, "DC scale: {}", scale_dc);
765
766        // Index 1 has exponent 0.51 - should have significant non-linear effect
767        let scale_1 = distance_to_scale(distance, 1);
768        // exp=0.51, mul=1.5^(1-0.51)=1.5^0.49≈1.22, scale=1.22*3^0.51≈2.15
769        // or 0.5*3=1.5, whichever is greater
770        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        // Test that scale_to_distance inverts distance_to_scale
776        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        // Verify exponent array has expected structure
795        assert_eq!(FREQUENCY_EXPONENT.len(), 64);
796
797        // DC coefficient should have exponent 1.0
798        assert!((FREQUENCY_EXPONENT[0] - 1.0).abs() < 1e-6);
799
800        // Low frequencies (top-left) should have lower exponents
801        assert!(FREQUENCY_EXPONENT[1] < 1.0); // 0.51
802        assert!(FREQUENCY_EXPONENT[8] < 1.0); // 0.51
803
804        // High frequencies (bottom-right) should have exponent 1.0
805        assert!((FREQUENCY_EXPONENT[63] - 1.0).abs() < 1e-6);
806        assert!((FREQUENCY_EXPONENT[62] - 1.0).abs() < 1e-6);
807    }
808
809    /// Test that matches C++ DistanceToScale output for specific values.
810    /// Reference data generated from instrumented C++ jpegli.
811    #[test]
812    fn test_distance_to_scale_cpp_reference() {
813        // Test cases: (distance, freq_idx, expected_scale)
814        // These values should match C++ jpegli exactly
815        let test_cases = [
816            // Linear region (distance < 1.5)
817            (1.0_f32, 0_usize, 1.0_f32),
818            (1.0, 1, 1.0),
819            (1.0, 63, 1.0),
820            // Non-linear region
821            (2.0, 0, 2.0), // exp=1.0: max(1.0, 2.0) = 2.0
822            (3.0, 0, 3.0), // exp=1.0: max(1.5, 3.0) = 3.0
823            (5.0, 0, 5.0), // exp=1.0: linear
824        ];
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        // Verify table dimensions
842        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        // DC coefficient (index 0) should have zero multiplier in both tables
851        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        // Default should be all zeros (matches C++ when AQ is disabled)
872        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        // At distance <= 1.0, should use HQ table
881        let params = ZeroBiasParams::for_ycbcr(0.5, 0);
882
883        // Check some values match HQ table for Y component
884        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        // Check offsets
888        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        // At distance >= 3.0, should use LQ table
895        let params = ZeroBiasParams::for_ycbcr(5.0, 0);
896
897        // Check some values match LQ table for Y component
898        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        // At distance = 2.0, should be 50/50 blend of HQ and LQ
905        let params = ZeroBiasParams::for_ycbcr(2.0, 0);
906
907        // Check a value is between HQ and LQ
908        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        // Test all three components
924        for c in 0..3 {
925            let params = ZeroBiasParams::for_ycbcr(1.5, c);
926
927            // DC offset should match component
928            assert!((params.offset[0] - ZERO_BIAS_OFFSET_YCBCR_DC[c]).abs() < 1e-5);
929
930            // AC offsets should match component
931            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        // Coefficient below threshold should become zero
942        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        // Coefficient above threshold should pass through
947        let large_coeff = threshold * 2.0;
948        assert!((params.apply(large_coeff, 1, quant) - large_coeff).abs() < 1e-6);
949    }
950
951    /// Test zero-bias values against C++ reference data.
952    /// These values are computed from C++ jpegli InitQuantizer.
953    #[test]
954    fn test_zero_bias_cpp_reference() {
955        // At distance 2.0 (50% blend), Y component, coefficient 1:
956        // LQ[1] = 0.0568, HQ[1] = 0.0044
957        // Expected: 0.5 * 0.0568 + 0.5 * 0.0044 = 0.0306
958        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        // Offset for AC should be 0.59082 (Y component)
968        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}