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/// Dequantizes a block of coefficients with optimal Laplacian biases.
653///
654/// This implements the dequantization bias from jpegli which reduces
655/// reconstruction error. The bias shifts reconstructed values toward
656/// zero based on coefficient statistics.
657///
658/// See: J. R. Price and M. Rabbani, "Dequantization bias for JPEG decompression"
659/// Proceedings International Conference on Information Technology: Coding and
660/// Computing (Cat. No.PR00540), 2000, pp. 30-35.
661pub fn dequantize_block_with_bias(
662    quantized: &[i16; DCT_BLOCK_SIZE],
663    quant: &[u16; DCT_BLOCK_SIZE],
664    biases: &[f32; DCT_BLOCK_SIZE],
665) -> [f32; DCT_BLOCK_SIZE] {
666    let mut result = [0.0f32; DCT_BLOCK_SIZE];
667    for k in 0..DCT_BLOCK_SIZE {
668        let q = quantized[k];
669        if q == 0 {
670            // Zero coefficients stay zero
671            result[k] = 0.0;
672        } else {
673            // Apply bias: shift toward zero
674            // For positive: (q - bias) * quant
675            // For negative: (q + bias) * quant = (q - (-bias)) * quant
676            let bias = biases[k];
677            let biased_q = if q > 0 {
678                q as f32 - bias
679            } else {
680                q as f32 + bias
681            };
682            result[k] = biased_q * quant[k] as f32;
683        }
684    }
685    result
686}
687
688/// Statistics for computing optimal dequantization biases.
689#[derive(Debug, Clone)]
690pub struct DequantBiasStats {
691    /// Number of nonzero coefficients at each position (64 values per component)
692    pub nonzeros: Vec<i32>,
693    /// Sum of absolute values at each position (64 values per component)
694    pub sumabs: Vec<i32>,
695    /// Number of blocks processed per component
696    pub num_blocks: Vec<usize>,
697}
698
699impl DequantBiasStats {
700    /// Create new statistics tracker for the given number of components.
701    #[must_use]
702    pub fn new(num_components: usize) -> Self {
703        let size = num_components * DCT_BLOCK_SIZE;
704        Self {
705            nonzeros: vec![0; size],
706            sumabs: vec![0; size],
707            num_blocks: vec![0; num_components],
708        }
709    }
710
711    /// Gather statistics from a block of coefficients.
712    pub fn gather_block(&mut self, component: usize, coeffs: &[i16; DCT_BLOCK_SIZE]) {
713        let offset = component * DCT_BLOCK_SIZE;
714        for (k, &coeff) in coeffs.iter().enumerate() {
715            let abs_coeff = coeff.abs() as i32;
716            if abs_coeff > 0 {
717                self.nonzeros[offset + k] += 1;
718                self.sumabs[offset + k] += abs_coeff;
719            }
720        }
721        self.num_blocks[component] += 1;
722    }
723
724    /// Compute optimal Laplacian biases for a component.
725    ///
726    /// Returns biases for each coefficient position (64 values).
727    /// See: J. R. Price and M. Rabbani, "Dequantization bias for JPEG decompression"
728    #[must_use]
729    pub fn compute_biases(&self, component: usize) -> [f32; DCT_BLOCK_SIZE] {
730        let mut biases = [0.0f32; DCT_BLOCK_SIZE];
731        let offset = component * DCT_BLOCK_SIZE;
732        let num_blocks = self.num_blocks[component];
733
734        if num_blocks == 0 {
735            return biases;
736        }
737
738        // DC coefficient (k=0) doesn't get bias
739        biases[0] = 0.0;
740
741        // AC coefficients
742        for k in 1..DCT_BLOCK_SIZE {
743            let n1 = self.nonzeros[offset + k];
744            if n1 == 0 {
745                // No nonzero coefficients at this position - use default 0.5
746                biases[k] = 0.5;
747                continue;
748            }
749
750            // Notation from C++ jpegli (render.cc ComputeOptimalLaplacianBiases):
751            // N = num_blocks, N1 = nonzeros[k], N0 = N - N1, S = sumabs[k]
752            // Note: C++ uses float variables with double literals, but we use f32 for result
753            let n = num_blocks as f32;
754            let n1_f = n1 as f32;
755            let n0 = num_blocks as f32 - n1_f; // Match C++ which uses (int - float)
756            let s = self.sumabs[offset + k] as f32;
757
758            // Compute gamma from eq. 11, with A and B being grouping of terms
759            // A = 4*S + 2*N
760            // B = 4*S - 2*N1
761            // gamma = (-N0 + sqrt(N0^2 + A*B)) / A
762            // Using f64 for computation to match C++ mixed precision
763            let a: f64 = 4.0 * s as f64 + 2.0 * n as f64;
764            let b: f64 = 4.0 * s as f64 - 2.0 * n1_f as f64;
765            let n0_f64 = n0 as f64;
766
767            let gamma: f32 = ((-n0_f64 + (n0_f64 * n0_f64 + a * b).sqrt()) / a) as f32;
768            let gamma2: f32 = gamma * gamma;
769
770            // Compute bias from equation (5) in paper:
771            // bias = 0.5 * ((1 + gamma^2)/(1 - gamma^2) + 1/ln(gamma))
772            let gamma2_f64 = gamma2 as f64;
773            let gamma_f64 = gamma as f64;
774            biases[k] =
775                (0.5 * (((1.0 + gamma2_f64) / (1.0 - gamma2_f64)) + 1.0 / gamma_f64.ln())) as f32;
776        }
777
778        biases
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785
786    #[test]
787    fn test_quality_conversion() {
788        // Traditional quality 90 should give reasonable distance
789        let q = Quality::from_quality(90.0);
790        let d = q.to_distance();
791        assert!(d > 0.0 && d < 5.0);
792
793        // Distance 1.0 should round-trip approximately
794        let q2 = Quality::from_distance(1.0);
795        let d2 = q2.to_distance();
796        assert!((d2 - 1.0).abs() < 0.001);
797    }
798
799    #[test]
800    fn test_standard_table_generation() {
801        let table_q50 = generate_standard_jpeg_table(50.0, false);
802        let table_q90 = generate_standard_jpeg_table(90.0, false);
803
804        // Higher quality should have smaller quantization values
805        let sum_q50: u32 = table_q50.values.iter().map(|&v| v as u32).sum();
806        let sum_q90: u32 = table_q90.values.iter().map(|&v| v as u32).sum();
807        assert!(sum_q90 < sum_q50);
808    }
809
810    #[test]
811    fn test_quantize_dequantize() {
812        let coeff = 123.456f32;
813        let quant = 16;
814
815        let quantized = quantize(coeff, quant);
816        let recovered = dequantize(quantized, quant);
817
818        // Should be within one quantization step
819        assert!((recovered - coeff).abs() < quant as f32);
820    }
821
822    #[test]
823    fn test_quant_values_in_range() {
824        // All generated tables should have values in [1, 255] for baseline
825        for q in [10.0, 50.0, 90.0, 100.0] {
826            let table = generate_standard_jpeg_table(q, false);
827            for &v in &table.values {
828                assert!(v >= 1 && v <= 255);
829            }
830        }
831    }
832
833    #[test]
834    fn test_xyb_table_generation() {
835        let table =
836            generate_quant_table(Quality::from_distance(1.0), 0, ColorSpace::Xyb, true, false);
837
838        // All values should be valid
839        for &v in &table.values {
840            assert!(v >= 1 && v <= 255);
841        }
842    }
843
844    #[test]
845    fn test_quant_table_comparison() {
846        println!("\n=== Quant Table Comparison (Y channel) ===");
847        println!(
848            "{:>5} {:>8} {:>10} {:>10} {:>8} {:>8}",
849            "Q", "dist", "YCbCr_sum", "XYB_sum", "YCbCr[0]", "XYB[0]"
850        );
851
852        for q in [10, 20, 30, 40, 50, 60, 70, 80, 90] {
853            let quality = Quality::from_quality(q as f32);
854            let distance = quality.to_distance();
855
856            let ycbcr = generate_quant_table(quality, 0, ColorSpace::YCbCr, false, false);
857            let xyb = generate_quant_table(quality, 0, ColorSpace::Xyb, true, false);
858
859            let ycbcr_sum: u32 = ycbcr.values.iter().map(|&x| x as u32).sum();
860            let xyb_sum: u32 = xyb.values.iter().map(|&x| x as u32).sum();
861
862            println!(
863                "{:>5} {:>8.2} {:>10} {:>10} {:>8} {:>8}",
864                q, distance, ycbcr_sum, xyb_sum, ycbcr.values[0], xyb.values[0]
865            );
866        }
867    }
868
869    #[test]
870    fn test_distance_to_scale_linear_region() {
871        // Below DIST_THRESHOLD (1.5), scaling should be linear
872        for distance in [0.1, 0.5, 1.0, 1.4] {
873            for freq_idx in 0..64 {
874                let scale = distance_to_scale(distance, freq_idx);
875                assert!(
876                    (scale - distance).abs() < 1e-6,
877                    "Linear region failed: d={}, k={}, scale={}",
878                    distance,
879                    freq_idx,
880                    scale
881                );
882            }
883        }
884    }
885
886    #[test]
887    fn test_distance_to_scale_nonlinear_region() {
888        // Above DIST_THRESHOLD, scaling should be non-linear for some frequencies
889        let distance = 3.0;
890
891        // DC coefficient (index 0) has exponent 1.0 - should be close to linear
892        let scale_dc = distance_to_scale(distance, 0);
893        // The formula with exp=1.0: max(0.5*d, d^1.0) = max(1.5, 3.0) = 3.0
894        assert!((scale_dc - distance).abs() < 0.1, "DC scale: {}", scale_dc);
895
896        // Index 1 has exponent 0.51 - should have significant non-linear effect
897        let scale_1 = distance_to_scale(distance, 1);
898        // exp=0.51, mul=1.5^(1-0.51)=1.5^0.49≈1.22, scale=1.22*3^0.51≈2.15
899        // or 0.5*3=1.5, whichever is greater
900        assert!(scale_1 > 1.5 && scale_1 < 3.0, "Index 1 scale: {}", scale_1);
901    }
902
903    #[test]
904    fn test_distance_to_scale_roundtrip() {
905        // Test that scale_to_distance inverts distance_to_scale
906        for distance in [0.5, 1.0, 2.0, 3.0, 5.0, 10.0] {
907            for freq_idx in 0..64 {
908                let scale = distance_to_scale(distance, freq_idx);
909                let recovered = scale_to_distance(scale, freq_idx);
910                assert!(
911                    (recovered - distance).abs() < 0.01,
912                    "Roundtrip failed: d={}, k={}, scale={}, recovered={}",
913                    distance,
914                    freq_idx,
915                    scale,
916                    recovered
917                );
918            }
919        }
920    }
921
922    #[test]
923    fn test_frequency_exponent_values() {
924        // Verify exponent array has expected structure
925        assert_eq!(FREQUENCY_EXPONENT.len(), 64);
926
927        // DC coefficient should have exponent 1.0
928        assert!((FREQUENCY_EXPONENT[0] - 1.0).abs() < 1e-6);
929
930        // Low frequencies (top-left) should have lower exponents
931        assert!(FREQUENCY_EXPONENT[1] < 1.0); // 0.51
932        assert!(FREQUENCY_EXPONENT[8] < 1.0); // 0.51
933
934        // High frequencies (bottom-right) should have exponent 1.0
935        assert!((FREQUENCY_EXPONENT[63] - 1.0).abs() < 1e-6);
936        assert!((FREQUENCY_EXPONENT[62] - 1.0).abs() < 1e-6);
937    }
938
939    /// Test that matches C++ DistanceToScale output for specific values.
940    /// Reference data generated from instrumented C++ jpegli.
941    #[test]
942    fn test_distance_to_scale_cpp_reference() {
943        // Test cases: (distance, freq_idx, expected_scale)
944        // These values should match C++ jpegli exactly
945        let test_cases = [
946            // Linear region (distance < 1.5)
947            (1.0_f32, 0_usize, 1.0_f32),
948            (1.0, 1, 1.0),
949            (1.0, 63, 1.0),
950            // Non-linear region
951            (2.0, 0, 2.0), // exp=1.0: max(1.0, 2.0) = 2.0
952            (3.0, 0, 3.0), // exp=1.0: max(1.5, 3.0) = 3.0
953            (5.0, 0, 5.0), // exp=1.0: linear
954        ];
955
956        for (distance, freq_idx, expected) in test_cases {
957            let actual = distance_to_scale(distance, freq_idx);
958            assert!(
959                (actual - expected).abs() < 0.01,
960                "Mismatch: distance_to_scale({}, {}) = {}, expected {}",
961                distance,
962                freq_idx,
963                actual,
964                expected
965            );
966        }
967    }
968
969    #[test]
970    fn test_zero_bias_table_sizes() {
971        // Verify table dimensions
972        assert_eq!(ZERO_BIAS_MUL_YCBCR_LQ.len(), 192);
973        assert_eq!(ZERO_BIAS_MUL_YCBCR_HQ.len(), 192);
974        assert_eq!(ZERO_BIAS_OFFSET_YCBCR_DC.len(), 3);
975        assert_eq!(ZERO_BIAS_OFFSET_YCBCR_AC.len(), 3);
976    }
977
978    #[test]
979    fn test_zero_bias_dc_is_zero_in_tables() {
980        // DC coefficient (index 0) should have zero multiplier in both tables
981        for c in 0..3 {
982            assert!(
983                ZERO_BIAS_MUL_YCBCR_LQ[c * 64].abs() < 1e-6,
984                "LQ DC mul for component {} should be 0, got {}",
985                c,
986                ZERO_BIAS_MUL_YCBCR_LQ[c * 64]
987            );
988            assert!(
989                ZERO_BIAS_MUL_YCBCR_HQ[c * 64].abs() < 1e-6,
990                "HQ DC mul for component {} should be 0, got {}",
991                c,
992                ZERO_BIAS_MUL_YCBCR_HQ[c * 64]
993            );
994        }
995    }
996
997    #[test]
998    fn test_zero_bias_params_default() {
999        let params = ZeroBiasParams::default();
1000
1001        // Default should be all zeros (matches C++ when AQ is disabled)
1002        for k in 0..64 {
1003            assert!((params.mul[k]).abs() < 1e-6);
1004            assert!((params.offset[k]).abs() < 1e-6);
1005        }
1006    }
1007
1008    #[test]
1009    fn test_zero_bias_for_ycbcr_hq() {
1010        // At distance <= 1.0, should use HQ table
1011        let params = ZeroBiasParams::for_ycbcr(0.5, 0);
1012
1013        // Check some values match HQ table for Y component
1014        assert!((params.mul[1] - ZERO_BIAS_MUL_YCBCR_HQ[1]).abs() < 1e-5);
1015        assert!((params.mul[10] - ZERO_BIAS_MUL_YCBCR_HQ[10]).abs() < 1e-5);
1016
1017        // Check offsets
1018        assert!((params.offset[0] - ZERO_BIAS_OFFSET_YCBCR_DC[0]).abs() < 1e-5);
1019        assert!((params.offset[1] - ZERO_BIAS_OFFSET_YCBCR_AC[0]).abs() < 1e-5);
1020    }
1021
1022    #[test]
1023    fn test_zero_bias_for_ycbcr_lq() {
1024        // At distance >= 3.0, should use LQ table
1025        let params = ZeroBiasParams::for_ycbcr(5.0, 0);
1026
1027        // Check some values match LQ table for Y component
1028        assert!((params.mul[1] - ZERO_BIAS_MUL_YCBCR_LQ[1]).abs() < 1e-5);
1029        assert!((params.mul[10] - ZERO_BIAS_MUL_YCBCR_LQ[10]).abs() < 1e-5);
1030    }
1031
1032    #[test]
1033    fn test_zero_bias_for_ycbcr_blend() {
1034        // At distance = 2.0, should be 50/50 blend of HQ and LQ
1035        let params = ZeroBiasParams::for_ycbcr(2.0, 0);
1036
1037        // Check a value is between HQ and LQ
1038        let hq_val = ZERO_BIAS_MUL_YCBCR_HQ[1];
1039        let lq_val = ZERO_BIAS_MUL_YCBCR_LQ[1];
1040        let expected = 0.5 * hq_val + 0.5 * lq_val;
1041        assert!(
1042            (params.mul[1] - expected).abs() < 1e-5,
1043            "Expected blend {} (HQ={}, LQ={}), got {}",
1044            expected,
1045            hq_val,
1046            lq_val,
1047            params.mul[1]
1048        );
1049    }
1050
1051    #[test]
1052    fn test_zero_bias_for_ycbcr_all_components() {
1053        // Test all three components
1054        for c in 0..3 {
1055            let params = ZeroBiasParams::for_ycbcr(1.5, c);
1056
1057            // DC offset should match component
1058            assert!((params.offset[0] - ZERO_BIAS_OFFSET_YCBCR_DC[c]).abs() < 1e-5);
1059
1060            // AC offsets should match component
1061            assert!((params.offset[1] - ZERO_BIAS_OFFSET_YCBCR_AC[c]).abs() < 1e-5);
1062            assert!((params.offset[63] - ZERO_BIAS_OFFSET_YCBCR_AC[c]).abs() < 1e-5);
1063        }
1064    }
1065
1066    #[test]
1067    fn test_zero_bias_apply() {
1068        let params = ZeroBiasParams::for_ycbcr(2.0, 0);
1069        let quant = 16.0;
1070
1071        // Coefficient below threshold should become zero
1072        let threshold = (params.mul[1] + params.offset[1]) * quant;
1073        let small_coeff = threshold * 0.5;
1074        assert!((params.apply(small_coeff, 1, quant)).abs() < 1e-6);
1075
1076        // Coefficient above threshold should pass through
1077        let large_coeff = threshold * 2.0;
1078        assert!((params.apply(large_coeff, 1, quant) - large_coeff).abs() < 1e-6);
1079    }
1080
1081    /// Test zero-bias values against C++ reference data.
1082    /// These values are computed from C++ jpegli InitQuantizer.
1083    #[test]
1084    fn test_zero_bias_cpp_reference() {
1085        // At distance 2.0 (50% blend), Y component, coefficient 1:
1086        // LQ[1] = 0.0568, HQ[1] = 0.0044
1087        // Expected: 0.5 * 0.0568 + 0.5 * 0.0044 = 0.0306
1088        let params = ZeroBiasParams::for_ycbcr(2.0, 0);
1089        let expected_mul_1 = 0.5 * 0.0568 + 0.5 * 0.0044;
1090        assert!(
1091            (params.mul[1] - expected_mul_1).abs() < 1e-4,
1092            "Y mul[1] at d=2.0: expected {}, got {}",
1093            expected_mul_1,
1094            params.mul[1]
1095        );
1096
1097        // Offset for AC should be 0.59082 (Y component)
1098        assert!(
1099            (params.offset[1] - 0.59082).abs() < 1e-4,
1100            "Y offset[1]: expected 0.59082, got {}",
1101            params.offset[1]
1102        );
1103    }
1104}