Skip to main content

libjpeg_turbo_rs/api/
coefficient.rs

1// libjpeg-turbo-rs: alloc prelude (no_std support, issue #356)
2/// Coefficient-level JPEG access for lossless transforms.
3///
4/// Provides read_coefficients() / write_coefficients() / transform() API
5/// similar to libjpeg-turbo's jpegtran workflow.
6use crate::common::error::{JpegError, Result};
7use crate::common::quant_table::NATURAL_ORDER;
8use crate::common::types::{MarkerSaveConfig, SavedMarker};
9use crate::decode::marker::{JpegMetadata, MarkerReader};
10use crate::encode::huffman_encode::{build_huff_table, BitWriter, HuffTable, HuffmanEncoder};
11use crate::encode::marker_writer;
12use crate::encode::pipeline as encoder_pipeline;
13use crate::encode::tables;
14use crate::transform::spatial;
15use crate::transform::{TransformOp, TransformOptions};
16#[allow(unused_imports)]
17use alloc::{format, vec};
18#[allow(unused_imports)]
19use alloc::{string::ToString, vec::Vec};
20
21/// Per-component DCT coefficient data.
22#[derive(Debug, Clone)]
23pub struct ComponentCoefficients {
24    /// Quantized DCT blocks in zigzag order, each block is 64 coefficients.
25    pub blocks: Vec<[i16; 64]>,
26    /// Width in blocks.
27    pub blocks_x: usize,
28    /// Height in blocks.
29    pub blocks_y: usize,
30    /// Horizontal sampling factor.
31    pub h_sampling: u8,
32    /// Vertical sampling factor.
33    pub v_sampling: u8,
34    /// Quantization table index.
35    pub quant_table_index: u8,
36    /// Component identifier from the source JPEG (1=Y, 2=Cb, 3=Cr per JFIF).
37    pub component_id: u8,
38}
39
40/// Complete coefficient representation of a JPEG image.
41#[derive(Debug, Clone)]
42pub struct JpegCoefficients {
43    /// Image width in pixels.
44    pub width: u16,
45    /// Image height in pixels.
46    pub height: u16,
47    /// Sample data precision in bits per component (8 for baseline,
48    /// 12 for extended sequential / `monkey12`-style sources).
49    /// Re-emitted as the SOF byte at offset 4 of the SOF segment so
50    /// transcoded output preserves the source precision instead of
51    /// silently downgrading to 8-bit. `0` is treated as 8 for
52    /// backward compatibility with callers constructed before this
53    /// field existed.
54    pub data_precision: u8,
55    /// Per-component coefficient data.
56    pub components: Vec<ComponentCoefficients>,
57    /// Quantization tables (up to 4, in zigzag order).
58    pub quant_tables: Vec<[u16; 64]>,
59    /// Restart interval from the source JPEG (0 = no restart markers).
60    pub restart_interval: u16,
61    /// JFIF density units from source (0=aspect ratio, 1=DPI, 2=DPCM).
62    pub density_unit: u8,
63    /// JFIF X density from source.
64    pub x_density: u16,
65    /// JFIF Y density from source.
66    pub y_density: u16,
67    /// Adobe APP14 color-transform byte from the source JPEG, if an
68    /// Adobe marker was present. `None` means no APP14 was seen.
69    /// Re-emitting the same value on transcode preserves the original
70    /// colorspace classification (RGB vs YCbCr vs YCCK vs CMYK).
71    pub adobe_transform: Option<u8>,
72}
73
74impl JpegCoefficients {
75    /// Effective sample precision (bits per component): the stored
76    /// `data_precision`, or 8 when the field was left zeroed by an
77    /// older caller that pre-dates the precision plumb.
78    #[inline]
79    pub fn effective_precision(&self) -> u8 {
80        if self.data_precision == 0 {
81            8
82        } else {
83            self.data_precision
84        }
85    }
86}
87
88/// Per-component info extracted for re-encoding.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct EncoderComponentInfo {
91    /// Horizontal sampling factor.
92    pub h_sampling: u8,
93    /// Vertical sampling factor.
94    pub v_sampling: u8,
95    /// Quantization table index.
96    pub quant_table_index: u8,
97}
98
99/// Critical JPEG parameters extracted from decoded coefficients for re-encoding.
100///
101/// Matches the subset of `jpeg_compress_struct` fields that
102/// `jpeg_copy_critical_parameters()` copies between a decompressor and compressor.
103#[derive(Debug, Clone)]
104pub struct EncoderConfig {
105    /// Image width in pixels.
106    pub width: usize,
107    /// Image height in pixels.
108    pub height: usize,
109    /// Number of components.
110    pub num_components: usize,
111    /// Per-component sampling and quantization info.
112    pub component_info: Vec<EncoderComponentInfo>,
113    /// Quantization tables (in zigzag order).
114    pub quant_tables: Vec<[u16; 64]>,
115}
116
117/// Copy critical JPEG parameters from decoded coefficients for re-encoding.
118///
119/// Extracts dimensions, sampling factors, and quantization tables from
120/// `JpegCoefficients` into an `EncoderConfig` suitable for driving a new
121/// encoding pass. Matches libjpeg-turbo's `jpeg_copy_critical_parameters()`.
122pub fn copy_critical_parameters(coeffs: &JpegCoefficients) -> EncoderConfig {
123    let component_info: Vec<EncoderComponentInfo> = coeffs
124        .components
125        .iter()
126        .map(|comp| EncoderComponentInfo {
127            h_sampling: comp.h_sampling,
128            v_sampling: comp.v_sampling,
129            quant_table_index: comp.quant_table_index,
130        })
131        .collect();
132
133    EncoderConfig {
134        width: coeffs.width as usize,
135        height: coeffs.height as usize,
136        num_components: coeffs.components.len(),
137        component_info,
138        quant_tables: coeffs.quant_tables.clone(),
139    }
140}
141
142/// Read DCT coefficients from a JPEG byte stream.
143///
144/// Decodes entropy data to recover quantized DCT coefficients
145/// without performing IDCT or color conversion.
146pub fn read_coefficients(data: &[u8]) -> Result<JpegCoefficients> {
147    let mut reader = MarkerReader::new(data);
148    let metadata = reader.read_markers()?;
149    // Default frame-dimension guard (issue #355 review HIGH-1): this
150    // entry point has no limits API, so the permissive defaults bound
151    // the header bomb before block buffers are sized from the SOF.
152    crate::common::types::DecodeLimits::default().check_frame(
153        metadata.frame.width as usize,
154        metadata.frame.height as usize,
155    )?;
156
157    let frame = &metadata.frame;
158
159    let max_h = frame
160        .components
161        .iter()
162        .map(|c| c.horizontal_sampling as usize)
163        .max()
164        .unwrap_or(1);
165    let max_v = frame
166        .components
167        .iter()
168        .map(|c| c.vertical_sampling as usize)
169        .max()
170        .unwrap_or(1);
171
172    let mcu_w = max_h * 8;
173    let mcu_h = max_v * 8;
174    let mcus_x = (frame.width as usize).div_ceil(mcu_w);
175    let mcus_y = (frame.height as usize).div_ceil(mcu_h);
176
177    // Collect quant tables in natural (row-major) order for write_dqt
178    // compatibility. The four DQT slots may be sparse (e.g. only slot 1
179    // defined, or a gap at slot 2) — the writers emit `quant_tables[i]`
180    // as slot `i`, so compacting the slots requires remapping every
181    // component's slot reference into the dense index space or the
182    // re-encoded SOF would reference a table the output never defines
183    // (Fuzz Smoke runs 29679993066..30064906856, P4-34).
184    let mut slot_to_dense: [Option<u8>; 4] = [None; 4];
185    let mut quant_tables: Vec<[u16; 64]> = Vec::new();
186    for (slot, qt) in metadata.quant_tables.iter().enumerate() {
187        if let Some(q) = qt.as_ref() {
188            slot_to_dense[slot] = Some(quant_tables.len() as u8);
189            quant_tables.push(q.values);
190        }
191    }
192
193    // Allocate component coefficient buffers
194    let mut comp_data: Vec<ComponentCoefficients> = frame
195        .components
196        .iter()
197        .map(|comp| {
198            let bx = mcus_x * comp.horizontal_sampling as usize;
199            let by = mcus_y * comp.vertical_sampling as usize;
200            ComponentCoefficients {
201                blocks: vec![[0i16; 64]; bx * by],
202                blocks_x: bx,
203                blocks_y: by,
204                h_sampling: comp.horizontal_sampling,
205                v_sampling: comp.vertical_sampling,
206                // A reference to an undefined slot falls back to dense
207                // index 0 — the coefficient pass never dequantizes, and
208                // djpeg rejects the C-side equivalent anyway, so any
209                // defined table keeps the output self-consistent.
210                quant_table_index: slot_to_dense[comp.quant_table_index as usize].unwrap_or(0),
211                component_id: comp.id,
212            }
213        })
214        .collect();
215
216    if frame.is_progressive && metadata.is_arithmetic {
217        // SOF10: arithmetic progressive — use arithmetic decoder with progressive scans.
218        decode_arithmetic_progressive_coefficients(
219            data,
220            &metadata,
221            &mut comp_data,
222            mcus_x,
223            mcus_y,
224        )?;
225    } else if frame.is_progressive {
226        decode_progressive_coefficients(data, &metadata, &mut comp_data, mcus_x, mcus_y)?;
227    } else if metadata.is_arithmetic {
228        decode_arithmetic_coefficients(data, &metadata, &mut comp_data, mcus_x, mcus_y)?;
229    } else {
230        decode_baseline_coefficients(data, &metadata, &mut comp_data, mcus_x, mcus_y)?;
231    }
232
233    // Decoder stores blocks in natural (row-major) order;
234    // convert to zigzag order for encoder compatibility.
235    convert_all_to_zigzag(&mut comp_data);
236
237    let density: &crate::common::types::DensityInfo = &metadata.density;
238    let density_unit: u8 = match density.unit {
239        crate::common::types::DensityUnit::Unknown => 0,
240        crate::common::types::DensityUnit::Dpi => 1,
241        crate::common::types::DensityUnit::Dpcm => 2,
242    };
243
244    Ok(JpegCoefficients {
245        width: frame.width,
246        height: frame.height,
247        data_precision: frame.precision,
248        components: comp_data,
249        quant_tables,
250        restart_interval: metadata.restart_interval,
251        density_unit,
252        x_density: density.x,
253        y_density: density.y,
254        adobe_transform: if metadata.saw_adobe_marker {
255            Some(metadata.adobe_transform)
256        } else {
257            None
258        },
259    })
260}
261
262/// Write DCT coefficients to a JPEG byte stream.
263///
264/// Encodes quantized DCT coefficients using Huffman coding,
265/// producing a valid baseline JPEG file.
266pub fn write_coefficients(coeffs: &JpegCoefficients) -> Result<Vec<u8>> {
267    let num_components = coeffs.components.len();
268    let is_grayscale = num_components == 1;
269
270    // Standard ITU-T T.81 Annex K Huffman tables only define DC
271    // categories 0..=11; for `data_precision > 8` the source can produce
272    // DC categories 12..=15 that would silently encode as 0-bit codes
273    // and corrupt the stream. The optimised path
274    // (`write_coefficients_optimized`) is the supported route for
275    // 12-bit transcode (FFI sets `optimize_coding=1` automatically when
276    // `data_precision == 12`); refusing here keeps the failure loud.
277    // Tracked under `docs/LAST_MILE.md` → P0-4 (12-bit transcode).
278    let precision: u8 = coeffs.effective_precision();
279    if precision > 8 {
280        return Err(JpegError::Unsupported(format!(
281            "write_coefficients: 12-bit (precision={precision}) transcode not yet \
282             supported on the non-optimized path; route through \
283             write_coefficients_optimized (set optimize_coding=1)"
284        )));
285    }
286
287    // Build Huffman tables
288    let dc_luma_table = build_huff_table(&tables::DC_LUMINANCE_BITS, &tables::DC_LUMINANCE_VALUES);
289    let ac_luma_table = build_huff_table(&tables::AC_LUMINANCE_BITS, &tables::AC_LUMINANCE_VALUES);
290    let dc_chroma_table =
291        build_huff_table(&tables::DC_CHROMINANCE_BITS, &tables::DC_CHROMINANCE_VALUES);
292    let ac_chroma_table =
293        build_huff_table(&tables::AC_CHROMINANCE_BITS, &tables::AC_CHROMINANCE_VALUES);
294
295    let max_h: usize = coeffs
296        .components
297        .iter()
298        .map(|c| c.h_sampling as usize)
299        .max()
300        .unwrap_or(1);
301    let max_v: usize = coeffs
302        .components
303        .iter()
304        .map(|c| c.v_sampling as usize)
305        .max()
306        .unwrap_or(1);
307    let mcus_x = coeffs.components[0].blocks_x / coeffs.components[0].h_sampling as usize;
308    let mcus_y = coeffs.components[0].blocks_y / coeffs.components[0].v_sampling as usize;
309
310    // Compute actual data block counts per component (not MCU-padded).
311    // Blocks beyond these are "dummy" blocks: DC copied from last real block,
312    // AC all zeros. Matches C libjpeg-turbo jccoefct.c:184-191.
313    let data_blocks_x: Vec<usize> = coeffs
314        .components
315        .iter()
316        .map(|c| (coeffs.width as usize * c.h_sampling as usize).div_ceil(max_h * 8))
317        .collect();
318    let data_blocks_y: Vec<usize> = coeffs
319        .components
320        .iter()
321        .map(|c| (coeffs.height as usize * c.v_sampling as usize).div_ceil(max_v * 8))
322        .collect();
323
324    // Entropy encode with optional restart markers
325    let mut bit_writer = BitWriter::new(coeffs.width as usize * coeffs.height as usize);
326    let mut prev_dc = vec![0i16; num_components];
327    let dummy_block: [i16; 64] = [0i16; 64];
328    let ri: u32 = coeffs.restart_interval as u32;
329    let mut mcu_count: u32 = 0;
330    let mut restart_idx: u8 = 0;
331
332    for mcu_y in 0..mcus_y {
333        for mcu_x in 0..mcus_x {
334            // Insert restart marker between MCUs when interval is set
335            if ri > 0 && mcu_count > 0 && mcu_count.is_multiple_of(ri) {
336                bit_writer.flush();
337                bit_writer.write_restart_marker(restart_idx);
338                restart_idx = (restart_idx + 1) & 7;
339                // Reset DC predictions after restart
340                for dc in prev_dc.iter_mut() {
341                    *dc = 0;
342                }
343            }
344
345            for (ci, comp) in coeffs.components.iter().enumerate() {
346                let dc_table = if ci == 0 {
347                    &dc_luma_table
348                } else {
349                    &dc_chroma_table
350                };
351                let ac_table = if ci == 0 {
352                    &ac_luma_table
353                } else {
354                    &ac_chroma_table
355                };
356
357                for v in 0..comp.v_sampling as usize {
358                    for h in 0..comp.h_sampling as usize {
359                        let bx = mcu_x * comp.h_sampling as usize + h;
360                        let by = mcu_y * comp.v_sampling as usize + v;
361                        let is_dummy: bool = bx >= data_blocks_x[ci] || by >= data_blocks_y[ci];
362
363                        if is_dummy {
364                            let mut dblock: [i16; 64] = dummy_block;
365                            dblock[0] = prev_dc[ci];
366                            HuffmanEncoder::encode_block(
367                                &mut bit_writer,
368                                &dblock,
369                                &mut prev_dc[ci],
370                                dc_table,
371                                ac_table,
372                            );
373                        } else {
374                            let block_idx = by * comp.blocks_x + bx;
375                            let block = &comp.blocks[block_idx];
376                            HuffmanEncoder::encode_block(
377                                &mut bit_writer,
378                                block,
379                                &mut prev_dc[ci],
380                                dc_table,
381                                ac_table,
382                            );
383                        }
384                    }
385                }
386            }
387            mcu_count += 1;
388        }
389    }
390
391    bit_writer.flush();
392
393    // Assemble output
394    let mut output = Vec::with_capacity(bit_writer.data().len() + 1024);
395
396    marker_writer::write_soi(&mut output);
397    // Preserve source JFIF density (matches C jpegtran behavior)
398    marker_writer::write_app0_jfif_with_density(
399        &mut output,
400        coeffs.density_unit,
401        coeffs.x_density,
402        coeffs.y_density,
403    );
404
405    // Quantization tables
406    for (i, qt) in coeffs.quant_tables.iter().enumerate() {
407        marker_writer::write_dqt(&mut output, i as u8, qt);
408    }
409
410    // Frame header — use SOF1 (extended sequential) when quant tables need 16-bit
411    // precision (values > 255). The 12-bit precision case is rejected
412    // earlier in the guard at the top of this function, so `precision`
413    // here is always ≤ 8 and only quant-table magnitude matters.
414    // Matching C jpegtran behavior in `references/libjpeg-turbo/src/jcparam.c`.
415    let needs_extended: bool = coeffs
416        .quant_tables
417        .iter()
418        .any(|qt| qt.iter().any(|&v| v > 255));
419    let components: Vec<(u8, u8, u8, u8)> = coeffs
420        .components
421        .iter()
422        .map(|c| {
423            (
424                c.component_id,
425                c.h_sampling,
426                c.v_sampling,
427                c.quant_table_index,
428            )
429        })
430        .collect();
431    output.push(0xFF);
432    output.push(if needs_extended { 0xC1 } else { 0xC0 });
433    let sof_len: u16 = 2 + 1 + 2 + 2 + 1 + (components.len() as u16 * 3);
434    output.extend_from_slice(&sof_len.to_be_bytes());
435    output.push(precision); // sample precision (8 default; >8 forces SOF1)
436    output.extend_from_slice(&coeffs.height.to_be_bytes());
437    output.extend_from_slice(&coeffs.width.to_be_bytes());
438    output.push(components.len() as u8);
439    for &(id, h_samp, v_samp, quant_tbl_id) in &components {
440        output.push(id);
441        output.push((h_samp << 4) | v_samp);
442        output.push(quant_tbl_id);
443    }
444
445    // Huffman tables
446    marker_writer::write_dht(
447        &mut output,
448        0,
449        0,
450        &tables::DC_LUMINANCE_BITS,
451        &tables::DC_LUMINANCE_VALUES,
452    );
453    marker_writer::write_dht(
454        &mut output,
455        1,
456        0,
457        &tables::AC_LUMINANCE_BITS,
458        &tables::AC_LUMINANCE_VALUES,
459    );
460    if !is_grayscale {
461        marker_writer::write_dht(
462            &mut output,
463            0,
464            1,
465            &tables::DC_CHROMINANCE_BITS,
466            &tables::DC_CHROMINANCE_VALUES,
467        );
468        marker_writer::write_dht(
469            &mut output,
470            1,
471            1,
472            &tables::AC_CHROMINANCE_BITS,
473            &tables::AC_CHROMINANCE_VALUES,
474        );
475    }
476
477    // DRI (restart interval) — after DHT, before SOS (matching C jpegtran order)
478    if coeffs.restart_interval > 0 {
479        marker_writer::write_dri(&mut output, coeffs.restart_interval);
480    }
481
482    // Scan header — preserve source component IDs
483    let scan_components: Vec<(u8, u8, u8)> = coeffs
484        .components
485        .iter()
486        .enumerate()
487        .map(|(i, c)| {
488            let tbl = if i == 0 { 0u8 } else { 1u8 };
489            (c.component_id, tbl, tbl)
490        })
491        .collect();
492    marker_writer::write_sos(&mut output, &scan_components);
493
494    output.extend_from_slice(bit_writer.data());
495    marker_writer::write_eoi(&mut output);
496
497    Ok(output)
498}
499
500/// Apply a lossless transform to a JPEG image.
501///
502/// Delegates to [`transform_jpeg_with_options`] with default options, so
503/// metadata (EXIF/ICC/COM markers) is preserved
504/// ([`MarkerCopyMode::All`](crate::MarkerCopyMode::All), matching both [`TransformOptions::default`]
505/// and C TurboJPEG's `tjTransform` without `TJXOPT_COPYNONE`). Pass
506/// [`MarkerCopyMode::None`](crate::MarkerCopyMode::None) via [`transform_jpeg_with_options`] to strip
507/// markers instead.
508pub fn transform_jpeg(data: &[u8], op: TransformOp) -> Result<Vec<u8>> {
509    transform_jpeg_with_options(
510        data,
511        &TransformOptions {
512            op,
513            ..Default::default()
514        },
515    )
516}
517
518/// Apply a lossless transform with full TJXOPT-compatible options.
519///
520/// Supports all 9 flags from libjpeg-turbo: perfect, trim, crop, grayscale,
521/// no_output, progressive, arithmetic, optimize, and copy_markers.
522pub fn transform_jpeg_with_options(data: &[u8], options: &TransformOptions) -> Result<Vec<u8>> {
523    // Read saved markers from the source based on copy_markers mode.
524    let saved_markers: Vec<SavedMarker> = match options.copy_markers {
525        crate::transform::MarkerCopyMode::All => {
526            let mut reader: MarkerReader<'_> = MarkerReader::new(data);
527            reader.set_marker_save_config(MarkerSaveConfig::All);
528            let meta: JpegMetadata = reader.read_markers()?;
529            // Filter out JFIF APP0 since write_coefficients writes its own.
530            meta.saved_markers
531                .into_iter()
532                .filter(|m| m.code != 0xE0)
533                .collect()
534        }
535        crate::transform::MarkerCopyMode::IccOnly => {
536            let mut reader: MarkerReader<'_> = MarkerReader::new(data);
537            reader.set_marker_save_config(MarkerSaveConfig::All);
538            let meta: JpegMetadata = reader.read_markers()?;
539            // Keep only APP2 markers that contain ICC profile data.
540            meta.saved_markers
541                .into_iter()
542                .filter(|m| m.code == 0xE2)
543                .collect()
544        }
545        crate::transform::MarkerCopyMode::None => Vec::new(),
546    };
547
548    let mut coeffs = read_coefficients(data)?;
549    let op: TransformOp = options.op;
550
551    // Determine iMCU dimensions from the coefficient data.
552    let max_h: usize = coeffs
553        .components
554        .iter()
555        .map(|c| c.h_sampling as usize)
556        .max()
557        .unwrap_or(1);
558    let max_v: usize = coeffs
559        .components
560        .iter()
561        .map(|c| c.v_sampling as usize)
562        .max()
563        .unwrap_or(1);
564    let imcu_w: usize = max_h * 8;
565    let imcu_h: usize = max_v * 8;
566
567    // For transforms that swap dimensions, use swapped iMCU sizes for alignment checks.
568    let swaps_dims: bool = matches!(
569        op,
570        TransformOp::Transpose | TransformOp::Transverse | TransformOp::Rot90 | TransformOp::Rot270
571    );
572
573    // Check which dimension(s) need to be iMCU-aligned for this transform.
574    let needs_width_aligned: bool = matches!(
575        op,
576        TransformOp::HFlip
577            | TransformOp::Transverse
578            | TransformOp::Rot90
579            | TransformOp::Rot180
580            | TransformOp::Rot270
581    );
582    let needs_height_aligned: bool = matches!(
583        op,
584        TransformOp::VFlip
585            | TransformOp::Transverse
586            | TransformOp::Rot90
587            | TransformOp::Rot180
588            | TransformOp::Rot270
589    );
590
591    let width_aligned: bool = (coeffs.width as usize).is_multiple_of(imcu_w);
592    let height_aligned: bool = (coeffs.height as usize).is_multiple_of(imcu_h);
593
594    let has_partial_width: bool = needs_width_aligned && !width_aligned;
595    let has_partial_height: bool = needs_height_aligned && !height_aligned;
596
597    // PERFECT: fail if partial iMCU blocks exist for this transform.
598    if options.perfect && (has_partial_width || has_partial_height) {
599        return Err(JpegError::CorruptData(format!(
600            "perfect transform requested but image {}x{} is not iMCU-aligned (iMCU={}x{})",
601            coeffs.width, coeffs.height, imcu_w, imcu_h
602        )));
603    }
604
605    // TRIM: discard partial iMCU blocks at edges.
606    // For dimension-swapping transforms (rot90, rot270), C jpegtran only
607    // trims selectively: rot90 trims what becomes the output width (source
608    // height), rot270 trims what becomes the output height (source width).
609    if options.trim && (has_partial_width || has_partial_height) {
610        let trim_width: bool = match op {
611            // ROT90: source height → output width, only trim source height
612            TransformOp::Rot90 => false,
613            _ => has_partial_width,
614        };
615        let trim_height: bool = match op {
616            // ROT270: source width → output height, only trim source width
617            TransformOp::Rot270 => false,
618            _ => has_partial_height,
619        };
620
621        let trimmed_w: usize = if trim_width {
622            (coeffs.width as usize / imcu_w) * imcu_w
623        } else {
624            coeffs.width as usize
625        };
626        let trimmed_h: usize = if trim_height {
627            (coeffs.height as usize / imcu_h) * imcu_h
628        } else {
629            coeffs.height as usize
630        };
631
632        if trimmed_w == 0 || trimmed_h == 0 {
633            return Err(JpegError::CorruptData(
634                "trim would remove all image data".to_string(),
635            ));
636        }
637
638        coeffs.width = trimmed_w as u16;
639        coeffs.height = trimmed_h as u16;
640
641        // Trim coefficient arrays for each component.
642        for comp in &mut coeffs.components {
643            let new_bx: usize = trimmed_w.div_ceil(8) * comp.h_sampling as usize / max_h;
644            let new_by: usize = trimmed_h.div_ceil(8) * comp.v_sampling as usize / max_v;
645
646            // Only need to rebuild if we actually trimmed columns or rows.
647            if new_bx < comp.blocks_x || new_by < comp.blocks_y {
648                let mut new_blocks: Vec<[i16; 64]> = Vec::with_capacity(new_bx * new_by);
649                for by in 0..new_by {
650                    for bx in 0..new_bx {
651                        let old_idx: usize = by * comp.blocks_x + bx;
652                        new_blocks.push(comp.blocks[old_idx]);
653                    }
654                }
655                comp.blocks = new_blocks;
656                comp.blocks_x = new_bx;
657                comp.blocks_y = new_by;
658            }
659        }
660    }
661
662    // GRAYSCALE: drop all non-Y components.
663    // When the original Y component had h_sampling > 1 or v_sampling > 1,
664    // the block grid was MCU-padded. Rearrange blocks to a 1x1 raster layout
665    // by stripping padding blocks at row/column edges.
666    if options.grayscale && coeffs.components.len() > 1 {
667        let orig_bx: usize = coeffs.components[0].blocks_x;
668        let orig_by: usize = coeffs.components[0].blocks_y;
669
670        // Target: ceil(width/8) x ceil(height/8) blocks in simple raster order
671        let target_bx: usize = (coeffs.width as usize).div_ceil(8);
672        let target_by: usize = (coeffs.height as usize).div_ceil(8);
673
674        // Strip-padding rebuild assumes the source grid covers at least as
675        // many blocks as the target raster. With degenerate sampling factors
676        // (e.g. Y=h2v1 alongside Cb=h2v3), max_v is dominated by a non-Y
677        // component, so the Y MCU grid can be smaller than ceil(H/8)×ceil(W/8)
678        // and the strip loop would read past `comp.blocks`. C jpegtran rejects
679        // the same input with "Unsupported color conversion request"; match
680        // that rather than fabricating zero blocks.
681        if orig_bx < target_bx || orig_by < target_by {
682            return Err(JpegError::Unsupported(format!(
683                "grayscale conversion requires Y component grid {}x{} \
684                 to cover image raster {}x{}; sampling factors are incompatible",
685                orig_bx, orig_by, target_bx, target_by
686            )));
687        }
688
689        if orig_bx != target_bx || orig_by != target_by {
690            // Rearrange: blocks are in raster order within MCU-padded grid,
691            // just strip the extra columns/rows.
692            let mut new_blocks: Vec<[i16; 64]> = Vec::with_capacity(target_bx * target_by);
693            for by in 0..target_by {
694                for bx in 0..target_bx {
695                    new_blocks.push(coeffs.components[0].blocks[by * orig_bx + bx]);
696                }
697            }
698            coeffs.components[0].blocks = new_blocks;
699            coeffs.components[0].blocks_x = target_bx;
700            coeffs.components[0].blocks_y = target_by;
701        }
702
703        coeffs.components.truncate(1);
704        coeffs.components[0].h_sampling = 1;
705        coeffs.components[0].v_sampling = 1;
706        if coeffs.quant_tables.len() > 1 {
707            coeffs.quant_tables.truncate(1);
708        }
709        coeffs.components[0].quant_table_index = 0;
710    }
711
712    // Recompute max sampling factors after grayscale may have changed them.
713    let max_h: usize = coeffs
714        .components
715        .iter()
716        .map(|c| c.h_sampling as usize)
717        .max()
718        .unwrap_or(1);
719    let max_v: usize = coeffs
720        .components
721        .iter()
722        .map(|c| c.v_sampling as usize)
723        .max()
724        .unwrap_or(1);
725
726    // Apply spatial transform (reuses existing logic from transform_jpeg).
727    // C libjpeg-turbo only transforms blocks within full MCU columns/rows,
728    // leaving partial edge MCU blocks untouched. comp_w/comp_h are the
729    // "mirrorable" region sizes per component.
730    if op != TransformOp::None {
731        // Blocks are stored in zigzag order; apply the op through its
732        // zigzag-composed permutation map instead of converting the whole
733        // coefficient corpus to natural order and back (issue #308).
734        // Bind the `MAP_*` consts directly (not via `zigzag_map(op)`) so
735        // each branch reads a statically-known table; `map_op` is only
736        // for the generic fallback branch.
737        let map_op: &spatial::ZigzagMap = spatial::zigzag_map(op);
738        let map_transpose: &spatial::ZigzagMap = &spatial::MAP_TRANSPOSE;
739        let map_rot90: &spatial::ZigzagMap = &spatial::MAP_ROT90;
740        let map_rot270: &spatial::ZigzagMap = &spatial::MAP_ROT270;
741        let map_flip_h: &spatial::ZigzagMap = &spatial::MAP_HFLIP;
742        let map_flip_v: &spatial::ZigzagMap = &spatial::MAP_VFLIP;
743        let map_transverse: &spatial::ZigzagMap = &spatial::MAP_TRANSVERSE;
744        let map_rot180: &spatial::ZigzagMap = &spatial::MAP_ROT180;
745
746        // Full MCU columns/rows (partial edge MCUs excluded from transform).
747        let mcu_cols: usize = coeffs.width as usize / (max_h * 8);
748        let mcu_rows: usize = coeffs.height as usize / (max_v * 8);
749
750        for comp in &mut coeffs.components {
751            let old_bx: usize = comp.blocks_x;
752            let old_by: usize = comp.blocks_y;
753            // Mirrorable region: only full MCU blocks participate in transform.
754            let comp_w: usize = mcu_cols * comp.h_sampling as usize;
755            let comp_h: usize = mcu_rows * comp.v_sampling as usize;
756            // Dimension-swapping ops (Transpose/Rot90/Rot270/Transverse)
757            // write every destination block below, so seeding the output
758            // with a copy of the source would be pure wasted memory
759            // traffic (~2×128 bytes per block). Only the in-place mirror
760            // ops (HFlip/VFlip/Rot180) need the "edge blocks stay
761            // untouched" pre-copy.
762            let mut new_blocks: Vec<[i16; 64]> = if swaps_dims {
763                vec![[0i16; 64]; old_bx * old_by]
764            } else {
765                comp.blocks.clone()
766            };
767
768            if matches!(op, TransformOp::Transpose) {
769                for by in 0..old_by {
770                    for bx in 0..old_bx {
771                        let src_idx: usize = by * old_bx + bx;
772                        let dst_idx: usize = bx * old_by + by;
773                        map_transpose.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
774                    }
775                }
776                comp.blocks_x = old_by;
777                comp.blocks_y = old_bx;
778            } else if matches!(op, TransformOp::Rot90) {
779                let new_bx: usize = old_by;
780                for by in 0..old_by {
781                    for bx in 0..old_bx {
782                        let src_idx: usize = by * old_bx + bx;
783                        if by < comp_h {
784                            let dst_idx: usize = bx * new_bx + (comp_h - 1 - by);
785                            map_rot90.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
786                        } else {
787                            let dst_idx: usize = bx * new_bx + by;
788                            map_transpose.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
789                        }
790                    }
791                }
792                comp.blocks_x = old_by;
793                comp.blocks_y = old_bx;
794            } else if matches!(op, TransformOp::Rot270) {
795                let new_bx: usize = old_by;
796                for by in 0..old_by {
797                    for bx in 0..old_bx {
798                        let src_idx: usize = by * old_bx + bx;
799                        if bx < comp_w {
800                            let dst_idx: usize = (comp_w - 1 - bx) * new_bx + by;
801                            map_rot270.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
802                        } else {
803                            let dst_idx: usize = bx * new_bx + by;
804                            map_transpose.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
805                        }
806                    }
807                }
808                comp.blocks_x = old_by;
809                comp.blocks_y = old_bx;
810            } else if matches!(op, TransformOp::Transverse) {
811                let new_bx: usize = old_by;
812                for by in 0..old_by {
813                    for bx in 0..old_bx {
814                        let src_idx: usize = by * old_bx + bx;
815                        let in_h: bool = by < comp_h;
816                        let in_w: bool = bx < comp_w;
817                        if in_h && in_w {
818                            let dst_idx: usize = (comp_w - 1 - bx) * new_bx + (comp_h - 1 - by);
819                            map_transverse.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
820                        } else if !in_h && in_w {
821                            let dst_idx: usize = (comp_w - 1 - bx) * new_bx + by;
822                            map_rot270.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
823                        } else if in_h && !in_w {
824                            let dst_idx: usize = bx * new_bx + (comp_h - 1 - by);
825                            map_rot90.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
826                        } else {
827                            let dst_idx: usize = bx * new_bx + by;
828                            map_transpose.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
829                        }
830                    }
831                }
832                comp.blocks_x = old_by;
833                comp.blocks_y = old_bx;
834            } else if matches!(op, TransformOp::HFlip) {
835                // Only flip within the mirrorable region (comp_w blocks).
836                // Edge blocks beyond comp_w are left untouched.
837                for by in 0..old_by {
838                    for bx in 0..comp_w {
839                        let src_idx: usize = by * old_bx + bx;
840                        let dst_idx: usize = by * old_bx + (comp_w - 1 - bx);
841                        map_flip_h.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
842                    }
843                }
844            } else if matches!(op, TransformOp::VFlip) {
845                // Only flip within the mirrorable region (comp_h rows).
846                for by in 0..comp_h {
847                    for bx in 0..old_bx {
848                        let src_idx: usize = by * old_bx + bx;
849                        let dst_idx: usize = (comp_h - 1 - by) * old_bx + bx;
850                        map_flip_v.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
851                    }
852                }
853            } else if matches!(op, TransformOp::Rot180) {
854                // 4-zone approach matching C transupp.c do_rot_180:
855                // Zone 1 (bx<comp_w, by<comp_h): full 180° (both axes mirror)
856                // Zone 2 (bx>=comp_w, by<comp_h): only vertical mirror
857                // Zone 3 (bx<comp_w, by>=comp_h): only horizontal mirror
858                // Zone 4 (bx>=comp_w, by>=comp_h): copy verbatim (already done)
859                for by in 0..old_by {
860                    for bx in 0..old_bx {
861                        let src_idx: usize = by * old_bx + bx;
862                        if by < comp_h && bx < comp_w {
863                            // Zone 1: full 180° rotation
864                            let dst_idx: usize = (comp_h - 1 - by) * old_bx + (comp_w - 1 - bx);
865                            map_rot180.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
866                        } else if by < comp_h {
867                            // Zone 2: only vertical mirror (right edge)
868                            let dst_idx: usize = (comp_h - 1 - by) * old_bx + bx;
869                            map_flip_v.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
870                        } else if bx < comp_w {
871                            // Zone 3: only horizontal mirror (bottom edge)
872                            let dst_idx: usize = by * old_bx + (comp_w - 1 - bx);
873                            map_flip_h.apply(&comp.blocks[src_idx], &mut new_blocks[dst_idx]);
874                        }
875                        // Zone 4: already copied verbatim from copy_from_slice
876                    }
877                }
878            } else {
879                for (i, new_block) in new_blocks.iter_mut().enumerate() {
880                    map_op.apply(&comp.blocks[i], new_block);
881                }
882            }
883
884            comp.blocks = new_blocks;
885        }
886
887        if swaps_dims {
888            core::mem::swap(&mut coeffs.width, &mut coeffs.height);
889            for comp in &mut coeffs.components {
890                core::mem::swap(&mut comp.h_sampling, &mut comp.v_sampling);
891            }
892            for qt in &mut coeffs.quant_tables {
893                transpose_quant_table(qt);
894            }
895        }
896    }
897
898    // CROP: crop coefficient arrays to the specified region.
899    // Applied AFTER spatial transform (crop coordinates are in output space).
900    // Matches C jpegtran semantics: X/Y are rounded DOWN to iMCU boundaries,
901    // and output dimensions are extended to fully cover the requested region.
902    if let Some(crop) = &options.crop {
903        // Recompute max sampling factors from post-transform state
904        // (dimension-swapping transforms swap h/v sampling factors).
905        let post_max_h: usize = coeffs
906            .components
907            .iter()
908            .map(|c| c.h_sampling as usize)
909            .max()
910            .unwrap_or(1);
911        let post_max_v: usize = coeffs
912            .components
913            .iter()
914            .map(|c| c.v_sampling as usize)
915            .max()
916            .unwrap_or(1);
917        let imcu_w: usize = post_max_h * 8;
918        let imcu_h: usize = post_max_v * 8;
919
920        // Crop coordinates are in *post-transform* image space (see comment
921        // above). A swap-dim spatial op (Rot90 / Rot270 / Transpose /
922        // Transverse) can leave the requested crop origin past the
923        // post-transform width or height — e.g. a 2048x16 source rotated 90°
924        // becomes 16x2048, so a crop with x=2032 from the original frame is
925        // outside the new width=16. The downstream `coeffs.width - (crop.x -
926        // remainder_x)` then underflows. Found via fuzz_transform_options
927        // round-5 (CI run 25218344069) at coefficient.rs:907. Reject up
928        // front rather than wrap silently.
929        if crop.x >= coeffs.width as usize || crop.y >= coeffs.height as usize {
930            return Err(JpegError::Unsupported(format!(
931                "crop origin (x={}, y={}) lies outside post-transform image \
932                 ({}x{}); crop coordinates must be in output space",
933                crop.x, crop.y, coeffs.width, coeffs.height,
934            )));
935        }
936
937        let remainder_x: usize = crop.x % imcu_w;
938        let remainder_y: usize = crop.y % imcu_h;
939
940        // Block-level offsets (rounded down to iMCU boundary)
941        let crop_x_blocks: usize = crop.x / imcu_w * post_max_h;
942        let crop_y_blocks: usize = crop.y / imcu_h * post_max_v;
943
944        // Extend output size to cover the full requested region from the
945        // MCU-aligned start position.
946        let out_w: usize =
947            (crop.width + remainder_x).min(coeffs.width as usize - (crop.x - remainder_x));
948        let out_h: usize =
949            (crop.height + remainder_y).min(coeffs.height as usize - (crop.y - remainder_y));
950        // Compute block dimensions in iMCU units first, then multiply by sampling
951        // factor. This guarantees block counts are always multiples of max_h/max_v,
952        // matching C libjpeg-turbo's transupp.c:1805-1822 approach.
953        let crop_w_blocks: usize = out_w.div_ceil(imcu_w) * post_max_h;
954        let crop_h_blocks: usize = out_h.div_ceil(imcu_h) * post_max_v;
955
956        coeffs.width = out_w as u16;
957        coeffs.height = out_h as u16;
958
959        for comp in &mut coeffs.components {
960            let comp_crop_x: usize = crop_x_blocks * comp.h_sampling as usize / post_max_h;
961            let comp_crop_y: usize = crop_y_blocks * comp.v_sampling as usize / post_max_v;
962            let comp_crop_w: usize = crop_w_blocks * comp.h_sampling as usize / post_max_h;
963            let comp_crop_h: usize = crop_h_blocks * comp.v_sampling as usize / post_max_v;
964
965            let new_bx: usize = comp_crop_w.min(comp.blocks_x - comp_crop_x);
966            let new_by: usize = comp_crop_h.min(comp.blocks_y - comp_crop_y);
967
968            let mut new_blocks: Vec<[i16; 64]> = Vec::with_capacity(new_bx * new_by);
969            for by in 0..new_by {
970                for bx in 0..new_bx {
971                    let old_idx: usize = (comp_crop_y + by) * comp.blocks_x + (comp_crop_x + bx);
972                    new_blocks.push(comp.blocks[old_idx]);
973                }
974            }
975            comp.blocks = new_blocks;
976            comp.blocks_x = new_bx;
977            comp.blocks_y = new_by;
978        }
979    }
980
981    // CUSTOM_FILTER: invoke user callback on each block after spatial transform.
982    if let Some(ref filter) = options.custom_filter {
983        for (ci, comp) in coeffs.components.iter_mut().enumerate() {
984            let blocks_x: usize = comp.blocks_x;
985            for by in 0..comp.blocks_y {
986                for bx in 0..blocks_x {
987                    let block_idx: usize = by * blocks_x + bx;
988                    filter(&mut comp.blocks[block_idx], ci, bx, by);
989                }
990            }
991        }
992    }
993
994    // NO_OUTPUT: skip writing, return empty.
995    if options.no_output {
996        return Ok(Vec::new());
997    }
998
999    // Apply restart interval: preserve source RI unless user explicitly overrides.
1000    // Matches C jpegtran behavior — source restart interval flows through
1001    // transforms unchanged. Only overwrite when explicitly requested.
1002    //
1003    // When `restart_in_rows == true`, the user-supplied value is in MCU rows.
1004    // For sequential/optimized writers the DRI is a single scan-wide value,
1005    // so it is precomputed against the output interleaved MCU grid. The
1006    // progressive writer recomputes per-scan DRI from `progressive_restart_rows`
1007    // below (matches C `per_scan_setup` which updates `cinfo->restart_interval`
1008    // based on each scan's `MCUs_per_row` — interleaved scans and
1009    // non-interleaved AC scans use different row counts).
1010    let progressive_restart_rows: Option<u16> =
1011        if options.progressive && options.restart_interval > 0 && options.restart_in_rows {
1012            Some(options.restart_interval)
1013        } else {
1014            None
1015        };
1016
1017    if options.restart_interval > 0 {
1018        if options.restart_in_rows {
1019            let max_h: usize = coeffs
1020                .components
1021                .iter()
1022                .map(|c| c.h_sampling as usize)
1023                .max()
1024                .unwrap_or(1);
1025            let output_mcus_per_row: usize = (coeffs.width as usize).div_ceil(max_h * 8);
1026            let dri: usize = options.restart_interval as usize * output_mcus_per_row;
1027            coeffs.restart_interval = dri.min(u16::MAX as usize) as u16;
1028        } else {
1029            coeffs.restart_interval = options.restart_interval;
1030        }
1031    }
1032    // Source RI carried over from the input JPEG can become invalid after
1033    // dimension-swapping transforms with trim, since the output MCU grid is
1034    // fundamentally different. Clear to avoid producing truncated entropy data.
1035    let swaps_dimensions: bool = matches!(
1036        options.op,
1037        crate::transform::TransformOp::Rot90
1038            | crate::transform::TransformOp::Rot270
1039            | crate::transform::TransformOp::Transpose
1040            | crate::transform::TransformOp::Transverse
1041    );
1042    if swaps_dimensions && options.trim && options.restart_interval == 0 {
1043        coeffs.restart_interval = 0;
1044    }
1045
1046    // Write output with the appropriate encoding. C jpegtran -progressive
1047    // implies -optimize (per-scan Huffman tables).
1048    //
1049    // The sampling-factor gate `max_{h,v} ∈ {1,2,4}` matches the eight standard
1050    // TJSAMP factors (444/422/440/420/411/441/410/24) — the set verified by
1051    // `tests/regression_progressive_4pixel_chroma_transform.rs` (P3-4 closure)
1052    // and the full `c_tjtrantest_full` matrix. Non-standard 3x sampling
1053    // (max_h or max_v = 3) is unverified against `jpegtran -progressive` and
1054    // is tracked under P3-6; it falls back to optimized baseline until that
1055    // entry closes.
1056    //
1057    // The `data_blocks_{x,y} ≤ comp.blocks_{x,y}` check guards malformed
1058    // coefficient buffers where the stored block grid is smaller than the
1059    // image dimensions imply — well-formed coefficients from
1060    // `read_coefficients` always satisfy it.
1061    let progressive_safe: bool = options.progressive && {
1062        let max_h: usize = coeffs
1063            .components
1064            .iter()
1065            .map(|c| c.h_sampling as usize)
1066            .max()
1067            .unwrap_or(1);
1068        let max_v: usize = coeffs
1069            .components
1070            .iter()
1071            .map(|c| c.v_sampling as usize)
1072            .max()
1073            .unwrap_or(1);
1074        let standard_factors: bool =
1075            max_h.is_power_of_two() && max_v.is_power_of_two() && max_h <= 4 && max_v <= 4;
1076        standard_factors
1077            && coeffs.components.iter().all(|comp| {
1078                let dbx: usize =
1079                    (coeffs.width as usize * comp.h_sampling as usize).div_ceil(max_h * 8);
1080                let dby: usize =
1081                    (coeffs.height as usize * comp.v_sampling as usize).div_ceil(max_v * 8);
1082                dbx <= comp.blocks_x && dby <= comp.blocks_y
1083            })
1084    };
1085    // 12-bit precision (e.g. `monkey12.jpg` transcode) and coefficient
1086    // buffers with out-of-range baseline symbols MUST go through the
1087    // optimized Huffman writer. The non-optimized path uses the standard
1088    // Annex K tables, which do not define every possible DC/AC category;
1089    // using it anyway would encode zero-bit Huffman symbols and produce
1090    // a JPEG that downstream C tools reject. This applies to any source
1091    // — baseline JPEGs with custom DHT tables can also carry categories
1092    // beyond the Annex K range (DC > 11, AC > 10).
1093    let force_optimize: bool =
1094        coeffs.effective_precision() > 8 || needs_optimized_baseline_huffman(&coeffs);
1095    let output: Vec<u8> = if options.arithmetic && progressive_safe {
1096        write_coefficients_progressive_arithmetic(&coeffs, progressive_restart_rows)?
1097    } else if options.arithmetic {
1098        write_coefficients_arithmetic(&coeffs)?
1099    } else if progressive_safe {
1100        write_coefficients_progressive(&coeffs, progressive_restart_rows)?
1101    } else if options.optimize || force_optimize {
1102        write_coefficients_optimized(&coeffs)?
1103    } else {
1104        write_coefficients(&coeffs)?
1105    };
1106
1107    // Inject saved markers from the source if copy_markers is enabled.
1108    if !saved_markers.is_empty() {
1109        Ok(encoder_pipeline::inject_saved_markers(
1110            &output,
1111            &saved_markers,
1112        ))
1113    } else {
1114        Ok(output)
1115    }
1116}
1117
1118fn huffman_category(value: i16) -> u8 {
1119    let magnitude: u16 = value.unsigned_abs();
1120    if magnitude == 0 {
1121        0
1122    } else {
1123        (16 - magnitude.leading_zeros()) as u8
1124    }
1125}
1126
1127fn needs_optimized_baseline_huffman(coeffs: &JpegCoefficients) -> bool {
1128    if coeffs.components.is_empty() {
1129        return false;
1130    }
1131
1132    let max_h: usize = coeffs
1133        .components
1134        .iter()
1135        .map(|c| c.h_sampling as usize)
1136        .max()
1137        .unwrap_or(1);
1138    let max_v: usize = coeffs
1139        .components
1140        .iter()
1141        .map(|c| c.v_sampling as usize)
1142        .max()
1143        .unwrap_or(1);
1144    let mcus_x: usize = coeffs.components[0].blocks_x / coeffs.components[0].h_sampling as usize;
1145    let mcus_y: usize = coeffs.components[0].blocks_y / coeffs.components[0].v_sampling as usize;
1146
1147    let data_blocks_x: Vec<usize> = coeffs
1148        .components
1149        .iter()
1150        .map(|c| (coeffs.width as usize * c.h_sampling as usize).div_ceil(max_h * 8))
1151        .collect();
1152    let data_blocks_y: Vec<usize> = coeffs
1153        .components
1154        .iter()
1155        .map(|c| (coeffs.height as usize * c.v_sampling as usize).div_ceil(max_v * 8))
1156        .collect();
1157
1158    let mut prev_dc: Vec<i16> = vec![0; coeffs.components.len()];
1159    let ri: u32 = coeffs.restart_interval as u32;
1160    let mut mcu_count: u32 = 0;
1161
1162    for mcu_y in 0..mcus_y {
1163        for mcu_x in 0..mcus_x {
1164            if ri > 0 && mcu_count > 0 && mcu_count.is_multiple_of(ri) {
1165                prev_dc.fill(0);
1166            }
1167
1168            for (ci, comp) in coeffs.components.iter().enumerate() {
1169                for v in 0..comp.v_sampling as usize {
1170                    for h in 0..comp.h_sampling as usize {
1171                        let bx: usize = mcu_x * comp.h_sampling as usize + h;
1172                        let by: usize = mcu_y * comp.v_sampling as usize + v;
1173                        let is_dummy: bool = bx >= data_blocks_x[ci] || by >= data_blocks_y[ci];
1174
1175                        if is_dummy {
1176                            continue;
1177                        }
1178
1179                        let block: &[i16; 64] = &comp.blocks[by * comp.blocks_x + bx];
1180                        let dc_diff: i16 = block[0].wrapping_sub(prev_dc[ci]);
1181                        prev_dc[ci] = block[0];
1182
1183                        if huffman_category(dc_diff) > 11 {
1184                            return true;
1185                        }
1186
1187                        if block[1..]
1188                            .iter()
1189                            .any(|&coef| coef != 0 && huffman_category(coef) > 10)
1190                        {
1191                            return true;
1192                        }
1193                    }
1194                }
1195            }
1196            mcu_count += 1;
1197        }
1198    }
1199
1200    false
1201}
1202
1203/// Write DCT coefficients with optimized Huffman tables (2-pass encoding).
1204///
1205/// Pass 1 gathers symbol frequencies from the coefficient data, then
1206/// generates optimal Huffman tables. Pass 2 encodes with those tables.
1207pub fn write_coefficients_optimized(coeffs: &JpegCoefficients) -> Result<Vec<u8>> {
1208    use crate::encode::huff_opt;
1209
1210    let num_components: usize = coeffs.components.len();
1211    let is_grayscale: bool = num_components == 1;
1212
1213    let opt_max_h: usize = coeffs
1214        .components
1215        .iter()
1216        .map(|c| c.h_sampling as usize)
1217        .max()
1218        .unwrap_or(1);
1219    let opt_max_v: usize = coeffs
1220        .components
1221        .iter()
1222        .map(|c| c.v_sampling as usize)
1223        .max()
1224        .unwrap_or(1);
1225    let mcus_x: usize = coeffs.components[0].blocks_x / coeffs.components[0].h_sampling as usize;
1226    let mcus_y: usize = coeffs.components[0].blocks_y / coeffs.components[0].v_sampling as usize;
1227
1228    let opt_data_bx: Vec<usize> = coeffs
1229        .components
1230        .iter()
1231        .map(|c| (coeffs.width as usize * c.h_sampling as usize).div_ceil(opt_max_h * 8))
1232        .collect();
1233    let opt_data_by: Vec<usize> = coeffs
1234        .components
1235        .iter()
1236        .map(|c| (coeffs.height as usize * c.v_sampling as usize).div_ceil(opt_max_v * 8))
1237        .collect();
1238
1239    // === Pass 1: gather symbol frequencies ===
1240    let mut dc_luma_freq = [0u32; 257];
1241    let mut dc_chroma_freq = [0u32; 257];
1242    let mut ac_luma_freq = [0u32; 257];
1243    let mut ac_chroma_freq = [0u32; 257];
1244
1245    let mut prev_dc: Vec<i16> = vec![0i16; num_components];
1246    let opt_dummy: [i16; 64] = [0i16; 64];
1247    let p1_ri: u32 = coeffs.restart_interval as u32;
1248    let mut p1_mcu_count: u32 = 0;
1249
1250    for mcu_y in 0..mcus_y {
1251        for mcu_x in 0..mcus_x {
1252            // Reset DC predictions at restart boundaries (matching Pass 2)
1253            if p1_ri > 0 && p1_mcu_count > 0 && p1_mcu_count.is_multiple_of(p1_ri) {
1254                for dc in prev_dc.iter_mut() {
1255                    *dc = 0;
1256                }
1257            }
1258
1259            for (ci, comp) in coeffs.components.iter().enumerate() {
1260                let dc_freq: &mut [u32; 257] = if ci == 0 {
1261                    &mut dc_luma_freq
1262                } else {
1263                    &mut dc_chroma_freq
1264                };
1265                let ac_freq: &mut [u32; 257] = if ci == 0 {
1266                    &mut ac_luma_freq
1267                } else {
1268                    &mut ac_chroma_freq
1269                };
1270
1271                for v in 0..comp.v_sampling as usize {
1272                    for h in 0..comp.h_sampling as usize {
1273                        let bx: usize = mcu_x * comp.h_sampling as usize + h;
1274                        let by: usize = mcu_y * comp.v_sampling as usize + v;
1275                        let is_dummy: bool = bx >= opt_data_bx[ci] || by >= opt_data_by[ci];
1276
1277                        let block: &[i16; 64] = if is_dummy {
1278                            &opt_dummy
1279                        } else {
1280                            let block_idx: usize = by * comp.blocks_x + bx;
1281                            &comp.blocks[block_idx]
1282                        };
1283
1284                        let dc_val: i16 = if is_dummy { prev_dc[ci] } else { block[0] };
1285                        // wrapping_sub: corrupt/adversarial input can pair DCs
1286                        // whose difference exceeds i16; wrap matches the
1287                        // baseline-encoder convention (huffman_encode.rs:461/495)
1288                        // and gather_dc_symbol's leading-zeros classification.
1289                        let diff: i16 = dc_val.wrapping_sub(prev_dc[ci]);
1290                        prev_dc[ci] = dc_val;
1291                        // Magnitude category 16 cannot be expressed in a DHT
1292                        // symbol (4-bit size field); in i16 storage only a
1293                        // value/diff of -32768 produces it. C's scalar encoder
1294                        // rejects it with ERREXIT(JERR_BAD_DCT_COEF)
1295                        // (jchuff.c); its SIMD path silently emits an
1296                        // undecodable stream instead — match the scalar
1297                        // contract (Fuzz Smoke run 30064906856, P4-35).
1298                        //
1299                        // Deliberate leniency vs C: C computes the DC diff in
1300                        // int and ERREXITs when the *pre-wrap* magnitude needs
1301                        // category 16 (e.g. 32767 - (-2) = 32769). Our wrapped
1302                        // diff stays representable, pass 2 wraps identically,
1303                        // and the decoder's own predictor wrap recovers the
1304                        // same i16 DC values — the output is valid and
1305                        // decodable, so we transcode where C refuses. Only the
1306                        // wrapped value -32768 (true category 16) must reject.
1307                        if diff == i16::MIN || block[1..].contains(&i16::MIN) {
1308                            return Err(JpegError::CorruptData(
1309                                "DCT coefficient out of range for Huffman coding".to_string(),
1310                            ));
1311                        }
1312                        huff_opt::gather_dc_symbol(diff, dc_freq);
1313                        huff_opt::gather_ac_symbols(block, ac_freq);
1314                    }
1315                }
1316            }
1317            p1_mcu_count += 1;
1318        }
1319    }
1320
1321    // Add pseudo-symbol (required by Annex K.2 optimal table generation).
1322    dc_luma_freq[256] = 1;
1323    ac_luma_freq[256] = 1;
1324    dc_chroma_freq[256] = 1;
1325    ac_chroma_freq[256] = 1;
1326
1327    // Generate optimal tables.
1328    let (dc_luma_bits, dc_luma_values) = huff_opt::gen_optimal_table(&dc_luma_freq);
1329    let (ac_luma_bits, ac_luma_values) = huff_opt::gen_optimal_table(&ac_luma_freq);
1330    let (dc_chroma_bits, dc_chroma_values) = huff_opt::gen_optimal_table(&dc_chroma_freq);
1331    let (ac_chroma_bits, ac_chroma_values) = huff_opt::gen_optimal_table(&ac_chroma_freq);
1332
1333    // Build encoding tables from optimal bits/values.
1334    let dc_luma_table = build_huff_table(&dc_luma_bits, &dc_luma_values);
1335    let ac_luma_table = build_huff_table(&ac_luma_bits, &ac_luma_values);
1336    let dc_chroma_table = build_huff_table(&dc_chroma_bits, &dc_chroma_values);
1337    let ac_chroma_table = build_huff_table(&ac_chroma_bits, &ac_chroma_values);
1338
1339    // === Pass 2: entropy encode with optimal tables ===
1340    let mut bit_writer = BitWriter::new(coeffs.width as usize * coeffs.height as usize);
1341    let mut prev_dc_pass2: Vec<i16> = vec![0i16; num_components];
1342    let opt_ri: u32 = coeffs.restart_interval as u32;
1343    let mut opt_mcu_count: u32 = 0;
1344    let mut opt_restart_idx: u8 = 0;
1345
1346    for mcu_y in 0..mcus_y {
1347        for mcu_x in 0..mcus_x {
1348            if opt_ri > 0 && opt_mcu_count > 0 && opt_mcu_count.is_multiple_of(opt_ri) {
1349                bit_writer.flush();
1350                bit_writer.write_restart_marker(opt_restart_idx);
1351                opt_restart_idx = (opt_restart_idx + 1) & 7;
1352                for dc in prev_dc_pass2.iter_mut() {
1353                    *dc = 0;
1354                }
1355            }
1356
1357            for (ci, comp) in coeffs.components.iter().enumerate() {
1358                let dc_table = if ci == 0 {
1359                    &dc_luma_table
1360                } else {
1361                    &dc_chroma_table
1362                };
1363                let ac_table = if ci == 0 {
1364                    &ac_luma_table
1365                } else {
1366                    &ac_chroma_table
1367                };
1368
1369                for v in 0..comp.v_sampling as usize {
1370                    for h in 0..comp.h_sampling as usize {
1371                        let bx: usize = mcu_x * comp.h_sampling as usize + h;
1372                        let by: usize = mcu_y * comp.v_sampling as usize + v;
1373                        let is_dummy: bool = bx >= opt_data_bx[ci] || by >= opt_data_by[ci];
1374
1375                        if is_dummy {
1376                            let mut dblock: [i16; 64] = opt_dummy;
1377                            dblock[0] = prev_dc_pass2[ci];
1378                            HuffmanEncoder::encode_block(
1379                                &mut bit_writer,
1380                                &dblock,
1381                                &mut prev_dc_pass2[ci],
1382                                dc_table,
1383                                ac_table,
1384                            );
1385                        } else {
1386                            let block_idx: usize = by * comp.blocks_x + bx;
1387                            let block: &[i16; 64] = &comp.blocks[block_idx];
1388                            HuffmanEncoder::encode_block(
1389                                &mut bit_writer,
1390                                block,
1391                                &mut prev_dc_pass2[ci],
1392                                dc_table,
1393                                ac_table,
1394                            );
1395                        }
1396                    }
1397                }
1398            }
1399            opt_mcu_count += 1;
1400        }
1401    }
1402
1403    bit_writer.flush();
1404
1405    // === Assemble output ===
1406    let mut output: Vec<u8> = Vec::with_capacity(bit_writer.data().len() + 1024);
1407
1408    marker_writer::write_soi(&mut output);
1409    marker_writer::write_app0_jfif_with_density(
1410        &mut output,
1411        coeffs.density_unit,
1412        coeffs.x_density,
1413        coeffs.y_density,
1414    );
1415
1416    // Quantization tables.
1417    for (i, qt) in coeffs.quant_tables.iter().enumerate() {
1418        marker_writer::write_dqt(&mut output, i as u8, qt);
1419    }
1420
1421    // Frame header — SOF1 for 16-bit quant tables OR sample precision > 8
1422    // (e.g. 12-bit `monkey12.jpg` transcode), SOF0 otherwise.
1423    let opt_precision: u8 = coeffs.effective_precision();
1424    let opt_needs_ext: bool = opt_precision > 8
1425        || coeffs
1426            .quant_tables
1427            .iter()
1428            .any(|qt| qt.iter().any(|&v| v > 255));
1429    let opt_comps: Vec<(u8, u8, u8, u8)> = coeffs
1430        .components
1431        .iter()
1432        .map(|c| {
1433            (
1434                c.component_id,
1435                c.h_sampling,
1436                c.v_sampling,
1437                c.quant_table_index,
1438            )
1439        })
1440        .collect();
1441    output.push(0xFF);
1442    output.push(if opt_needs_ext { 0xC1 } else { 0xC0 });
1443    let opt_sof_len: u16 = 2 + 1 + 2 + 2 + 1 + (opt_comps.len() as u16 * 3);
1444    output.extend_from_slice(&opt_sof_len.to_be_bytes());
1445    output.push(opt_precision);
1446    output.extend_from_slice(&coeffs.height.to_be_bytes());
1447    output.extend_from_slice(&coeffs.width.to_be_bytes());
1448    output.push(opt_comps.len() as u8);
1449    for &(id, h_samp, v_samp, quant_tbl_id) in &opt_comps {
1450        output.push(id);
1451        output.push((h_samp << 4) | v_samp);
1452        output.push(quant_tbl_id);
1453    }
1454
1455    // Optimized Huffman tables.
1456    marker_writer::write_dht(&mut output, 0, 0, &dc_luma_bits, &dc_luma_values);
1457    marker_writer::write_dht(&mut output, 1, 0, &ac_luma_bits, &ac_luma_values);
1458    if !is_grayscale {
1459        marker_writer::write_dht(&mut output, 0, 1, &dc_chroma_bits, &dc_chroma_values);
1460        marker_writer::write_dht(&mut output, 1, 1, &ac_chroma_bits, &ac_chroma_values);
1461    }
1462
1463    // DRI (restart interval)
1464    if coeffs.restart_interval > 0 {
1465        marker_writer::write_dri(&mut output, coeffs.restart_interval);
1466    }
1467
1468    // Scan header — preserve source component IDs.
1469    let scan_components: Vec<(u8, u8, u8)> = coeffs
1470        .components
1471        .iter()
1472        .enumerate()
1473        .map(|(i, c)| {
1474            let tbl: u8 = if i == 0 { 0u8 } else { 1u8 };
1475            (c.component_id, tbl, tbl)
1476        })
1477        .collect();
1478    marker_writer::write_sos(&mut output, &scan_components);
1479
1480    output.extend_from_slice(bit_writer.data());
1481    marker_writer::write_eoi(&mut output);
1482
1483    Ok(output)
1484}
1485
1486/// Write DCT coefficients as progressive JPEG (SOF2, multi-scan) with
1487/// per-scan optimized Huffman tables.
1488///
1489/// Matches C `jpegtran -progressive` behavior, which implies `-optimize`.
1490/// Uses the default libjpeg-turbo scan progression (simple_progression).
1491///
1492/// `restart_rows` selects the restart accounting mode:
1493/// - `Some(rows)` — row mode (`jpegtran -restart N`): the DRI is recomputed
1494///   per scan as `rows * MCUs_per_row_of_scan`, where `MCUs_per_row` is the
1495///   interleaved MCU count for multi-component scans and `width_in_blocks`
1496///   for non-interleaved AC scans (matches C `per_scan_setup`).
1497/// - `None` — byte mode (`-restart Nb`) or source-preserved RI: `coeffs.restart_interval`
1498///   is used uniformly for every scan.
1499pub fn write_coefficients_progressive(
1500    coeffs: &JpegCoefficients,
1501    restart_rows: Option<u16>,
1502) -> Result<Vec<u8>> {
1503    use crate::encode::huff_opt;
1504    use crate::encode::progressive::simple_progression;
1505
1506    let num_components: usize = coeffs.components.len();
1507    let is_grayscale: bool = num_components == 1;
1508
1509    let max_h: usize = coeffs
1510        .components
1511        .iter()
1512        .map(|c| c.h_sampling as usize)
1513        .max()
1514        .unwrap_or(1);
1515    let max_v: usize = coeffs
1516        .components
1517        .iter()
1518        .map(|c| c.v_sampling as usize)
1519        .max()
1520        .unwrap_or(1);
1521    let mcus_x: usize = coeffs.components[0].blocks_x / coeffs.components[0].h_sampling as usize;
1522    let mcus_y: usize = coeffs.components[0].blocks_y / coeffs.components[0].v_sampling as usize;
1523
1524    // Per-component actual block counts for non-interleaved AC scans.
1525    // C libjpeg-turbo only encodes width_in_blocks × height_in_blocks data
1526    // units for non-interleaved scans, not the MCU-padded count.
1527    let data_blocks_x: Vec<usize> = coeffs
1528        .components
1529        .iter()
1530        .map(|c| (coeffs.width as usize * c.h_sampling as usize).div_ceil(max_h * 8))
1531        .collect();
1532    let data_blocks_y: Vec<usize> = coeffs
1533        .components
1534        .iter()
1535        .map(|c| (coeffs.height as usize * c.v_sampling as usize).div_ceil(max_v * 8))
1536        .collect();
1537
1538    let scans = simple_progression(num_components);
1539
1540    // === Assemble output header ===
1541    let mut output: Vec<u8> = Vec::with_capacity(coeffs.width as usize * coeffs.height as usize);
1542
1543    marker_writer::write_soi(&mut output);
1544    marker_writer::write_app0_jfif_with_density(
1545        &mut output,
1546        coeffs.density_unit,
1547        coeffs.x_density,
1548        coeffs.y_density,
1549    );
1550
1551    for (i, qt) in coeffs.quant_tables.iter().enumerate() {
1552        marker_writer::write_dqt(&mut output, i as u8, qt);
1553    }
1554
1555    // SOF2 (progressive)
1556    let components: Vec<(u8, u8, u8, u8)> = coeffs
1557        .components
1558        .iter()
1559        .map(|c| {
1560            (
1561                c.component_id,
1562                c.h_sampling,
1563                c.v_sampling,
1564                c.quant_table_index,
1565            )
1566        })
1567        .collect();
1568    marker_writer::write_sof2_with_precision(
1569        &mut output,
1570        coeffs.width,
1571        coeffs.height,
1572        &components,
1573        coeffs.effective_precision(),
1574    );
1575
1576    let mut bit_writer: BitWriter =
1577        BitWriter::new(coeffs.width as usize * coeffs.height as usize / 4);
1578
1579    // DRI is emitted per-scan, after DHT and before SOS — only when the
1580    // restart interval changes from the previous scan (matches C jcmarker.c
1581    // `write_scan_header`). `saved_ri` starts at 0 so the first scan emits
1582    // DRI whenever the image has restart markers.
1583    let mut saved_ri: u16 = 0;
1584
1585    // Compute the per-scan DRI used for RST emission and stream markers.
1586    // Row mode follows C `per_scan_setup`: interleaved (multi-component)
1587    // scans use the interleaved MCU grid; non-interleaved scans use that
1588    // component's `width_in_blocks`. Byte mode applies `coeffs.restart_interval`
1589    // uniformly.
1590    let per_scan_ri = |scan_ci: &[usize]| -> u16 {
1591        match restart_rows {
1592            Some(rows) => {
1593                let mcus_per_row: usize = if scan_ci.len() == 1 {
1594                    // Non-interleaved: MCU row count = component's width in blocks.
1595                    let ci: usize = scan_ci[0];
1596                    let comp = &coeffs.components[ci];
1597                    (coeffs.width as usize * comp.h_sampling as usize).div_ceil(max_h * 8)
1598                } else {
1599                    // Interleaved: width divided by max horizontal sampling × 8.
1600                    (coeffs.width as usize).div_ceil(max_h * 8)
1601                };
1602                let dri: usize = rows as usize * mcus_per_row;
1603                dri.min(u16::MAX as usize) as u16
1604            }
1605            None => coeffs.restart_interval,
1606        }
1607    };
1608
1609    // === Encode each scan with per-scan optimized Huffman tables ===
1610    for scan in &scans {
1611        let is_dc_scan: bool = scan.ss == 0 && scan.se == 0;
1612        let is_first: bool = scan.ah == 0;
1613        let scan_ri: u16 = per_scan_ri(&scan.component_indices);
1614
1615        // Build SOS component list preserving source component IDs.
1616        // DC refine scans (ah>0) use no Huffman table — set Td=0 to match C.
1617        let scan_comps: Vec<(u8, u8, u8)> = scan
1618            .component_indices
1619            .iter()
1620            .map(|&ci| {
1621                let tbl: u8 = if ci == 0 { 0 } else { 1 };
1622                let dc_tbl: u8 = if is_dc_scan && is_first { tbl } else { 0 };
1623                let ac_tbl: u8 = if is_dc_scan { 0 } else { tbl };
1624                (coeffs.components[ci].component_id, dc_tbl, ac_tbl)
1625            })
1626            .collect();
1627
1628        if is_dc_scan && is_first {
1629            // === DC FIRST scan ===
1630            // Pass 1: gather DC symbol frequencies.
1631            let mut dc_luma_freq = [0u32; 257];
1632            let mut dc_chroma_freq = [0u32; 257];
1633            dc_luma_freq[256] = 1;
1634            dc_chroma_freq[256] = 1;
1635
1636            let mut prev_dc: Vec<i16> = vec![0i16; scan.component_indices.len()];
1637            let ri: u32 = scan_ri as u32;
1638            let mut restarts_to_go: u32 = ri;
1639
1640            for mcu_y in 0..mcus_y {
1641                for mcu_x in 0..mcus_x {
1642                    if ri > 0 && restarts_to_go == 0 {
1643                        for dc in prev_dc.iter_mut() {
1644                            *dc = 0;
1645                        }
1646                        restarts_to_go = ri;
1647                    }
1648                    for (scan_ci, &ci) in scan.component_indices.iter().enumerate() {
1649                        let comp = &coeffs.components[ci];
1650                        let freq: &mut [u32; 257] = if ci == 0 {
1651                            &mut dc_luma_freq
1652                        } else {
1653                            &mut dc_chroma_freq
1654                        };
1655                        for v in 0..comp.v_sampling as usize {
1656                            for h in 0..comp.h_sampling as usize {
1657                                let bx: usize = mcu_x * comp.h_sampling as usize + h;
1658                                let by: usize = mcu_y * comp.v_sampling as usize + v;
1659                                let is_dummy: bool =
1660                                    bx >= data_blocks_x[ci] || by >= data_blocks_y[ci];
1661                                let dc: i16 = if is_dummy {
1662                                    prev_dc[scan_ci]
1663                                } else {
1664                                    let block: &[i16; 64] = &comp.blocks[by * comp.blocks_x + bx];
1665                                    block[0] >> scan.al
1666                                };
1667                                let diff: i16 = dc.wrapping_sub(prev_dc[scan_ci]);
1668                                prev_dc[scan_ci] = dc;
1669                                // DC diff of -32768 needs magnitude category 16,
1670                                // which no DHT symbol can express — C's scalar
1671                                // encoder ERREXITs (JERR_BAD_DCT_COEF); match it
1672                                // (P4-35).
1673                                if diff == i16::MIN {
1674                                    return Err(JpegError::CorruptData(
1675                                        "DCT coefficient out of range for Huffman coding"
1676                                            .to_string(),
1677                                    ));
1678                                }
1679                                huff_opt::gather_dc_symbol(diff, freq);
1680                            }
1681                        }
1682                    }
1683                    if ri > 0 {
1684                        restarts_to_go -= 1;
1685                    }
1686                }
1687            }
1688
1689            // Generate optimal tables and write DHT markers.
1690            let (dc_luma_bits, dc_luma_values) = huff_opt::gen_optimal_table(&dc_luma_freq);
1691            let dc_luma_table: HuffTable = build_huff_table(&dc_luma_bits, &dc_luma_values);
1692            marker_writer::write_dht(&mut output, 0, 0, &dc_luma_bits, &dc_luma_values);
1693
1694            let dc_chroma_table: HuffTable = if !is_grayscale {
1695                let (bits, vals) = huff_opt::gen_optimal_table(&dc_chroma_freq);
1696                marker_writer::write_dht(&mut output, 0, 1, &bits, &vals);
1697                build_huff_table(&bits, &vals)
1698            } else {
1699                // Unused for grayscale.
1700                build_huff_table(&tables::DC_CHROMINANCE_BITS, &tables::DC_CHROMINANCE_VALUES)
1701            };
1702
1703            if scan_ri != saved_ri {
1704                marker_writer::write_dri(&mut output, scan_ri);
1705                saved_ri = scan_ri;
1706            }
1707            marker_writer::write_sos_progressive(
1708                &mut output,
1709                &scan_comps,
1710                scan.ss,
1711                scan.se,
1712                scan.ah,
1713                scan.al,
1714            );
1715
1716            // Pass 2: encode DC first scan.
1717            bit_writer.reset();
1718            let mut enc_prev_dc: Vec<i16> = vec![0i16; scan.component_indices.len()];
1719            let ri: u32 = scan_ri as u32;
1720            let mut restarts_to_go: u32 = ri;
1721            let mut next_restart_num: u8 = 0;
1722
1723            for mcu_y in 0..mcus_y {
1724                for mcu_x in 0..mcus_x {
1725                    if ri > 0 && restarts_to_go == 0 {
1726                        bit_writer.flush_restart();
1727                        bit_writer.write_restart_marker(next_restart_num);
1728                        next_restart_num = (next_restart_num + 1) & 7;
1729                        for dc in enc_prev_dc.iter_mut() {
1730                            *dc = 0;
1731                        }
1732                        restarts_to_go = ri;
1733                    }
1734                    for (scan_ci, &ci) in scan.component_indices.iter().enumerate() {
1735                        let comp = &coeffs.components[ci];
1736                        let dc_table: &HuffTable = if ci == 0 {
1737                            &dc_luma_table
1738                        } else {
1739                            &dc_chroma_table
1740                        };
1741                        for v in 0..comp.v_sampling as usize {
1742                            for h in 0..comp.h_sampling as usize {
1743                                let bx: usize = mcu_x * comp.h_sampling as usize + h;
1744                                let by: usize = mcu_y * comp.v_sampling as usize + v;
1745                                let is_dummy: bool =
1746                                    bx >= data_blocks_x[ci] || by >= data_blocks_y[ci];
1747                                let dc: i16 = if is_dummy {
1748                                    enc_prev_dc[scan_ci]
1749                                } else {
1750                                    let block: &[i16; 64] = &comp.blocks[by * comp.blocks_x + bx];
1751                                    block[0] >> scan.al
1752                                };
1753                                let diff: i16 = dc.wrapping_sub(enc_prev_dc[scan_ci]);
1754                                enc_prev_dc[scan_ci] = dc;
1755                                HuffmanEncoder::encode_dc_only(&mut bit_writer, diff, dc_table);
1756                            }
1757                        }
1758                    }
1759                    if ri > 0 {
1760                        restarts_to_go -= 1;
1761                    }
1762                }
1763            }
1764
1765            bit_writer.flush();
1766            output.extend_from_slice(bit_writer.data());
1767        } else if is_dc_scan {
1768            // === DC REFINE scan ===
1769            // No Huffman table needed — just raw bits.
1770            if scan_ri != saved_ri {
1771                marker_writer::write_dri(&mut output, scan_ri);
1772                saved_ri = scan_ri;
1773            }
1774            marker_writer::write_sos_progressive(
1775                &mut output,
1776                &scan_comps,
1777                scan.ss,
1778                scan.se,
1779                scan.ah,
1780                scan.al,
1781            );
1782
1783            bit_writer.reset();
1784            // Track last real DC for dummy block refine bits.
1785            let mut refine_prev_dc: Vec<i16> = vec![0i16; num_components];
1786            let ri: u32 = scan_ri as u32;
1787            let mut restarts_to_go: u32 = ri;
1788            let mut next_restart_num: u8 = 0;
1789
1790            for mcu_y in 0..mcus_y {
1791                for mcu_x in 0..mcus_x {
1792                    if ri > 0 && restarts_to_go == 0 {
1793                        bit_writer.flush_restart();
1794                        bit_writer.write_restart_marker(next_restart_num);
1795                        next_restart_num = (next_restart_num + 1) & 7;
1796                        restarts_to_go = ri;
1797                    }
1798                    for &ci in &scan.component_indices {
1799                        let comp = &coeffs.components[ci];
1800                        for v in 0..comp.v_sampling as usize {
1801                            for h in 0..comp.h_sampling as usize {
1802                                let bx: usize = mcu_x * comp.h_sampling as usize + h;
1803                                let by: usize = mcu_y * comp.v_sampling as usize + v;
1804                                let is_dummy: bool =
1805                                    bx >= data_blocks_x[ci] || by >= data_blocks_y[ci];
1806                                let dc_val: i16 = if is_dummy {
1807                                    refine_prev_dc[ci]
1808                                } else {
1809                                    let block: &[i16; 64] = &comp.blocks[by * comp.blocks_x + bx];
1810                                    refine_prev_dc[ci] = block[0];
1811                                    block[0]
1812                                };
1813                                let bit: u32 = ((dc_val >> scan.al) & 1) as u32;
1814                                bit_writer.put_bits(bit, 1);
1815                            }
1816                        }
1817                    }
1818                    if ri > 0 {
1819                        restarts_to_go -= 1;
1820                    }
1821                }
1822            }
1823
1824            bit_writer.flush();
1825            output.extend_from_slice(bit_writer.data());
1826        } else {
1827            // === AC scan (single component, non-interleaved) ===
1828            let ci: usize = scan.component_indices[0];
1829            let comp = &coeffs.components[ci];
1830            let wib: usize = data_blocks_x[ci].min(comp.blocks_x);
1831            let hib: usize = data_blocks_y[ci].min(comp.blocks_y);
1832            let stride: usize = comp.blocks_x;
1833            let ss: usize = scan.ss as usize;
1834            let se: usize = scan.se as usize;
1835            let al: u8 = scan.al;
1836            let band_len: usize = se - ss + 1;
1837
1838            if is_first {
1839                // --- AC first scan ---
1840                // Pass 1: gather AC symbol frequencies.
1841                let mut ac_freq = [0u32; 257];
1842                ac_freq[256] = 1;
1843                let mut eobrun_gather: u32 = 0;
1844                let ri: u32 = scan_ri as u32;
1845                let mut restarts_to_go: u32 = ri;
1846
1847                for by in 0..hib {
1848                    for bx in 0..wib {
1849                        if ri > 0 && restarts_to_go == 0 {
1850                            if eobrun_gather > 0 {
1851                                let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
1852                                ac_freq[(nbits as usize) << 4] += 1;
1853                                eobrun_gather = 0;
1854                            }
1855                            restarts_to_go = ri;
1856                        }
1857                        let block: &[i16; 64] = &comp.blocks[by * stride + bx];
1858
1859                        let mut zerobits: u64 = 0;
1860                        let mut values = [0u16; 64];
1861
1862                        for i in 0..band_len {
1863                            let coeff: i16 = block[ss + i];
1864                            if coeff == 0 {
1865                                continue;
1866                            }
1867                            // i32 widen to handle adversarial coeff = i16::MIN:
1868                            // |i16::MIN| = 32768 doesn't fit in i16 (the
1869                            // branchless abs `(c ^ -1) - -1 = ~c + 1`
1870                            // overflowed). Found via fuzz_transform_options
1871                            // round-3 (CI run 25215431132) at coefficient.rs:1696.
1872                            let coeff: i32 = coeff as i32;
1873                            let sign_mask: i32 = coeff >> 31;
1874                            let abs_coeff: i32 = (coeff ^ sign_mask) - sign_mask;
1875                            let temp: u16 = (abs_coeff >> al) as u16;
1876                            if temp == 0 {
1877                                continue;
1878                            }
1879                            // temp = 32768 (coeff = i16::MIN with al = 0) needs
1880                            // magnitude category 16, which no DHT symbol can
1881                            // express — C's scalar encoder ERREXITs
1882                            // (JERR_BAD_DCT_COEF); match it (P4-35).
1883                            if temp >= 0x8000 {
1884                                return Err(JpegError::CorruptData(
1885                                    "DCT coefficient out of range for Huffman coding".to_string(),
1886                                ));
1887                            }
1888                            values[i] = temp;
1889                            zerobits |= 1u64 << i;
1890                        }
1891
1892                        if zerobits == 0 {
1893                            eobrun_gather += 1;
1894                            if eobrun_gather == 0x7FFF {
1895                                let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
1896                                ac_freq[(nbits as usize) << 4] += 1;
1897                                eobrun_gather = 0;
1898                            }
1899                            if ri > 0 {
1900                                restarts_to_go -= 1;
1901                            }
1902                            continue;
1903                        }
1904
1905                        if eobrun_gather > 0 {
1906                            let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
1907                            ac_freq[(nbits as usize) << 4] += 1;
1908                            eobrun_gather = 0;
1909                        }
1910
1911                        let mut prev_pos: usize = 0;
1912                        let mut bits: u64 = zerobits;
1913                        while bits != 0 {
1914                            let pos: usize = bits.trailing_zeros() as usize;
1915                            bits &= bits - 1;
1916                            let mut zero_run: usize = pos - prev_pos;
1917                            while zero_run >= 16 {
1918                                ac_freq[0xF0] += 1;
1919                                zero_run -= 16;
1920                            }
1921                            let nbits: u8 = 16 - values[pos].leading_zeros() as u8;
1922                            let symbol: usize = (zero_run << 4) | (nbits as usize);
1923                            ac_freq[symbol] += 1;
1924                            prev_pos = pos + 1;
1925                        }
1926
1927                        if prev_pos < band_len {
1928                            eobrun_gather += 1;
1929                            if eobrun_gather == 0x7FFF {
1930                                let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
1931                                ac_freq[(nbits as usize) << 4] += 1;
1932                                eobrun_gather = 0;
1933                            }
1934                        }
1935                        if ri > 0 {
1936                            restarts_to_go -= 1;
1937                        }
1938                    }
1939                }
1940                if eobrun_gather > 0 {
1941                    let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
1942                    ac_freq[(nbits as usize) << 4] += 1;
1943                }
1944
1945                // Generate optimal table, write DHT + SOS.
1946                let (ac_bits, ac_values) = huff_opt::gen_optimal_table(&ac_freq);
1947                let table_id: u8 = if ci == 0 { 0 } else { 1 };
1948                marker_writer::write_dht(&mut output, 1, table_id, &ac_bits, &ac_values);
1949                if scan_ri != saved_ri {
1950                    marker_writer::write_dri(&mut output, scan_ri);
1951                    saved_ri = scan_ri;
1952                }
1953                marker_writer::write_sos_progressive(
1954                    &mut output,
1955                    &scan_comps,
1956                    scan.ss,
1957                    scan.se,
1958                    scan.ah,
1959                    scan.al,
1960                );
1961
1962                // Pass 2: encode AC first scan.
1963                let ac_table: HuffTable = build_huff_table(&ac_bits, &ac_values);
1964                bit_writer.reset();
1965                let mut eobrun: u32 = 0;
1966                let ri: u32 = scan_ri as u32;
1967                let mut restarts_to_go: u32 = ri;
1968                let mut next_restart_num: u8 = 0;
1969
1970                for by in 0..hib {
1971                    for bx in 0..wib {
1972                        if ri > 0 && restarts_to_go == 0 {
1973                            if eobrun > 0 {
1974                                encoder_pipeline::emit_eobrun(
1975                                    &ac_table,
1976                                    &mut bit_writer,
1977                                    &mut eobrun,
1978                                );
1979                            }
1980                            bit_writer.flush_restart();
1981                            bit_writer.write_restart_marker(next_restart_num);
1982                            next_restart_num = (next_restart_num + 1) & 7;
1983                            restarts_to_go = ri;
1984                        }
1985                        let block: &[i16; 64] = &comp.blocks[by * stride + bx];
1986                        encoder_pipeline::encode_ac_first_block(
1987                            block,
1988                            ss,
1989                            se,
1990                            al,
1991                            &ac_table,
1992                            &mut bit_writer,
1993                            &mut eobrun,
1994                        );
1995                        if ri > 0 {
1996                            restarts_to_go -= 1;
1997                        }
1998                    }
1999                }
2000                if eobrun > 0 {
2001                    encoder_pipeline::emit_eobrun(&ac_table, &mut bit_writer, &mut eobrun);
2002                }
2003
2004                bit_writer.flush();
2005                output.extend_from_slice(bit_writer.data());
2006            } else {
2007                // --- AC refine scan ---
2008                // Pass 1: gather AC refine symbol frequencies.
2009                let mut ac_freq = [0u32; 257];
2010                ac_freq[256] = 1;
2011                let mut eobrun_gather: u32 = 0;
2012                let mut be: usize = 0;
2013                let ri: u32 = scan_ri as u32;
2014                let mut restarts_to_go: u32 = ri;
2015
2016                for by in 0..hib {
2017                    for bx in 0..wib {
2018                        if ri > 0 && restarts_to_go == 0 {
2019                            if eobrun_gather > 0 {
2020                                let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
2021                                ac_freq[(nbits as usize) << 4] += 1;
2022                            }
2023                            // Match C `emit_restart` (jcphuff.c:444-446): EOBRUN
2024                            // and BE correction buffer are always cleared at an
2025                            // RST boundary regardless of whether EOBRUN was
2026                            // pending. Keeps `be` accounting robust even if the
2027                            // gather/encode invariant `be>0 ⇒ eobrun>0` ever
2028                            // loosens.
2029                            eobrun_gather = 0;
2030                            be = 0;
2031                            restarts_to_go = ri;
2032                        }
2033                        let block: &[i16; 64] = &comp.blocks[by * stride + bx];
2034
2035                        let mut absvals = [0u16; 64];
2036                        let mut eob_pos: usize = 0;
2037
2038                        for i in 0..band_len {
2039                            let coeff: i32 = block[ss + i] as i32;
2040                            let sign_mask: i32 = coeff >> 31;
2041                            let abs_coeff: i32 = (coeff ^ sign_mask) - sign_mask;
2042                            let temp: u16 = (abs_coeff >> al) as u16;
2043                            absvals[i] = temp;
2044                            if temp == 1 {
2045                                eob_pos = i + 1;
2046                            }
2047                        }
2048
2049                        let mut r: usize = 0;
2050                        let mut br: usize = 0;
2051                        let mut idx: usize = 0;
2052
2053                        while idx < band_len {
2054                            let temp: u16 = absvals[idx];
2055
2056                            if temp == 0 {
2057                                r += 1;
2058                                idx += 1;
2059                                continue;
2060                            }
2061
2062                            while r > 15 && idx < eob_pos {
2063                                if eobrun_gather > 0 {
2064                                    let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
2065                                    ac_freq[(nbits as usize) << 4] += 1;
2066                                    eobrun_gather = 0;
2067                                    be = 0;
2068                                }
2069                                ac_freq[0xF0] += 1;
2070                                r -= 16;
2071                                br = 0;
2072                            }
2073
2074                            if temp > 1 {
2075                                br += 1;
2076                                idx += 1;
2077                                continue;
2078                            }
2079
2080                            if eobrun_gather > 0 {
2081                                let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
2082                                ac_freq[(nbits as usize) << 4] += 1;
2083                                eobrun_gather = 0;
2084                                be = 0;
2085                            }
2086                            let symbol: usize = (r << 4) | 1;
2087                            ac_freq[symbol] += 1;
2088                            r = 0;
2089                            br = 0;
2090                            idx += 1;
2091                        }
2092
2093                        if r > 0 || br > 0 {
2094                            eobrun_gather += 1;
2095                            be += br;
2096                            if eobrun_gather == 0x7FFF
2097                                || be > (encoder_pipeline::MAX_CORR_BITS - 64 + 1)
2098                            {
2099                                let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
2100                                ac_freq[(nbits as usize) << 4] += 1;
2101                                eobrun_gather = 0;
2102                                be = 0;
2103                            }
2104                        }
2105                        if ri > 0 {
2106                            restarts_to_go -= 1;
2107                        }
2108                    }
2109                }
2110                if eobrun_gather > 0 {
2111                    let nbits: u8 = (32 - eobrun_gather.leading_zeros()) as u8 - 1;
2112                    ac_freq[(nbits as usize) << 4] += 1;
2113                }
2114
2115                // Generate optimal table, write DHT + SOS.
2116                let (ac_bits, ac_values) = huff_opt::gen_optimal_table(&ac_freq);
2117                let table_id: u8 = if ci == 0 { 0 } else { 1 };
2118                marker_writer::write_dht(&mut output, 1, table_id, &ac_bits, &ac_values);
2119                if scan_ri != saved_ri {
2120                    marker_writer::write_dri(&mut output, scan_ri);
2121                    saved_ri = scan_ri;
2122                }
2123                marker_writer::write_sos_progressive(
2124                    &mut output,
2125                    &scan_comps,
2126                    scan.ss,
2127                    scan.se,
2128                    scan.ah,
2129                    scan.al,
2130                );
2131
2132                // Pass 2: encode AC refine scan.
2133                let ac_table: HuffTable = build_huff_table(&ac_bits, &ac_values);
2134                bit_writer.reset();
2135                let mut eobrun: u32 = 0;
2136                let mut corr_buffer: Vec<u8> = Vec::with_capacity(encoder_pipeline::MAX_CORR_BITS);
2137                let ri: u32 = scan_ri as u32;
2138                let mut restarts_to_go: u32 = ri;
2139                let mut next_restart_num: u8 = 0;
2140
2141                for by in 0..hib {
2142                    for bx in 0..wib {
2143                        if ri > 0 && restarts_to_go == 0 {
2144                            if eobrun > 0 {
2145                                encoder_pipeline::emit_eobrun_with_corr(
2146                                    &ac_table,
2147                                    &mut bit_writer,
2148                                    &mut eobrun,
2149                                    &mut corr_buffer,
2150                                );
2151                            }
2152                            bit_writer.flush_restart();
2153                            bit_writer.write_restart_marker(next_restart_num);
2154                            next_restart_num = (next_restart_num + 1) & 7;
2155                            restarts_to_go = ri;
2156                        }
2157                        let block: &[i16; 64] = &comp.blocks[by * stride + bx];
2158                        encoder_pipeline::encode_ac_refine_block(
2159                            block,
2160                            ss,
2161                            se,
2162                            al,
2163                            &ac_table,
2164                            &mut bit_writer,
2165                            &mut eobrun,
2166                            &mut corr_buffer,
2167                        );
2168                        if ri > 0 {
2169                            restarts_to_go -= 1;
2170                        }
2171                    }
2172                }
2173                if eobrun > 0 {
2174                    encoder_pipeline::emit_eobrun_with_corr(
2175                        &ac_table,
2176                        &mut bit_writer,
2177                        &mut eobrun,
2178                        &mut corr_buffer,
2179                    );
2180                }
2181
2182                bit_writer.flush();
2183                output.extend_from_slice(bit_writer.data());
2184            }
2185        }
2186    }
2187
2188    marker_writer::write_eoi(&mut output);
2189    Ok(output)
2190}
2191
2192/// Write DCT coefficients with arithmetic entropy coding (SOF9).
2193///
2194/// Re-encodes coefficient blocks using the JPEG arithmetic coder, matching
2195/// jpegtran's arithmetic output mode for non-progressive transforms.
2196pub fn write_coefficients_arithmetic(coeffs: &JpegCoefficients) -> Result<Vec<u8>> {
2197    use crate::encode::arithmetic::ArithEncoder;
2198
2199    let num_components: usize = coeffs.components.len();
2200    let is_grayscale: bool = num_components == 1;
2201    let num_arith_tables: usize = if is_grayscale { 1 } else { 2 };
2202
2203    let max_h: usize = coeffs
2204        .components
2205        .iter()
2206        .map(|c| c.h_sampling as usize)
2207        .max()
2208        .unwrap_or(1);
2209    let max_v: usize = coeffs
2210        .components
2211        .iter()
2212        .map(|c| c.v_sampling as usize)
2213        .max()
2214        .unwrap_or(1);
2215    let mcus_x: usize = coeffs.components[0].blocks_x / coeffs.components[0].h_sampling as usize;
2216    let mcus_y: usize = coeffs.components[0].blocks_y / coeffs.components[0].v_sampling as usize;
2217
2218    let data_blocks_x: Vec<usize> = coeffs
2219        .components
2220        .iter()
2221        .map(|c| (coeffs.width as usize * c.h_sampling as usize).div_ceil(max_h * 8))
2222        .collect();
2223    let data_blocks_y: Vec<usize> = coeffs
2224        .components
2225        .iter()
2226        .map(|c| (coeffs.height as usize * c.v_sampling as usize).div_ceil(max_v * 8))
2227        .collect();
2228
2229    let mut arith_enc: ArithEncoder =
2230        ArithEncoder::new(coeffs.width as usize * coeffs.height as usize);
2231    let mut prev_dc: Vec<i16> = vec![0; num_components];
2232    let ri: u32 = coeffs.restart_interval as u32;
2233    let mut mcu_count: u32 = 0;
2234    let mut restart_idx: u8 = 0;
2235
2236    for mcu_y in 0..mcus_y {
2237        for mcu_x in 0..mcus_x {
2238            // Insert RST marker between MCU groups when restart_interval is set.
2239            // Mirrors libjpeg-turbo `jcarith.c` arithmetic restart: flush the
2240            // current entropy state byte-aligned, push `FF Dn`, reset coder
2241            // and DC predictors, then continue with the next group.
2242            if ri > 0 && mcu_count > 0 && mcu_count.is_multiple_of(ri) {
2243                arith_enc.emit_restart(restart_idx);
2244                restart_idx = restart_idx.wrapping_add(1) & 7;
2245                prev_dc.iter_mut().for_each(|v| *v = 0);
2246            }
2247            for (ci, comp) in coeffs.components.iter().enumerate() {
2248                let dc_tbl: usize = arithmetic_table_for_component(ci);
2249                let ac_tbl: usize = arithmetic_table_for_component(ci);
2250
2251                for v in 0..comp.v_sampling as usize {
2252                    for h in 0..comp.h_sampling as usize {
2253                        let bx: usize = mcu_x * comp.h_sampling as usize + h;
2254                        let by: usize = mcu_y * comp.v_sampling as usize + v;
2255
2256                        let mut dummy = [0i16; 64];
2257                        let block: &[i16; 64] =
2258                            if bx >= data_blocks_x[ci] || by >= data_blocks_y[ci] {
2259                                dummy[0] = prev_dc[ci];
2260                                &dummy
2261                            } else {
2262                                let real_block: &[i16; 64] = &comp.blocks[by * comp.blocks_x + bx];
2263                                prev_dc[ci] = real_block[0];
2264                                real_block
2265                            };
2266
2267                        arith_enc.encode_dc_sequential(block, ci, dc_tbl);
2268                        arith_enc.encode_ac_sequential(block, ac_tbl);
2269                    }
2270                }
2271            }
2272            mcu_count += 1;
2273        }
2274    }
2275
2276    arith_enc.finish();
2277
2278    let mut output: Vec<u8> = Vec::with_capacity(arith_enc.data().len() + 1024);
2279
2280    marker_writer::write_soi(&mut output);
2281    marker_writer::write_app0_jfif_with_density(
2282        &mut output,
2283        coeffs.density_unit,
2284        coeffs.x_density,
2285        coeffs.y_density,
2286    );
2287
2288    for (i, qt) in coeffs.quant_tables.iter().enumerate() {
2289        marker_writer::write_dqt(&mut output, i as u8, qt);
2290    }
2291
2292    let components: Vec<(u8, u8, u8, u8)> = coeffs
2293        .components
2294        .iter()
2295        .map(|c| {
2296            (
2297                c.component_id,
2298                c.h_sampling,
2299                c.v_sampling,
2300                c.quant_table_index,
2301            )
2302        })
2303        .collect();
2304    marker_writer::write_sof9_with_precision(
2305        &mut output,
2306        coeffs.width,
2307        coeffs.height,
2308        &components,
2309        coeffs.effective_precision(),
2310    );
2311
2312    let dc_params = [(0u8, 1u8); crate::decode::arithmetic::NUM_ARITH_TBLS];
2313    let ac_params = [5u8; crate::decode::arithmetic::NUM_ARITH_TBLS];
2314    let mut dc_in_use = [false; crate::decode::arithmetic::NUM_ARITH_TBLS];
2315    let mut ac_in_use = [false; crate::decode::arithmetic::NUM_ARITH_TBLS];
2316    for table in 0..num_arith_tables {
2317        dc_in_use[table] = true;
2318        ac_in_use[table] = true;
2319    }
2320    marker_writer::write_dac_selected(&mut output, &dc_in_use, &dc_params, &ac_in_use, &ac_params);
2321
2322    if coeffs.restart_interval > 0 {
2323        marker_writer::write_dri(&mut output, coeffs.restart_interval);
2324    }
2325
2326    let scan_components: Vec<(u8, u8, u8)> = coeffs
2327        .components
2328        .iter()
2329        .enumerate()
2330        .map(|(ci, c)| {
2331            let tbl: u8 = arithmetic_table_for_component(ci) as u8;
2332            (c.component_id, tbl, tbl)
2333        })
2334        .collect();
2335    marker_writer::write_sos(&mut output, &scan_components);
2336
2337    output.extend_from_slice(arith_enc.data());
2338    marker_writer::write_eoi(&mut output);
2339
2340    Ok(output)
2341}
2342
2343/// Write DCT coefficients with arithmetic progressive entropy coding (SOF10).
2344///
2345/// Uses the default libjpeg-turbo progressive scan script and emits the
2346/// arithmetic conditioning marker for each scan, matching libjpeg marker order.
2347///
2348/// `restart_rows` selects the restart accounting mode (mirrors
2349/// `write_coefficients_progressive`):
2350/// - `Some(rows)` — row mode (`jpegtran -restart N`): the DRI is recomputed
2351///   per scan as `rows * MCUs_per_row_of_scan`. Multi-component scans use
2352///   the interleaved MCU grid; non-interleaved AC scans use that
2353///   component's `width_in_blocks`.
2354/// - `None` — byte mode (`-restart Nb`) or source-preserved RI:
2355///   `coeffs.restart_interval` applies uniformly across all scans.
2356pub fn write_coefficients_progressive_arithmetic(
2357    coeffs: &JpegCoefficients,
2358    restart_rows: Option<u16>,
2359) -> Result<Vec<u8>> {
2360    use crate::encode::arithmetic::ArithEncoder;
2361    use crate::encode::progressive::simple_progression;
2362
2363    let num_components: usize = coeffs.components.len();
2364
2365    let max_h: usize = coeffs
2366        .components
2367        .iter()
2368        .map(|c| c.h_sampling as usize)
2369        .max()
2370        .unwrap_or(1);
2371    let max_v: usize = coeffs
2372        .components
2373        .iter()
2374        .map(|c| c.v_sampling as usize)
2375        .max()
2376        .unwrap_or(1);
2377    let mcus_x: usize = coeffs.components[0].blocks_x / coeffs.components[0].h_sampling as usize;
2378    let mcus_y: usize = coeffs.components[0].blocks_y / coeffs.components[0].v_sampling as usize;
2379
2380    let data_blocks_x: Vec<usize> = coeffs
2381        .components
2382        .iter()
2383        .map(|c| (coeffs.width as usize * c.h_sampling as usize).div_ceil(max_h * 8))
2384        .collect();
2385    let data_blocks_y: Vec<usize> = coeffs
2386        .components
2387        .iter()
2388        .map(|c| (coeffs.height as usize * c.v_sampling as usize).div_ceil(max_v * 8))
2389        .collect();
2390
2391    let scans = simple_progression(num_components);
2392    let dc_params = [(0u8, 1u8); crate::decode::arithmetic::NUM_ARITH_TBLS];
2393    let ac_params = [5u8; crate::decode::arithmetic::NUM_ARITH_TBLS];
2394
2395    let mut output: Vec<u8> = Vec::with_capacity(coeffs.width as usize * coeffs.height as usize);
2396
2397    marker_writer::write_soi(&mut output);
2398    marker_writer::write_app0_jfif_with_density(
2399        &mut output,
2400        coeffs.density_unit,
2401        coeffs.x_density,
2402        coeffs.y_density,
2403    );
2404
2405    for (i, qt) in coeffs.quant_tables.iter().enumerate() {
2406        marker_writer::write_dqt(&mut output, i as u8, qt);
2407    }
2408
2409    let components: Vec<(u8, u8, u8, u8)> = coeffs
2410        .components
2411        .iter()
2412        .map(|c| {
2413            (
2414                c.component_id,
2415                c.h_sampling,
2416                c.v_sampling,
2417                c.quant_table_index,
2418            )
2419        })
2420        .collect();
2421    marker_writer::write_sof10_with_precision(
2422        &mut output,
2423        coeffs.width,
2424        coeffs.height,
2425        &components,
2426        coeffs.effective_precision(),
2427    );
2428
2429    let mut arith_enc: ArithEncoder =
2430        ArithEncoder::new(coeffs.width as usize * coeffs.height as usize / 4);
2431
2432    // Per-scan DRI tracking. Mirrors the Huffman progressive writer
2433    // (`write_coefficients_progressive`): row mode recomputes the
2434    // restart interval per scan as `rows * MCUs_per_row_of_scan` where
2435    // multi-component scans use the interleaved MCU grid and
2436    // non-interleaved AC scans use that component's width in blocks.
2437    // Byte mode applies `coeffs.restart_interval` uniformly.
2438    let per_scan_ri = |scan_ci: &[usize]| -> u16 {
2439        match restart_rows {
2440            Some(rows) => {
2441                let mcus_per_row: usize = if scan_ci.len() == 1 {
2442                    let ci: usize = scan_ci[0];
2443                    let comp = &coeffs.components[ci];
2444                    (coeffs.width as usize * comp.h_sampling as usize).div_ceil(max_h * 8)
2445                } else {
2446                    (coeffs.width as usize).div_ceil(max_h * 8)
2447                };
2448                let dri: usize = rows as usize * mcus_per_row;
2449                dri.min(u16::MAX as usize) as u16
2450            }
2451            None => coeffs.restart_interval,
2452        }
2453    };
2454
2455    // Track the last-emitted DRI so we only re-emit the marker when the
2456    // value changes between scans (matches C `jcmarker.c::write_scan_header`).
2457    let mut saved_ri: u16 = 0;
2458
2459    for scan in &scans {
2460        arith_enc.reset();
2461
2462        let is_dc_scan: bool = scan.ss == 0 && scan.se == 0;
2463        let is_first: bool = scan.ah == 0;
2464        let scan_ri: u16 = per_scan_ri(&scan.component_indices);
2465
2466        let scan_components: Vec<(u8, u8, u8)> = scan
2467            .component_indices
2468            .iter()
2469            .map(|&ci| {
2470                let tbl: u8 = arithmetic_table_for_component(ci) as u8;
2471                let dc_tbl: u8 = if is_dc_scan && is_first { tbl } else { 0 };
2472                let ac_tbl: u8 = if scan.se > 0 { tbl } else { 0 };
2473                (coeffs.components[ci].component_id, dc_tbl, ac_tbl)
2474            })
2475            .collect();
2476
2477        let mut dc_in_use = [false; crate::decode::arithmetic::NUM_ARITH_TBLS];
2478        let mut ac_in_use = [false; crate::decode::arithmetic::NUM_ARITH_TBLS];
2479        if is_dc_scan && is_first {
2480            for &ci in &scan.component_indices {
2481                dc_in_use[arithmetic_table_for_component(ci)] = true;
2482            }
2483        }
2484        if scan.se > 0 {
2485            for &ci in &scan.component_indices {
2486                ac_in_use[arithmetic_table_for_component(ci)] = true;
2487            }
2488        }
2489
2490        marker_writer::write_dac_selected(
2491            &mut output,
2492            &dc_in_use,
2493            &dc_params,
2494            &ac_in_use,
2495            &ac_params,
2496        );
2497        // DRI is emitted only when the per-scan restart interval changes
2498        // from what the previous scan installed. `saved_ri == 0` initially,
2499        // so the first scan with restart markers re-emits DRI.
2500        if scan_ri != saved_ri {
2501            marker_writer::write_dri(&mut output, scan_ri);
2502            saved_ri = scan_ri;
2503        }
2504        marker_writer::write_sos_progressive(
2505            &mut output,
2506            &scan_components,
2507            scan.ss,
2508            scan.se,
2509            scan.ah,
2510            scan.al,
2511        );
2512
2513        let ri: u32 = scan_ri as u32;
2514        let mut restarts_to_go: u32 = ri;
2515        let mut next_restart_num: u8 = 0;
2516
2517        if is_dc_scan && is_first {
2518            let mut prev_dc: Vec<i16> = vec![0; num_components];
2519
2520            for mcu_y in 0..mcus_y {
2521                for mcu_x in 0..mcus_x {
2522                    // Mirrors libjpeg-turbo `jcarith.c::encode_mcu_DC_first`:
2523                    // when the per-scan restart counter reaches zero,
2524                    // flush byte-aligned, push FF Dn, reset coder state
2525                    // and DC predictors, then continue the next group.
2526                    if ri > 0 && restarts_to_go == 0 {
2527                        arith_enc.emit_restart(next_restart_num);
2528                        next_restart_num = (next_restart_num + 1) & 7;
2529                        prev_dc.iter_mut().for_each(|v| *v = 0);
2530                        restarts_to_go = ri;
2531                    }
2532                    for &ci in &scan.component_indices {
2533                        let comp = &coeffs.components[ci];
2534                        let dc_tbl: usize = arithmetic_table_for_component(ci);
2535
2536                        for v in 0..comp.v_sampling as usize {
2537                            for h in 0..comp.h_sampling as usize {
2538                                let bx: usize = mcu_x * comp.h_sampling as usize + h;
2539                                let by: usize = mcu_y * comp.v_sampling as usize + v;
2540
2541                                let mut dummy = [0i16; 64];
2542                                let block: &[i16; 64] =
2543                                    if bx >= data_blocks_x[ci] || by >= data_blocks_y[ci] {
2544                                        dummy[0] = prev_dc[ci];
2545                                        &dummy
2546                                    } else {
2547                                        let real_block: &[i16; 64] =
2548                                            &comp.blocks[by * comp.blocks_x + bx];
2549                                        prev_dc[ci] = real_block[0];
2550                                        real_block
2551                                    };
2552
2553                                arith_enc.encode_dc_first(block, ci, dc_tbl, scan.al);
2554                            }
2555                        }
2556                    }
2557                    if ri > 0 {
2558                        restarts_to_go -= 1;
2559                    }
2560                }
2561            }
2562        } else if is_dc_scan {
2563            let mut prev_dc: Vec<i16> = vec![0; num_components];
2564
2565            for mcu_y in 0..mcus_y {
2566                for mcu_x in 0..mcus_x {
2567                    if ri > 0 && restarts_to_go == 0 {
2568                        arith_enc.emit_restart(next_restart_num);
2569                        next_restart_num = (next_restart_num + 1) & 7;
2570                        prev_dc.iter_mut().for_each(|v| *v = 0);
2571                        restarts_to_go = ri;
2572                    }
2573                    for &ci in &scan.component_indices {
2574                        let comp = &coeffs.components[ci];
2575
2576                        for v in 0..comp.v_sampling as usize {
2577                            for h in 0..comp.h_sampling as usize {
2578                                let bx: usize = mcu_x * comp.h_sampling as usize + h;
2579                                let by: usize = mcu_y * comp.v_sampling as usize + v;
2580
2581                                let mut dummy = [0i16; 64];
2582                                let block: &[i16; 64] =
2583                                    if bx >= data_blocks_x[ci] || by >= data_blocks_y[ci] {
2584                                        dummy[0] = prev_dc[ci];
2585                                        &dummy
2586                                    } else {
2587                                        let real_block: &[i16; 64] =
2588                                            &comp.blocks[by * comp.blocks_x + bx];
2589                                        prev_dc[ci] = real_block[0];
2590                                        real_block
2591                                    };
2592
2593                                arith_enc.encode_dc_refine(block, scan.al);
2594                            }
2595                        }
2596                    }
2597                    if ri > 0 {
2598                        restarts_to_go -= 1;
2599                    }
2600                }
2601            }
2602        } else if is_first {
2603            let ci: usize = scan.component_indices[0];
2604            let comp = &coeffs.components[ci];
2605            let ac_tbl: usize = arithmetic_table_for_component(ci);
2606            let wib: usize = data_blocks_x[ci].min(comp.blocks_x);
2607            let hib: usize = data_blocks_y[ci].min(comp.blocks_y);
2608
2609            // Non-interleaved AC scan: each block is one MCU per the
2610            // JPEG spec, so the restart counter applies per block.
2611            for by in 0..hib {
2612                for bx in 0..wib {
2613                    if ri > 0 && restarts_to_go == 0 {
2614                        arith_enc.emit_restart(next_restart_num);
2615                        next_restart_num = (next_restart_num + 1) & 7;
2616                        restarts_to_go = ri;
2617                    }
2618                    let block: &[i16; 64] = &comp.blocks[by * comp.blocks_x + bx];
2619                    arith_enc.encode_ac_first(block, ac_tbl, scan.ss, scan.se, scan.al);
2620                    if ri > 0 {
2621                        restarts_to_go -= 1;
2622                    }
2623                }
2624            }
2625        } else {
2626            let ci: usize = scan.component_indices[0];
2627            let comp = &coeffs.components[ci];
2628            let ac_tbl: usize = arithmetic_table_for_component(ci);
2629            let wib: usize = data_blocks_x[ci].min(comp.blocks_x);
2630            let hib: usize = data_blocks_y[ci].min(comp.blocks_y);
2631
2632            for by in 0..hib {
2633                for bx in 0..wib {
2634                    if ri > 0 && restarts_to_go == 0 {
2635                        arith_enc.emit_restart(next_restart_num);
2636                        next_restart_num = (next_restart_num + 1) & 7;
2637                        restarts_to_go = ri;
2638                    }
2639                    let block: &[i16; 64] = &comp.blocks[by * comp.blocks_x + bx];
2640                    arith_enc.encode_ac_refine(block, ac_tbl, scan.ss, scan.se, scan.al, scan.ah);
2641                    if ri > 0 {
2642                        restarts_to_go -= 1;
2643                    }
2644                }
2645            }
2646        }
2647
2648        arith_enc.finish();
2649        output.extend_from_slice(arith_enc.data());
2650    }
2651
2652    marker_writer::write_eoi(&mut output);
2653    Ok(output)
2654}
2655
2656fn arithmetic_table_for_component(component_index: usize) -> usize {
2657    if component_index == 0 {
2658        0
2659    } else {
2660        1
2661    }
2662}
2663
2664/// Transpose a quantization table (8x8 matrix) in-place.
2665/// Required for dimension-swapping transforms (transpose, rot90, rot270, transverse)
2666/// so that each coefficient position uses the correct quantization value.
2667fn transpose_quant_table(qt: &mut [u16; 64]) {
2668    let mut transposed: [u16; 64] = [0u16; 64];
2669    for row in 0..8 {
2670        for col in 0..8 {
2671            transposed[col * 8 + row] = qt[row * 8 + col];
2672        }
2673    }
2674    *qt = transposed;
2675}
2676
2677/// Convert a block from natural (row-major) order to zigzag order.
2678fn natural_to_zigzag(natural: &[i16; 64]) -> [i16; 64] {
2679    let mut zigzag = [0i16; 64];
2680    for i in 0..64 {
2681        zigzag[NATURAL_ORDER[i]] = natural[i];
2682    }
2683    zigzag
2684}
2685
2686/// Convert all blocks in comp_data from natural to zigzag order.
2687fn convert_all_to_zigzag(comp_data: &mut [ComponentCoefficients]) {
2688    for comp in comp_data.iter_mut() {
2689        for block in &mut comp.blocks {
2690            *block = natural_to_zigzag(block);
2691        }
2692    }
2693}
2694
2695// --- Internal decode helpers ---
2696
2697fn decode_baseline_coefficients(
2698    data: &[u8],
2699    metadata: &JpegMetadata,
2700    comp_data: &mut [ComponentCoefficients],
2701    mcus_x: usize,
2702    mcus_y: usize,
2703) -> Result<()> {
2704    use crate::decode::bitstream::BitReader;
2705    use crate::decode::entropy;
2706
2707    let frame = &metadata.frame;
2708    let scan = &metadata.scan;
2709
2710    let mcu_plan = entropy::resolve_mcu_plan(
2711        frame,
2712        scan,
2713        &metadata.dc_huffman_tables,
2714        &metadata.ac_huffman_tables,
2715    )?;
2716
2717    // Baseline single-scan JPEG must reference every frame component in
2718    // its SOS — otherwise the per-component indexing below would walk off
2719    // the plan. Malformed streams surface a clean CorruptData error
2720    // rather than panicking on `mcu_plan[comp_idx]`.
2721    if mcu_plan.len() != comp_data.len() {
2722        return Err(crate::common::error::JpegError::CorruptData(format!(
2723            "baseline SOS covers {} components but frame has {}",
2724            mcu_plan.len(),
2725            comp_data.len()
2726        )));
2727    }
2728
2729    let entropy_data = &data[metadata.entropy_data_offset..];
2730    let mut bit_reader = BitReader::new(entropy_data);
2731    let mut mcu_decoder = entropy::McuDecoder::new(frame.components.len());
2732    let mut mcu_count: u32 = 0;
2733    let mut coeffs = [0i16; 64];
2734
2735    for mcu_y in 0..mcus_y {
2736        for mcu_x in 0..mcus_x {
2737            if metadata.restart_interval > 0
2738                && mcu_count > 0
2739                && mcu_count.is_multiple_of(metadata.restart_interval as u32)
2740            {
2741                bit_reader.reset();
2742                mcu_decoder.reset();
2743            }
2744
2745            for (comp_idx, comp) in comp_data.iter_mut().enumerate() {
2746                let plan = &mcu_plan[comp_idx];
2747
2748                let h_blocks = frame.components[comp_idx].horizontal_sampling as usize;
2749                let v_blocks = frame.components[comp_idx].vertical_sampling as usize;
2750
2751                for v in 0..v_blocks {
2752                    for h in 0..h_blocks {
2753                        mcu_decoder.decode_block(
2754                            &mut bit_reader,
2755                            plan.comp_idx,
2756                            plan.dc_table,
2757                            plan.ac_table,
2758                            &mut coeffs,
2759                        )?;
2760
2761                        let bx = mcu_x * h_blocks + h;
2762                        let by = mcu_y * v_blocks + v;
2763                        let block_idx = by * comp.blocks_x + bx;
2764                        comp.blocks[block_idx] = coeffs;
2765                    }
2766                }
2767            }
2768
2769            mcu_count += 1;
2770        }
2771    }
2772
2773    Ok(())
2774}
2775
2776fn decode_arithmetic_coefficients(
2777    data: &[u8],
2778    metadata: &JpegMetadata,
2779    comp_data: &mut [ComponentCoefficients],
2780    mcus_x: usize,
2781    mcus_y: usize,
2782) -> Result<()> {
2783    use crate::decode::arithmetic::ArithDecoder;
2784
2785    let frame = &metadata.frame;
2786    let scan = &metadata.scan;
2787
2788    let scan_comps: Vec<(usize, usize, usize)> = scan
2789        .components
2790        .iter()
2791        .map(|sc| {
2792            let comp_idx = frame
2793                .components
2794                .iter()
2795                .position(|fc| fc.id == sc.component_id)
2796                .unwrap_or(0);
2797            (
2798                comp_idx,
2799                sc.dc_table_index as usize,
2800                sc.ac_table_index as usize,
2801            )
2802        })
2803        .collect();
2804
2805    let entropy_data = &data[metadata.entropy_data_offset..];
2806    let mut arith = ArithDecoder::new(entropy_data, 0);
2807
2808    for i in 0..crate::decode::arithmetic::NUM_ARITH_TBLS {
2809        let (l, u) = metadata.arith_dc_params[i];
2810        arith.set_dc_conditioning(i, l, u);
2811        arith.set_ac_conditioning(i, metadata.arith_ac_params[i]);
2812    }
2813
2814    // Pre-extract layout info to avoid borrow conflicts
2815    let layouts: Vec<(usize, usize, usize)> = comp_data
2816        .iter()
2817        .map(|c| (c.h_sampling as usize, c.v_sampling as usize, c.blocks_x))
2818        .collect();
2819    let mut coeffs: [i16; 64];
2820
2821    for mcu_y in 0..mcus_y {
2822        for mcu_x in 0..mcus_x {
2823            for &(comp_idx, dc_tbl, ac_tbl) in &scan_comps {
2824                let (h_blocks, v_blocks, blocks_x) = layouts[comp_idx];
2825
2826                for v in 0..v_blocks {
2827                    for h in 0..h_blocks {
2828                        coeffs = [0i16; 64];
2829                        arith.decode_dc_sequential(&mut coeffs, comp_idx, dc_tbl)?;
2830                        arith.decode_ac_sequential(&mut coeffs, ac_tbl)?;
2831
2832                        let bx = mcu_x * h_blocks + h;
2833                        let by = mcu_y * v_blocks + v;
2834                        let block_idx = by * blocks_x + bx;
2835                        comp_data[comp_idx].blocks[block_idx] = coeffs;
2836                    }
2837                }
2838            }
2839        }
2840    }
2841
2842    Ok(())
2843}
2844
2845/// Decode SOF10 (arithmetic progressive) coefficients.
2846fn decode_arithmetic_progressive_coefficients(
2847    data: &[u8],
2848    metadata: &JpegMetadata,
2849    comp_data: &mut [ComponentCoefficients],
2850    _mcus_x: usize,
2851    _mcus_y: usize,
2852) -> Result<()> {
2853    use crate::decode::arithmetic::ArithDecoder;
2854
2855    let frame = &metadata.frame;
2856
2857    for scan_info in &metadata.scans {
2858        let scan = &scan_info.header;
2859        let ss: u8 = scan.spec_start;
2860        let se: u8 = scan.spec_end;
2861        let ah: u8 = scan.succ_high;
2862        let al: u8 = scan.succ_low;
2863        let is_dc: bool = ss == 0 && se == 0;
2864
2865        let entropy_data: &[u8] = &data[scan_info.data_offset..];
2866        let mut arith: ArithDecoder<'_> = ArithDecoder::new(entropy_data, 0);
2867
2868        // Set arithmetic conditioning parameters (16 slots per NUM_ARITH_TBLS).
2869        for i in 0..crate::decode::arithmetic::NUM_ARITH_TBLS {
2870            let (l, u) = metadata.arith_dc_params[i];
2871            arith.set_dc_conditioning(i, l, u);
2872            arith.set_ac_conditioning(i, metadata.arith_ac_params[i]);
2873        }
2874
2875        let scan_comp_indices: Vec<usize> = scan
2876            .components
2877            .iter()
2878            .map(|sc| {
2879                frame
2880                    .components
2881                    .iter()
2882                    .position(|fc| fc.id == sc.component_id)
2883                    .unwrap_or(0)
2884            })
2885            .collect();
2886
2887        if scan.components.len() > 1 {
2888            // Interleaved DC scan — iterate MCU by MCU
2889            let max_h: usize = frame
2890                .components
2891                .iter()
2892                .map(|c| c.horizontal_sampling as usize)
2893                .max()
2894                .unwrap_or(1);
2895            let max_v: usize = frame
2896                .components
2897                .iter()
2898                .map(|c| c.vertical_sampling as usize)
2899                .max()
2900                .unwrap_or(1);
2901            let mcu_w: usize = max_h * 8;
2902            let mcu_h: usize = max_v * 8;
2903            let mcus_x: usize = (frame.width as usize).div_ceil(mcu_w);
2904            let mcus_y: usize = (frame.height as usize).div_ceil(mcu_h);
2905
2906            for _mcu_y in 0..mcus_y {
2907                for _mcu_x in 0..mcus_x {
2908                    for (si, &comp_idx) in scan_comp_indices.iter().enumerate() {
2909                        let h_samp: usize = frame.components[comp_idx].horizontal_sampling as usize;
2910                        let v_samp: usize = frame.components[comp_idx].vertical_sampling as usize;
2911                        let blocks_x: usize = comp_data[comp_idx].blocks_x;
2912                        let dc_tbl: usize = scan.components[si].dc_table_index as usize;
2913
2914                        for v in 0..v_samp {
2915                            for h in 0..h_samp {
2916                                let bx: usize = _mcu_x * h_samp + h;
2917                                let by: usize = _mcu_y * v_samp + v;
2918                                let block_idx: usize = by * blocks_x + bx;
2919                                let block: &mut [i16; 64] =
2920                                    &mut comp_data[comp_idx].blocks[block_idx];
2921
2922                                if is_dc && ah == 0 {
2923                                    arith
2924                                        .decode_dc_first_progressive(block, comp_idx, dc_tbl, al)?;
2925                                } else if is_dc {
2926                                    arith.decode_dc_refine_progressive(block, al)?;
2927                                }
2928                            }
2929                        }
2930                    }
2931                }
2932            }
2933        } else {
2934            // Non-interleaved scan (single component)
2935            let comp_idx: usize = scan_comp_indices[0];
2936            // Use actual data block counts for non-interleaved scans, not MCU-padded.
2937            // The JPEG bitstream contains exactly width_in_blocks * height_in_blocks
2938            // data units per component (C libjpeg-turbo jdinput.c:119-124,175-176).
2939            let arith_max_h: usize = frame
2940                .components
2941                .iter()
2942                .map(|c| c.horizontal_sampling as usize)
2943                .max()
2944                .unwrap_or(1);
2945            let arith_max_v: usize = frame
2946                .components
2947                .iter()
2948                .map(|c| c.vertical_sampling as usize)
2949                .max()
2950                .unwrap_or(1);
2951            let h_samp: usize = comp_data[comp_idx].h_sampling as usize;
2952            let v_samp: usize = comp_data[comp_idx].v_sampling as usize;
2953            let comp_blocks_x: usize = (frame.width as usize * h_samp).div_ceil(arith_max_h * 8);
2954            let comp_blocks_y: usize = (frame.height as usize * v_samp).div_ceil(arith_max_v * 8);
2955            let stride_x: usize = comp_data[comp_idx].blocks_x;
2956            let dc_tbl: usize = scan.components[0].dc_table_index as usize;
2957            let ac_tbl: usize = scan.components[0].ac_table_index as usize;
2958
2959            for by in 0..comp_blocks_y {
2960                for bx in 0..comp_blocks_x {
2961                    let block_idx: usize = by * stride_x + bx;
2962                    let block: &mut [i16; 64] = &mut comp_data[comp_idx].blocks[block_idx];
2963
2964                    if is_dc {
2965                        if ah == 0 {
2966                            arith.decode_dc_first_progressive(block, comp_idx, dc_tbl, al)?;
2967                        } else {
2968                            arith.decode_dc_refine_progressive(block, al)?;
2969                        }
2970                    } else if ah == 0 {
2971                        arith.decode_ac_first_progressive(block, ac_tbl, ss, se, al)?;
2972                    } else {
2973                        arith.decode_ac_refine_progressive(block, ac_tbl, ss, se, al)?;
2974                    }
2975                }
2976            }
2977        }
2978    }
2979
2980    Ok(())
2981}
2982
2983fn decode_progressive_coefficients(
2984    data: &[u8],
2985    metadata: &JpegMetadata,
2986    comp_data: &mut [ComponentCoefficients],
2987    mcus_x: usize,
2988    mcus_y: usize,
2989) -> Result<()> {
2990    use crate::decode::bitstream::BitReader;
2991    use crate::decode::progressive;
2992
2993    let frame = &metadata.frame;
2994    let max_h = frame
2995        .components
2996        .iter()
2997        .map(|c| c.horizontal_sampling as usize)
2998        .max()
2999        .unwrap_or(1);
3000    let max_v = frame
3001        .components
3002        .iter()
3003        .map(|c| c.vertical_sampling as usize)
3004        .max()
3005        .unwrap_or(1);
3006
3007    // Per-block highest nonzero AC zigzag index (issue #352: bounds the
3008    // refinement EOB-run walk to the block's spectral extent).
3009    let mut ac_max_k: Vec<Vec<u8>> = comp_data
3010        .iter()
3011        .map(|cd| vec![0u8; cd.blocks.len()])
3012        .collect();
3013
3014    for scan_info in &metadata.scans {
3015        let scan = &scan_info.header;
3016        let ss = scan.spec_start;
3017        let se = scan.spec_end;
3018        let ah = scan.succ_high;
3019        let al = scan.succ_low;
3020        let is_dc = ss == 0 && se == 0;
3021
3022        let entropy_data = &data[scan_info.data_offset..];
3023        let mut bit_reader = BitReader::new(entropy_data);
3024
3025        let scan_comp_indices: Vec<usize> = scan
3026            .components
3027            .iter()
3028            .map(|sc| {
3029                frame
3030                    .components
3031                    .iter()
3032                    .position(|fc| fc.id == sc.component_id)
3033                    .ok_or_else(|| {
3034                        JpegError::CorruptData(format!(
3035                            "scan references unknown component {}",
3036                            sc.component_id
3037                        ))
3038                    })
3039            })
3040            .collect::<Result<Vec<_>>>()?;
3041
3042        if scan.components.len() > 1 {
3043            // Interleaved scan (DC only)
3044            let mut dc_preds = [0i16; 4];
3045            let mut mcu_count: u32 = 0;
3046
3047            for mcu_y in 0..mcus_y {
3048                for mcu_x in 0..mcus_x {
3049                    if scan_info.restart_interval > 0
3050                        && mcu_count > 0
3051                        && mcu_count.is_multiple_of(scan_info.restart_interval as u32)
3052                    {
3053                        bit_reader.reset();
3054                        dc_preds = [0i16; 4];
3055                    }
3056
3057                    for (si, &comp_idx) in scan_comp_indices.iter().enumerate() {
3058                        let h_samp = comp_data[comp_idx].h_sampling as usize;
3059                        let v_samp = comp_data[comp_idx].v_sampling as usize;
3060                        let blocks_x = comp_data[comp_idx].blocks_x;
3061                        let scan_comp = &scan.components[si];
3062
3063                        let dc_table = scan_info.dc_huffman_tables
3064                            [scan_comp.dc_table_index as usize]
3065                            .as_ref()
3066                            .ok_or_else(|| {
3067                                JpegError::CorruptData(format!(
3068                                    "missing DC table {}",
3069                                    scan_comp.dc_table_index
3070                                ))
3071                            })?;
3072
3073                        for v in 0..v_samp {
3074                            for h in 0..h_samp {
3075                                let bx = mcu_x * h_samp + h;
3076                                let by = mcu_y * v_samp + v;
3077                                let block_idx = by * blocks_x + bx;
3078                                let coeffs = &mut comp_data[comp_idx].blocks[block_idx];
3079
3080                                if is_dc {
3081                                    if ah == 0 {
3082                                        progressive::decode_dc_first(
3083                                            &mut bit_reader,
3084                                            dc_table,
3085                                            &mut dc_preds[comp_idx],
3086                                            coeffs,
3087                                            al,
3088                                        )?;
3089                                    } else {
3090                                        progressive::decode_dc_refine(&mut bit_reader, coeffs, al)?;
3091                                    }
3092                                }
3093                            }
3094                        }
3095                    }
3096
3097                    mcu_count += 1;
3098                }
3099            }
3100        } else {
3101            // Non-interleaved scan
3102            let comp_idx = scan_comp_indices[0];
3103            let scan_comp = &scan.components[0];
3104            // Use actual data block counts for non-interleaved scans, not MCU-padded.
3105            // The JPEG bitstream contains exactly width_in_blocks * height_in_blocks
3106            // data units per component (C libjpeg-turbo jdinput.c:119-124,175-176).
3107            let h_samp: usize = comp_data[comp_idx].h_sampling as usize;
3108            let v_samp: usize = comp_data[comp_idx].v_sampling as usize;
3109            let comp_blocks_x: usize = (frame.width as usize * h_samp).div_ceil(max_h * 8);
3110            let comp_blocks_y: usize = (frame.height as usize * v_samp).div_ceil(max_v * 8);
3111            let stride_x: usize = comp_data[comp_idx].blocks_x;
3112            let mut dc_pred = 0i16;
3113            let mut eob_run = 0u16;
3114            let mut mcu_count: u32 = 0;
3115
3116            let dc_table = if is_dc {
3117                Some(
3118                    scan_info.dc_huffman_tables[scan_comp.dc_table_index as usize]
3119                        .as_ref()
3120                        .ok_or_else(|| {
3121                            JpegError::CorruptData(format!(
3122                                "missing DC table {}",
3123                                scan_comp.dc_table_index
3124                            ))
3125                        })?,
3126                )
3127            } else {
3128                None
3129            };
3130            let ac_table = if !is_dc || se > 0 {
3131                Some(
3132                    scan_info.ac_huffman_tables[scan_comp.ac_table_index as usize]
3133                        .as_ref()
3134                        .ok_or_else(|| {
3135                            JpegError::CorruptData(format!(
3136                                "missing AC table {}",
3137                                scan_comp.ac_table_index
3138                            ))
3139                        })?,
3140                )
3141            } else {
3142                None
3143            };
3144
3145            let restart_interval = scan_info.restart_interval as u32;
3146
3147            for by in 0..comp_blocks_y {
3148                for bx in 0..comp_blocks_x {
3149                    if restart_interval > 0
3150                        && mcu_count > 0
3151                        && mcu_count.is_multiple_of(restart_interval)
3152                    {
3153                        bit_reader.reset();
3154                        dc_pred = 0;
3155                        eob_run = 0;
3156                    }
3157
3158                    let block_idx = by * stride_x + bx;
3159                    let coeffs = &mut comp_data[comp_idx].blocks[block_idx];
3160
3161                    if is_dc {
3162                        if ah == 0 {
3163                            progressive::decode_dc_first(
3164                                &mut bit_reader,
3165                                dc_table.unwrap(),
3166                                &mut dc_pred,
3167                                coeffs,
3168                                al,
3169                            )?;
3170                        } else {
3171                            progressive::decode_dc_refine(&mut bit_reader, coeffs, al)?;
3172                        }
3173                    } else if ah == 0 {
3174                        progressive::decode_ac_first_tracked(
3175                            &mut bit_reader,
3176                            ac_table.unwrap(),
3177                            coeffs,
3178                            ss,
3179                            se,
3180                            al,
3181                            &mut eob_run,
3182                            &mut ac_max_k[comp_idx][block_idx],
3183                        )?;
3184                    } else {
3185                        progressive::decode_ac_refine_tracked(
3186                            &mut bit_reader,
3187                            ac_table.unwrap(),
3188                            coeffs,
3189                            ss,
3190                            se,
3191                            al,
3192                            &mut eob_run,
3193                            &mut ac_max_k[comp_idx][block_idx],
3194                        )?;
3195                    }
3196
3197                    mcu_count += 1;
3198                }
3199            }
3200        }
3201    }
3202
3203    Ok(())
3204}