Skip to main content

libjpeg_turbo_rs/api/
progressive_output.rs

1// libjpeg-turbo-rs: alloc prelude (no_std support, issue #356)
2/// Progressive buffered output / scan-by-scan decode.
3///
4/// Matches libjpeg-turbo's `buffered_image` mode: `jpeg_has_multiple_scans()`,
5/// `jpeg_start_output()` / `jpeg_finish_output()`, `jpeg_consume_input()`,
6/// `jpeg_input_complete()`.
7///
8/// Progressive JPEGs are encoded in multiple scans. This decoder allows you to
9/// output the image after each scan, progressively refining the quality.
10use crate::common::error::{JpegError, Result};
11use crate::common::icc;
12use crate::common::quant_table::QuantTable;
13use crate::common::types::*;
14use crate::decode::bitstream::BitReader;
15use crate::decode::marker::{JpegMetadata, MarkerReader, ScanInfo};
16use crate::decode::pipeline::{upsample_generic_nearest, Image};
17use crate::decode::progressive;
18use crate::simd::{self, SimdRoutines};
19#[allow(unused_imports)]
20use alloc::vec::Vec;
21#[allow(unused_imports)]
22use alloc::{format, vec};
23
24/// Per-component layout info for progressive coefficient management.
25struct CompInfo {
26    blocks_x: usize,
27    blocks_y: usize,
28    h_samp: usize,
29    v_samp: usize,
30    comp_w: usize,
31}
32
33/// Decoder that supports scan-by-scan progressive output.
34///
35/// Progressive JPEGs encode image data in multiple scans, each refining
36/// the image quality. This decoder lets you consume scans one at a time
37/// and output the best available reconstruction at any point.
38pub struct ProgressiveDecoder {
39    /// Raw JPEG data (borrowed lifetime replaced with owned for simplicity).
40    raw_data: Vec<u8>,
41    /// Parsed metadata from JPEG headers.
42    metadata: JpegMetadata,
43    /// SIMD dispatch routines.
44    routines: SimdRoutines,
45    /// Per-component coefficient buffers, accumulated across scans.
46    coeff_bufs: Vec<Vec<[i16; 64]>>,
47    /// Per-block highest nonzero AC zigzag index (see issue #352:
48    /// bounds refinement EOB-run walks to the block's spectral extent).
49    ac_max_k_bufs: Vec<Vec<u8>>,
50    /// Per-component layout info.
51    comp_infos: Vec<CompInfo>,
52    /// MCUs in horizontal direction.
53    mcus_x: usize,
54    /// MCUs in vertical direction.
55    mcus_y: usize,
56    /// Max horizontal sampling factor.
57    max_h: usize,
58    /// Max vertical sampling factor.
59    max_v: usize,
60    /// Number of scans consumed so far.
61    scans_consumed: usize,
62}
63
64impl ProgressiveDecoder {
65    /// Create from JPEG data. Returns error if not a progressive JPEG.
66    /// Applies [`crate::common::types::DecodeLimits::default`]; use
67    /// [`Self::with_limits`] to tighten (e.g. a `max_memory` ceiling for
68    /// the coefficient buffers this decoder holds across scans).
69    pub fn new(data: &[u8]) -> Result<Self> {
70        Self::with_limits(data, crate::common::types::DecodeLimits::default())
71    }
72
73    /// Like [`Self::new`] with caller-chosen resource limits, applied
74    /// from marker parsing onward (issue #355).
75    pub fn with_limits(data: &[u8], limits: crate::common::types::DecodeLimits) -> Result<Self> {
76        let mut reader: MarkerReader<'_> = MarkerReader::new(data);
77        reader.set_scan_cap(limits.max_scans);
78        let metadata: JpegMetadata = reader.read_markers()?;
79
80        if !metadata.frame.is_progressive {
81            return Err(JpegError::Unsupported(
82                "ProgressiveDecoder requires a progressive JPEG (SOF2)".into(),
83            ));
84        }
85
86        let frame = &metadata.frame;
87        let max_h: usize = frame
88            .components
89            .iter()
90            .map(|c| c.horizontal_sampling as usize)
91            .max()
92            .unwrap_or(1);
93        let max_v: usize = frame
94            .components
95            .iter()
96            .map(|c| c.vertical_sampling as usize)
97            .max()
98            .unwrap_or(1);
99
100        // Resource guards (issue #355): coefficient buffers are
101        // allocated below from header-declared dimensions, so bound them
102        // before any allocation.
103        limits.check_frame(frame.width as usize, frame.height as usize)?;
104        // Defence-in-depth: with default limits this is subsumed by the
105        // parse-time cap in read_markers; it stays so the two cannot
106        // drift and so tighter caller limits apply here too.
107        if metadata.scans.len() > limits.max_scans {
108            return Err(JpegError::LimitExceeded {
109                what: "progressive scan count",
110                actual: metadata.scans.len() as u64,
111                limit: limits.max_scans as u64,
112            });
113        }
114        // Coefficient memory ceiling (progressive holds ~2 B/px/component
115        // of i16 coefficients plus the raw stream copy).
116        if let Some(max_mem) = limits.max_memory {
117            let total_pixels: u64 = (frame.width as u64) * (frame.height as u64);
118            let nc: u64 = frame.components.len() as u64;
119            let estimated: u64 =
120                total_pixels * (2 * nc) + total_pixels * nc / 64 + data.len() as u64;
121            if estimated > max_mem {
122                return Err(JpegError::LimitExceeded {
123                    what: "estimated decode memory",
124                    actual: estimated,
125                    limit: max_mem,
126                });
127            }
128        }
129
130        let mcu_w: usize = max_h * 8;
131        let mcu_h: usize = max_v * 8;
132        let mcus_x: usize = (frame.width as usize).div_ceil(mcu_w);
133        let mcus_y: usize = (frame.height as usize).div_ceil(mcu_h);
134
135        let comp_infos: Vec<CompInfo> = frame
136            .components
137            .iter()
138            .map(|comp| {
139                let h_samp: usize = comp.horizontal_sampling as usize;
140                let v_samp: usize = comp.vertical_sampling as usize;
141                CompInfo {
142                    blocks_x: mcus_x * h_samp,
143                    blocks_y: mcus_y * v_samp,
144                    h_samp,
145                    v_samp,
146                    // Full IDCT block size = 8
147                    comp_w: mcus_x * h_samp * 8,
148                }
149            })
150            .collect();
151
152        // Allocate coefficient buffers (zero-initialized for progressive accumulation)
153        let coeff_bufs: Vec<Vec<[i16; 64]>> = comp_infos
154            .iter()
155            .map(|ci| vec![[0i16; 64]; ci.blocks_x * ci.blocks_y])
156            .collect();
157        let ac_max_k_bufs: Vec<Vec<u8>> = comp_infos
158            .iter()
159            .map(|ci| vec![0u8; ci.blocks_x * ci.blocks_y])
160            .collect();
161
162        let routines: SimdRoutines = simd::detect();
163
164        Ok(Self {
165            raw_data: data.to_vec(),
166            metadata,
167            routines,
168            coeff_bufs,
169            ac_max_k_bufs,
170            comp_infos,
171            mcus_x,
172            mcus_y,
173            max_h,
174            max_v,
175            scans_consumed: 0,
176        })
177    }
178
179    /// Check if the JPEG has multiple scans (i.e., is progressive).
180    pub fn has_multiple_scans(&self) -> bool {
181        self.metadata.scans.len() > 1
182    }
183
184    /// Get total number of scans in the image.
185    pub fn num_scans(&self) -> usize {
186        self.metadata.scans.len()
187    }
188
189    /// Get image width in pixels.
190    pub fn width(&self) -> usize {
191        self.metadata.frame.width as usize
192    }
193
194    /// Get image height in pixels.
195    pub fn height(&self) -> usize {
196        self.metadata.frame.height as usize
197    }
198
199    /// Consume the next scan from input.
200    /// Returns true if a scan was consumed, false if all scans are done.
201    pub fn consume_input(&mut self) -> Result<bool> {
202        let scan_idx: usize = self.scans_consumed;
203        if scan_idx >= self.metadata.scans.len() {
204            return Ok(false);
205        }
206
207        self.decode_one_scan(scan_idx)?;
208        self.scans_consumed += 1;
209        Ok(true)
210    }
211
212    /// Check if all input scans have been consumed.
213    pub fn input_complete(&self) -> bool {
214        self.scans_consumed >= self.metadata.scans.len()
215    }
216
217    /// Get the number of scans consumed so far.
218    pub fn scans_consumed(&self) -> usize {
219        self.scans_consumed
220    }
221
222    /// Output the current image state (after consuming some scans).
223    /// Returns the best available reconstruction from scans consumed so far.
224    /// Each call to `consume_input()` followed by `output()` gives a
225    /// progressively better image.
226    pub fn output(&self) -> Result<Image> {
227        let frame = &self.metadata.frame;
228        let block_size: usize = 8;
229        let num_components: usize = frame.components.len();
230        let out_width: usize = frame.width as usize;
231        let out_height: usize = frame.height as usize;
232        let full_width: usize = self.mcus_x * self.max_h * block_size;
233        let full_height: usize = self.mcus_y * self.max_v * block_size;
234
235        // Resolve quant tables
236        let quant_tables: Vec<&QuantTable> = frame
237            .components
238            .iter()
239            .map(|comp| {
240                self.metadata.quant_tables[comp.quant_table_index as usize]
241                    .as_ref()
242                    .ok_or_else(|| {
243                        JpegError::CorruptData(format!(
244                            "missing quant table {}",
245                            comp.quant_table_index
246                        ))
247                    })
248            })
249            .collect::<Result<Vec<_>>>()?;
250
251        // IDCT all blocks into component planes
252        let mut component_planes: Vec<Vec<u8>> = self
253            .comp_infos
254            .iter()
255            .map(|ci| {
256                let size: usize = ci.comp_w * ci.blocks_y * block_size;
257                let mut v: Vec<u8> = Vec::with_capacity(size);
258                #[allow(clippy::uninit_vec)]
259                unsafe {
260                    v.set_len(size)
261                };
262                v
263            })
264            .collect();
265
266        for (comp_idx, ci) in self.comp_infos.iter().enumerate() {
267            let qt_values: &[u16; 64] = &quant_tables[comp_idx].values;
268            for by in 0..ci.blocks_y {
269                for bx in 0..ci.blocks_x {
270                    let block_idx: usize = by * ci.blocks_x + bx;
271                    let coeffs: &[i16; 64] = &self.coeff_bufs[comp_idx][block_idx];
272
273                    let px_x: usize = bx * block_size;
274                    let px_y: usize = by * block_size;
275                    let dst_offset: usize = px_y * ci.comp_w + px_x;
276
277                    unsafe {
278                        let dst: *mut u8 = component_planes[comp_idx].as_mut_ptr().add(dst_offset);
279                        self.idct_islow_strided(coeffs, qt_values, dst, ci.comp_w);
280                    }
281                }
282            }
283        }
284
285        // Assemble into final Image with color conversion
286        let icc_profile: Option<Vec<u8>> = icc::reassemble_icc_profile(&self.metadata.icc_chunks);
287        let exif_data: Option<Vec<u8>> = self.metadata.exif_data.clone();
288
289        if num_components == 1 {
290            self.assemble_grayscale(
291                &component_planes,
292                out_width,
293                out_height,
294                icc_profile,
295                exif_data,
296            )
297        } else if num_components == 3 {
298            self.assemble_ycbcr(
299                &component_planes,
300                frame,
301                out_width,
302                out_height,
303                full_width,
304                full_height,
305                icc_profile,
306                exif_data,
307            )
308        } else if num_components == 4 {
309            self.assemble_4_component(
310                &component_planes,
311                frame,
312                out_width,
313                out_height,
314                full_width,
315                full_height,
316                icc_profile,
317                exif_data,
318            )
319        } else {
320            Err(JpegError::Unsupported(format!(
321                "{} components not supported in progressive output",
322                num_components
323            )))
324        }
325    }
326
327    /// Consume all remaining scans and output the final image.
328    /// Equivalent to calling `consume_input()` in a loop then `output()`.
329    pub fn finish(mut self) -> Result<Image> {
330        while self.consume_input()? {}
331        self.output()
332    }
333
334    // ---- Private helpers ----
335
336    /// IDCT writing directly to a strided destination buffer.
337    ///
338    /// # Safety
339    /// `output` must point to at least `7 * stride + 8` writable bytes.
340    #[inline(always)]
341    unsafe fn idct_islow_strided(
342        &self,
343        coeffs: &[i16; 64],
344        quant: &[u16; 64],
345        output: *mut u8,
346        stride: usize,
347    ) {
348        unsafe {
349            #[cfg(all(target_arch = "aarch64", feature = "simd"))]
350            {
351                return crate::simd::aarch64::idct::neon_idct_islow_strided(
352                    coeffs, quant, output, stride,
353                );
354            }
355
356            #[cfg(all(target_arch = "x86_64", feature = "simd"))]
357            {
358                if crate::cpu_has!("avx2") {
359                    return crate::simd::x86_64::avx2_idct::avx2_idct_islow_strided(
360                        coeffs, quant, output, stride,
361                    );
362                }
363                if crate::cpu_has!("sse2") {
364                    return crate::simd::x86_64::idct::sse2_idct_islow_strided(
365                        coeffs, quant, output, stride,
366                    );
367                }
368            }
369
370            #[allow(unreachable_code)]
371            {
372                let mut tmp = [0u8; 64];
373                (self.routines.idct_islow)(coeffs, quant, &mut tmp);
374                for row in 0..8 {
375                    core::ptr::copy_nonoverlapping(
376                        tmp.as_ptr().add(row * 8),
377                        output.add(row * stride),
378                        8,
379                    );
380                }
381            }
382        }
383    }
384
385    /// Decode a single progressive scan's entropy data into coefficient buffers.
386    fn decode_one_scan(&mut self, scan_idx: usize) -> Result<()> {
387        // Extract all needed scan parameters before mutably borrowing coeff_bufs.
388        let scan_info: &ScanInfo = &self.metadata.scans[scan_idx];
389        let ss: u8 = scan_info.header.spec_start;
390        let se: u8 = scan_info.header.spec_end;
391        let ah: u8 = scan_info.header.succ_high;
392        let al: u8 = scan_info.header.succ_low;
393        let is_dc: bool = ss == 0 && se == 0;
394        let data_offset: usize = scan_info.data_offset;
395        let restart_interval: u16 = scan_info.restart_interval;
396        let num_scan_components: usize = scan_info.header.components.len();
397
398        // Clone scan component selectors to avoid holding borrow on metadata
399        let scan_components: Vec<ScanComponentSelector> = scan_info.header.components.clone();
400
401        // Clone Huffman table handles needed for this scan (Arc refcount
402        // bumps — the tables themselves are shared, not copied).
403        let dc_tables: [Option<alloc::sync::Arc<crate::common::huffman_table::HuffmanTable>>; 4] =
404            scan_info.dc_huffman_tables.clone();
405        let ac_tables: [Option<alloc::sync::Arc<crate::common::huffman_table::HuffmanTable>>; 4] =
406            scan_info.ac_huffman_tables.clone();
407
408        let entropy_data: &[u8] = &self.raw_data[data_offset..];
409        let mut bit_reader: BitReader = BitReader::new(entropy_data);
410
411        // Resolve component indices for this scan
412        let scan_comp_indices: Vec<usize> = scan_components
413            .iter()
414            .map(|sc| {
415                self.metadata
416                    .frame
417                    .components
418                    .iter()
419                    .position(|fc| fc.id == sc.component_id)
420                    .ok_or_else(|| {
421                        JpegError::CorruptData(format!(
422                            "scan references unknown component {}",
423                            sc.component_id
424                        ))
425                    })
426            })
427            .collect::<Result<Vec<_>>>()?;
428
429        if num_scan_components > 1 {
430            // Interleaved scan (DC only in progressive)
431            let mut dc_preds = [0i16; 4];
432            let mut mcu_count: u32 = 0;
433
434            for mcu_y in 0..self.mcus_y {
435                for mcu_x in 0..self.mcus_x {
436                    if restart_interval > 0
437                        && mcu_count > 0
438                        && mcu_count.is_multiple_of(restart_interval as u32)
439                    {
440                        bit_reader.reset();
441                        dc_preds = [0i16; 4];
442                    }
443
444                    for (si, &comp_idx) in scan_comp_indices.iter().enumerate() {
445                        let blocks_x: usize = self.comp_infos[comp_idx].blocks_x;
446                        let h_samp: usize = self.comp_infos[comp_idx].h_samp;
447                        let v_samp: usize = self.comp_infos[comp_idx].v_samp;
448                        let sc = &scan_components[si];
449
450                        let dc_table =
451                            dc_tables[sc.dc_table_index as usize]
452                                .as_ref()
453                                .ok_or_else(|| {
454                                    JpegError::CorruptData(format!(
455                                        "missing DC table {}",
456                                        sc.dc_table_index
457                                    ))
458                                })?;
459
460                        for v in 0..v_samp {
461                            for h in 0..h_samp {
462                                let bx: usize = mcu_x * h_samp + h;
463                                let by: usize = mcu_y * v_samp + v;
464                                let block_idx: usize = by * blocks_x + bx;
465                                let coeffs: &mut [i16; 64] =
466                                    &mut self.coeff_bufs[comp_idx][block_idx];
467
468                                if is_dc {
469                                    if ah == 0 {
470                                        progressive::decode_dc_first(
471                                            &mut bit_reader,
472                                            dc_table,
473                                            &mut dc_preds[comp_idx],
474                                            coeffs,
475                                            al,
476                                        )?;
477                                    } else {
478                                        progressive::decode_dc_refine(&mut bit_reader, coeffs, al)?;
479                                    }
480                                }
481                            }
482                        }
483                    }
484
485                    mcu_count += 1;
486                }
487            }
488            Ok(())
489        } else {
490            // Non-interleaved scan (single component)
491            let comp_idx: usize = scan_comp_indices[0];
492            let sc = &scan_components[0];
493            let blocks_x: usize = self.comp_infos[comp_idx].blocks_x;
494            let blocks_y: usize = self.comp_infos[comp_idx].blocks_y;
495            let mut dc_pred: i16 = 0;
496            let mut eob_run: u16 = 0;
497            let mut mcu_count: u32 = 0;
498
499            let dc_table_ref = if is_dc {
500                Some(
501                    dc_tables[sc.dc_table_index as usize]
502                        .as_ref()
503                        .ok_or_else(|| {
504                            JpegError::CorruptData(format!(
505                                "missing DC table {}",
506                                sc.dc_table_index
507                            ))
508                        })?,
509                )
510            } else {
511                None
512            };
513
514            let ac_table_ref = if !is_dc || se > 0 {
515                Some(
516                    ac_tables[sc.ac_table_index as usize]
517                        .as_ref()
518                        .ok_or_else(|| {
519                            JpegError::CorruptData(format!(
520                                "missing AC table {}",
521                                sc.ac_table_index
522                            ))
523                        })?,
524                )
525            } else {
526                None
527            };
528
529            for by in 0..blocks_y {
530                for bx in 0..blocks_x {
531                    if restart_interval > 0
532                        && mcu_count > 0
533                        && mcu_count.is_multiple_of(restart_interval as u32)
534                    {
535                        bit_reader.reset();
536                        dc_pred = 0;
537                        eob_run = 0;
538                    }
539
540                    let block_idx: usize = by * blocks_x + bx;
541                    let coeffs: &mut [i16; 64] = &mut self.coeff_bufs[comp_idx][block_idx];
542
543                    if is_dc {
544                        if ah == 0 {
545                            progressive::decode_dc_first(
546                                &mut bit_reader,
547                                dc_table_ref.unwrap(),
548                                &mut dc_pred,
549                                coeffs,
550                                al,
551                            )?;
552                        } else {
553                            progressive::decode_dc_refine(&mut bit_reader, coeffs, al)?;
554                        }
555                    } else if ah == 0 {
556                        progressive::decode_ac_first_tracked(
557                            &mut bit_reader,
558                            ac_table_ref.unwrap(),
559                            coeffs,
560                            ss,
561                            se,
562                            al,
563                            &mut eob_run,
564                            &mut self.ac_max_k_bufs[comp_idx][block_idx],
565                        )?;
566                    } else {
567                        progressive::decode_ac_refine_tracked(
568                            &mut bit_reader,
569                            ac_table_ref.unwrap(),
570                            coeffs,
571                            ss,
572                            se,
573                            al,
574                            &mut eob_run,
575                            &mut self.ac_max_k_bufs[comp_idx][block_idx],
576                        )?;
577                    }
578
579                    mcu_count += 1;
580                }
581            }
582            Ok(())
583        }
584    }
585
586    /// Assemble a grayscale image from component planes.
587    fn assemble_grayscale(
588        &self,
589        component_planes: &[Vec<u8>],
590        out_width: usize,
591        out_height: usize,
592        icc_profile: Option<Vec<u8>>,
593        exif_data: Option<Vec<u8>>,
594    ) -> Result<Image> {
595        let comp_w: usize = self.comp_infos[0].comp_w;
596        let mut data: Vec<u8> = Vec::with_capacity(out_width * out_height);
597        for y in 0..out_height {
598            data.extend_from_slice(&component_planes[0][y * comp_w..y * comp_w + out_width]);
599        }
600        Ok(Image {
601            xmp_data: self.metadata.xmp_data.clone(),
602            iptc_data: self.metadata.iptc_data.clone(),
603            width: out_width,
604            height: out_height,
605            pixel_format: PixelFormat::Grayscale,
606            precision: 8,
607            data,
608            icc_profile,
609            exif_data,
610            comment: self.metadata.comment.clone(),
611            density: self.metadata.density,
612            saved_markers: self.metadata.saved_markers.clone(),
613            warnings: Vec::new(),
614        })
615    }
616
617    /// Assemble a 3-component YCbCr image with upsampling and color conversion.
618    #[allow(clippy::too_many_arguments)]
619    fn assemble_ycbcr(
620        &self,
621        component_planes: &[Vec<u8>],
622        frame: &FrameHeader,
623        out_width: usize,
624        out_height: usize,
625        full_width: usize,
626        full_height: usize,
627        icc_profile: Option<Vec<u8>>,
628        exif_data: Option<Vec<u8>>,
629    ) -> Result<Image> {
630        let out_format: PixelFormat = PixelFormat::Rgb;
631        let bpp: usize = out_format.bytes_per_pixel();
632
633        // The progressive YCbCr assembly paths below assume the standard
634        // JPEG sampling layout: Y at the frame's max sampling, Cb/Cr at
635        // sampling factors ≤ max with matching ratios. Adversarial /
636        // malformed streams can pick e.g. Y=h1v2, Cb=h4v1, Cr=h1v1 — all
637        // legal per ITU-T T.81 §B.2.2 syntax — which yields component
638        // planes whose row strides and counts disagree, and the SIMD
639        // upsamplers (`fancy_h1v2`, …) read past the chroma plane. ASan
640        // caught this as a heap-buffer-overflow READ of size 16 in
641        // fuzz_progressive_decoder (CI run 25215431132). C `libjpeg`
642        // accepts only the standard layouts; reject the rest as
643        // Unsupported up front rather than tripping unsafe SIMD loads.
644        let y_comp = &frame.components[0];
645        let cb_comp = &frame.components[1];
646        let cr_comp = &frame.components[2];
647        if y_comp.horizontal_sampling as usize != self.max_h
648            || y_comp.vertical_sampling as usize != self.max_v
649            || cb_comp.horizontal_sampling != cr_comp.horizontal_sampling
650            || cb_comp.vertical_sampling != cr_comp.vertical_sampling
651        {
652            return Err(JpegError::Unsupported(format!(
653                "non-standard YCbCr sampling Y={}x{}, Cb={}x{}, Cr={}x{} \
654                 (max={}x{}); progressive assembly requires Y at full \
655                 sampling and Cb/Cr matching",
656                y_comp.horizontal_sampling,
657                y_comp.vertical_sampling,
658                cb_comp.horizontal_sampling,
659                cb_comp.vertical_sampling,
660                cr_comp.horizontal_sampling,
661                cr_comp.vertical_sampling,
662                self.max_h,
663                self.max_v
664            )));
665        }
666
667        let y_plane: &[u8] = &component_planes[0];
668        let y_width: usize = self.comp_infos[0].comp_w;
669
670        let cb_w: usize = self.comp_infos[1].comp_w;
671        let cb_h: usize = self.comp_infos[1].blocks_y * 8;
672
673        let h_factor: usize = self.max_h / cb_comp.horizontal_sampling as usize;
674        let v_factor: usize = self.max_v / cb_comp.vertical_sampling as usize;
675
676        // The 4:4:4 fast path below assumes all three components share the
677        // same row stride and have at least `out_height` rows of decoded
678        // data. h_factor / v_factor are derived from Cb only, so a stream
679        // with Cb at max sampling but Y or Cr undersampled (e.g.
680        // Y=h1v1, Cb=h1v3, Cr=h1v1 — max_v=3 dominated by Cb) would
681        // satisfy `h_factor == 1 && v_factor == 1` while the Y plane is
682        // shorter than the image raster. The previous code then
683        // panicked at the slice index in `y_plane[y * y_width..]` when
684        // `y` exceeded the actual Y plane height. Found via
685        // fuzz_progressive_decoder on a 16x16 SOF2 stream with these
686        // factors (Fuzz Smoke run 25213799463). Demand all three
687        // components be at full sampling for the fast path.
688        let y_comp = &frame.components[0];
689        let cr_comp = &frame.components[2];
690        let all_full_sampling = y_comp.horizontal_sampling as usize == self.max_h
691            && y_comp.vertical_sampling as usize == self.max_v
692            && cb_comp.horizontal_sampling as usize == self.max_h
693            && cb_comp.vertical_sampling as usize == self.max_v
694            && cr_comp.horizontal_sampling as usize == self.max_h
695            && cr_comp.vertical_sampling as usize == self.max_v;
696
697        if h_factor == 1 && v_factor == 1 && all_full_sampling {
698            // 4:4:4: no upsampling needed
699            let data_size: usize = out_width * out_height * bpp;
700            let mut data: Vec<u8> = Vec::with_capacity(data_size);
701            #[allow(clippy::uninit_vec)]
702            unsafe {
703                data.set_len(data_size)
704            };
705            for y in 0..out_height {
706                self.ycbcr_to_rgb_row(
707                    &y_plane[y * y_width..],
708                    &component_planes[1][y * cb_w..],
709                    &component_planes[2][y * cb_w..],
710                    &mut data[y * out_width * bpp..],
711                    out_width,
712                );
713            }
714            Ok(Image {
715                xmp_data: self.metadata.xmp_data.clone(),
716                iptc_data: self.metadata.iptc_data.clone(),
717                width: out_width,
718                height: out_height,
719                pixel_format: out_format,
720                precision: 8,
721                data,
722                icc_profile,
723                exif_data,
724                comment: self.metadata.comment.clone(),
725                density: self.metadata.density,
726                saved_markers: self.metadata.saved_markers.clone(),
727                warnings: Vec::new(),
728            })
729        } else {
730            // Upsample chroma
731            let alloc_size: usize = full_width * full_height;
732            let mut cb_full: Vec<u8> = Vec::with_capacity(alloc_size);
733            let mut cr_full: Vec<u8> = Vec::with_capacity(alloc_size);
734            unsafe {
735                cb_full.set_len(alloc_size);
736                cr_full.set_len(alloc_size);
737            }
738
739            if h_factor == 2 && v_factor == 1 {
740                for row in 0..cb_h {
741                    self.fancy_upsample_h2v1(
742                        &component_planes[1][row * cb_w..],
743                        cb_w,
744                        &mut cb_full[row * full_width..],
745                    );
746                    self.fancy_upsample_h2v1(
747                        &component_planes[2][row * cb_w..],
748                        cb_w,
749                        &mut cr_full[row * full_width..],
750                    );
751                }
752            } else if h_factor == 2 && v_factor == 2 {
753                self.fancy_h2v2(&component_planes[1], cb_w, cb_h, &mut cb_full, full_width);
754                self.fancy_h2v2(&component_planes[2], cb_w, cb_h, &mut cr_full, full_width);
755            } else if h_factor == 1 && v_factor == 2 {
756                self.fancy_h1v2(&component_planes[1], cb_w, cb_h, &mut cb_full, full_width);
757                self.fancy_h1v2(&component_planes[2], cb_w, cb_h, &mut cr_full, full_width);
758            } else if h_factor == 4 && v_factor == 1 {
759                // S411: C uses int_upsample (box filter), not fancy interpolation.
760                upsample_generic_nearest(
761                    &component_planes[1],
762                    cb_w,
763                    cb_h,
764                    &mut cb_full,
765                    full_width,
766                    h_factor,
767                    1,
768                );
769                upsample_generic_nearest(
770                    &component_planes[2],
771                    cb_w,
772                    cb_h,
773                    &mut cr_full,
774                    full_width,
775                    h_factor,
776                    1,
777                );
778            } else if h_factor == 1 && v_factor == 4 {
779                // S441: C uses int_upsample (box filter), not fancy interpolation.
780                upsample_generic_nearest(
781                    &component_planes[1],
782                    cb_w,
783                    cb_h,
784                    &mut cb_full,
785                    full_width,
786                    1,
787                    v_factor,
788                );
789                upsample_generic_nearest(
790                    &component_planes[2],
791                    cb_w,
792                    cb_h,
793                    &mut cr_full,
794                    full_width,
795                    1,
796                    v_factor,
797                );
798            } else {
799                return Err(JpegError::Unsupported(format!(
800                    "subsampling {}x{} not supported in progressive output",
801                    h_factor, v_factor
802                )));
803            }
804
805            let data_size: usize = out_width * out_height * bpp;
806            let mut data: Vec<u8> = Vec::with_capacity(data_size);
807            #[allow(clippy::uninit_vec)]
808            unsafe {
809                data.set_len(data_size)
810            };
811            for y in 0..out_height {
812                self.ycbcr_to_rgb_row(
813                    &y_plane[y * y_width..],
814                    &cb_full[y * full_width..],
815                    &cr_full[y * full_width..],
816                    &mut data[y * out_width * bpp..],
817                    out_width,
818                );
819            }
820
821            Ok(Image {
822                xmp_data: self.metadata.xmp_data.clone(),
823                iptc_data: self.metadata.iptc_data.clone(),
824                width: out_width,
825                height: out_height,
826                pixel_format: out_format,
827                precision: 8,
828                data,
829                icc_profile,
830                exif_data,
831                comment: self.metadata.comment.clone(),
832                density: self.metadata.density,
833                saved_markers: self.metadata.saved_markers.clone(),
834                warnings: Vec::new(),
835            })
836        }
837    }
838
839    /// Assemble a 4-component (CMYK/YCCK) image.
840    #[allow(clippy::too_many_arguments)]
841    fn assemble_4_component(
842        &self,
843        component_planes: &[Vec<u8>],
844        _frame: &FrameHeader,
845        out_width: usize,
846        out_height: usize,
847        _full_width: usize,
848        _full_height: usize,
849        icc_profile: Option<Vec<u8>>,
850        exif_data: Option<Vec<u8>>,
851    ) -> Result<Image> {
852        // For 4-component, output as CMYK (no color conversion)
853        let bpp: usize = 4;
854        let data_size: usize = out_width * out_height * bpp;
855        let mut data: Vec<u8> = Vec::with_capacity(data_size);
856        #[allow(clippy::uninit_vec)]
857        unsafe {
858            data.set_len(data_size)
859        };
860
861        for y in 0..out_height {
862            for x in 0..out_width {
863                for c in 0..4 {
864                    let comp_w: usize = self.comp_infos[c].comp_w;
865                    data[y * out_width * bpp + x * bpp + c] = component_planes[c][y * comp_w + x];
866                }
867            }
868        }
869
870        Ok(Image {
871            xmp_data: self.metadata.xmp_data.clone(),
872            iptc_data: self.metadata.iptc_data.clone(),
873            width: out_width,
874            height: out_height,
875            pixel_format: PixelFormat::Cmyk,
876            precision: 8,
877            data,
878            icc_profile,
879            exif_data,
880            comment: self.metadata.comment.clone(),
881            density: self.metadata.density,
882            saved_markers: self.metadata.saved_markers.clone(),
883            warnings: Vec::new(),
884        })
885    }
886
887    // ---- Color conversion and upsampling delegates ----
888    // These mirror the Decoder methods but operate on &self.
889
890    #[inline(always)]
891    fn ycbcr_to_rgb_row(&self, y: &[u8], cb: &[u8], cr: &[u8], out: &mut [u8], width: usize) {
892        #[cfg(all(target_arch = "aarch64", feature = "simd"))]
893        {
894            return crate::simd::aarch64::color::neon_ycbcr_to_rgb_row(y, cb, cr, out, width);
895        }
896
897        #[allow(unreachable_code)]
898        (self.routines.ycbcr_to_rgb_row)(y, cb, cr, out, width)
899    }
900
901    #[inline(always)]
902    fn fancy_upsample_h2v1(&self, input: &[u8], in_width: usize, output: &mut [u8]) {
903        #[cfg(all(target_arch = "aarch64", feature = "simd"))]
904        {
905            return crate::simd::aarch64::upsample::neon_fancy_upsample_h2v1(
906                input, in_width, output,
907            );
908        }
909
910        #[allow(unreachable_code)]
911        (self.routines.fancy_upsample_h2v1)(input, in_width, output)
912    }
913
914    fn fancy_h2v2(
915        &self,
916        input: &[u8],
917        in_width: usize,
918        in_height: usize,
919        output: &mut [u8],
920        out_width: usize,
921    ) {
922        #[cfg(all(target_arch = "aarch64", feature = "simd"))]
923        {
924            crate::simd::aarch64::upsample::neon_fancy_upsample_h2v2(
925                input, in_width, in_height, output, out_width,
926            )
927        }
928
929        #[cfg(all(target_arch = "wasm32", feature = "simd"))]
930        {
931            return crate::simd::wasm32::upsample::wasm_fancy_upsample_h2v2(
932                input, in_width, in_height, output, out_width,
933            );
934        }
935
936        // Fused H2V2: vertical + horizontal in one pass using >> 4 arithmetic.
937        #[allow(unreachable_code)]
938        {
939            crate::decode::upsample::fancy_h2v2(
940                input,
941                in_width,
942                in_height,
943                output,
944                out_width,
945                in_height * 2,
946            );
947        }
948    }
949
950    fn fancy_h1v2(
951        &self,
952        input: &[u8],
953        in_width: usize,
954        in_height: usize,
955        output: &mut [u8],
956        out_width: usize,
957    ) {
958        // Defensive bounds: malformed progressive JPEGs can produce a chroma
959        // plane shorter than `in_height * in_width` if blocks_y was clobbered
960        // mid-stream. Clamp every per-row slice to the actual buffer length so
961        // the upsample falls back to repeating the current row at boundaries
962        // instead of panicking. Discovered via fuzz_progressive_decoder on a
963        // double-SOF input that left coeff_bufs sized to the first SOF and
964        // in_height/in_width sized to the second.
965        let actual_rows: usize = input.len().checked_div(in_width).unwrap_or(0);
966        let safe_height: usize = in_height.min(actual_rows);
967        for y in 0..safe_height {
968            let cur_row = &input[y * in_width..(y + 1) * in_width];
969            let above = if y > 0 {
970                &input[(y - 1) * in_width..y * in_width]
971            } else {
972                cur_row
973            };
974            let below = if y + 1 < safe_height {
975                &input[(y + 1) * in_width..(y + 2) * in_width]
976            } else {
977                cur_row
978            };
979
980            let out_y_top: usize = y * 2;
981            let out_y_bot: usize = y * 2 + 1;
982
983            // Ordered dither bias: top=1, bottom=2 (matches C jdsample.c)
984            for i in 0..in_width {
985                output[out_y_top * out_width + i] =
986                    ((3 * cur_row[i] as u16 + above[i] as u16 + 1) >> 2) as u8;
987                output[out_y_bot * out_width + i] =
988                    ((3 * cur_row[i] as u16 + below[i] as u16 + 2) >> 2) as u8;
989            }
990        }
991        // The caller allocates `output` with `set_len` (uninitialized). When
992        // safe_height < in_height we skipped writing the tail rows; zero them
993        // explicitly so downstream color-conversion never reads uninit bytes.
994        let written: usize = safe_height * 2 * out_width;
995        let cap: usize = in_height * 2 * out_width;
996        if written < cap && cap <= output.len() {
997            output[written..cap].fill(0);
998        }
999    }
1000}