Skip to main content

j2k_jpeg/
decoder.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Public [`Decoder`] entry points.
4
5use crate::backend::Backend;
6use crate::context::DecoderContext;
7use crate::entropy::block::{decode_block_with_activity, BlockActivity, CoefficientBlock};
8use crate::entropy::huffman::HuffmanTable;
9use crate::entropy::progressive::{
10    decode_progressive, decode_progressive_dct_blocks, PreparedProgressiveComponentPlan,
11    PreparedProgressivePlan, PreparedProgressiveScan, PreparedProgressiveScanComponent,
12};
13use crate::entropy::sequential::{
14    decode_scan_baseline, decode_scan_baseline_rgb, decode_scan_fast_rgb_444,
15    decode_scan_fast_tile_rgb, decode_scan_fast_tile_rgb_region,
16    decode_scan_fast_tile_rgb_region_scaled, fast_tile_region_first_decode_mcu, finish_scan,
17    stripe_region_layout, PreparedComponentPlan, PreparedDecodePlan,
18};
19use crate::entropy::ZIGZAG;
20use crate::error::{JpegError, MarkerKind, Warning};
21use crate::info::{
22    ColorSpace, DecodeOptions, DownscaleFactor, Info, OutputFormat, Rect, RestartIndex,
23    RestartSegment, SofKind,
24};
25use crate::internal::bit_reader::BitReader;
26use crate::internal::checkpoint::{checkpoint_before_mcu, CpuCheckpointCache, DeviceCheckpoint};
27use crate::internal::scratch::{ScratchPool, SinkRows};
28use crate::lossless::{lossless_predict, LosslessSample};
29use crate::output::{
30    validate_buffer, Gray8Writer, InterleavedRgbWriter, OutputWriter, Rgb8Writer, Rgba8Writer,
31};
32use crate::parse::header::{parse_header, parse_info, ParsedHeader};
33use crate::parse::tables::{HuffmanValues, RawHuffmanTable};
34use crate::profile::{duration_us_string, emit_jpeg_profile_row, jpeg_profile_stages_enabled};
35use crate::segment::PreparedJpeg;
36use crate::JpegCodec;
37use alloc::sync::Arc;
38use alloc::vec::Vec;
39use core::cell::RefCell;
40pub use j2k_core::TileBatchOptions;
41use j2k_core::{
42    CompressedPayloadKind, CompressedTransferSyntax, DecodeOutcome as CoreDecodeOutcome,
43    DecodeRowsError, DecoderContext as CoreDecoderContext, Downscale, ImageCodec, ImageDecode,
44    ImageDecodeRows, PassthroughCandidate, PixelFormat, RowSink, TileBatchDecode,
45};
46use std::sync::Mutex;
47use std::time::{Duration, Instant};
48
49const DEFAULT_MAX_DECODE_BYTES: usize = 512 * 1024 * 1024;
50const CPU_ROI_CHECKPOINT_CADENCE_MCUS: u32 = 1024;
51const CPU_ROI_CHECKPOINT_MIN_TARGET_MCUS: u32 = 4096;
52
53std::thread_local! {
54    static DEFAULT_SCRATCH: RefCell<ScratchPool> = RefCell::new(ScratchPool::new());
55    static DEFAULT_CONTEXT: RefCell<DecoderContext> = RefCell::new(DecoderContext::new());
56}
57
58/// Non-fatal outcome of a successful decode. See spec Section 2.
59///
60/// `DecodeOutcome` lives on `decoder.rs` rather than `info.rs` because it
61/// carries `Warning` values from `error.rs`, and moving it into `info` would
62/// create a `info → error` cycle (see `info.rs` header note).
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct DecodeOutcome {
65    /// The rectangle actually written to the output buffer. For `decode_into`
66    /// this is always `Rect::full(info.dimensions)`; later milestones add
67    /// `decode_region_into` which can return a narrower rect.
68    pub decoded: Rect,
69    /// Warnings emitted during parse or decode. Empty when the stream is
70    /// syntactically clean and every capability was exercised without fallback.
71    pub warnings: Vec<Warning>,
72}
73
74impl From<DecodeOutcome> for CoreDecodeOutcome<Warning> {
75    fn from(outcome: DecodeOutcome) -> Self {
76        Self {
77            decoded: outcome.decoded.into(),
78            warnings: outcome.warnings,
79        }
80    }
81}
82
83/// One tile decode request for [`decode_tiles_into`].
84pub type TileDecodeJob<'i, 'o> = j2k_core::TileDecodeJob<'i, 'o>;
85
86/// One decode request for a JPEG tile already normalized by
87/// [`prepare_tiff_jpeg_tile`](crate::prepare_tiff_jpeg_tile).
88pub struct PreparedJpegTileJob<'i, 'o> {
89    /// Decode-ready prepared JPEG bytes.
90    pub input: PreparedJpeg<'i>,
91    /// Caller-owned RGB8 output buffer for this tile.
92    pub out: &'o mut [u8],
93    /// Distance in bytes between output rows.
94    pub stride: usize,
95    /// Per-job JPEG decode options.
96    pub options: DecodeOptions,
97}
98
99/// Result for one successful prepared JPEG tile decode.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct DecodedTile {
102    /// Tile dimensions reported by the prepared JPEG header.
103    pub dimensions: (u32, u32),
104    /// Rectangle written into the output buffer.
105    pub decoded: Rect,
106    /// Non-fatal warnings emitted during parse or decode.
107    pub warnings: Vec<Warning>,
108}
109
110/// One scaled tile decode request for [`decode_tiles_scaled_into`].
111pub type TileScaledDecodeJob<'i, 'o> = j2k_core::TileScaledDecodeJob<'i, 'o>;
112
113/// One ROI+scaled tile decode request for
114/// [`decode_tiles_region_scaled_into`].
115pub type TileRegionScaledDecodeJob<'i, 'o> = j2k_core::TileRegionScaledDecodeJob<'i, 'o>;
116
117/// Error returned by [`decode_tiles_into`], annotated with the failing tile
118/// index from the caller's input order.
119pub type TileBatchError = j2k_core::TileBatchError<JpegError>;
120
121/// Receives decoded component rows before they are packed into the final
122/// interleaved pixel format.
123pub trait ComponentRowWriter {
124    /// Receive one grayscale row.
125    fn write_gray_row(&mut self, y: u32, gray_row: &[u8]) -> Result<(), JpegError>;
126
127    /// Receive one full-width Y/Cb/Cr row.
128    fn write_ycbcr_row(
129        &mut self,
130        y: u32,
131        y_row: &[u8],
132        cb_row: &[u8],
133        cr_row: &[u8],
134    ) -> Result<(), JpegError>;
135
136    /// Receive one full-width planar RGB row.
137    fn write_rgb_row(
138        &mut self,
139        y: u32,
140        r_row: &[u8],
141        g_row: &[u8],
142        b_row: &[u8],
143    ) -> Result<(), JpegError>;
144}
145
146/// A parsed borrowed view of a JPEG stream.
147#[derive(Debug)]
148pub struct JpegView<'a> {
149    bytes: &'a [u8],
150    header: ParsedHeader,
151    info: Info,
152    options: DecodeOptions,
153}
154
155impl<'a> JpegView<'a> {
156    /// Parse the stream into a borrowed view that can later build a decoder.
157    pub fn parse(input: &'a [u8]) -> Result<Self, JpegError> {
158        Self::parse_with_options(input, DecodeOptions::default())
159    }
160
161    /// Parse the stream with explicit decode options.
162    pub fn parse_with_options(input: &'a [u8], options: DecodeOptions) -> Result<Self, JpegError> {
163        let header = parse_header(input)?;
164        let mut info = header.info();
165        options.apply_to_info(&mut info);
166        Ok(Self {
167            bytes: input,
168            header,
169            info,
170            options,
171        })
172    }
173
174    /// Header-derived metadata for the parsed stream.
175    pub fn info(&self) -> &Info {
176        &self.info
177    }
178
179    /// Original compressed bytes backing this view.
180    pub fn bytes(&self) -> &'a [u8] {
181        self.bytes
182    }
183
184    /// Return a byte-preserving passthrough candidate for active DICOM/WSI
185    /// transfer syntaxes.
186    ///
187    /// Progressive JPEG is intentionally not exposed here because the active
188    /// conversion path should transcode it rather than introduce a retired or
189    /// unsupported destination syntax.
190    pub fn passthrough_candidate(&self) -> Option<PassthroughCandidate<'a>> {
191        jpeg_passthrough_syntax(&self.info).map(|transfer_syntax| {
192            PassthroughCandidate::new(
193                self.bytes,
194                transfer_syntax,
195                CompressedPayloadKind::JpegInterchange,
196                self.info.to_core_info(),
197            )
198        })
199    }
200
201    /// Build a restart-marker byte-offset index for the first scan.
202    ///
203    /// Offsets are absolute byte positions in the original JPEG byte slice.
204    /// Returns `Ok(None)` when the stream has no non-zero DRI marker.
205    pub fn restart_index(&self) -> Result<Option<RestartIndex>, JpegError> {
206        restart_index_for_stream(
207            self.bytes,
208            self.header.sos_offset,
209            &self.info,
210            self.info.restart_interval,
211        )
212    }
213
214    pub(crate) fn has_lossless_subsampled_color_capability_shape(&self) -> bool {
215        if self.info.sof_kind != SofKind::Lossless
216            || !matches!(self.info.color_space, ColorSpace::Rgb | ColorSpace::YCbCr)
217            || !matches!(self.info.bit_depth, 8 | 16)
218            || self.info.sampling.len() != 3
219            || !self
220                .info
221                .sampling
222                .components()
223                .iter()
224                .any(|&(h, v)| h != 1 || v != 1)
225            || self.header.scan_count != 1
226        {
227            return false;
228        }
229
230        let Some(scan) = self.header.scan.as_ref() else {
231            return false;
232        };
233        if !(1..=7).contains(&scan.ss)
234            || scan.se != 0
235            || scan.ah != 0
236            || scan.al != 0
237            || scan.components.len() != 3
238        {
239            return false;
240        }
241
242        scan.components.iter().all(|scan_component| {
243            find_component_index(&self.header.component_ids, scan_component.id).is_some()
244                && self.header.huffman_tables.dc[scan_component.dc_table as usize].is_some()
245        })
246    }
247}
248
249/// A borrowed view of a JPEG stream ready to decode. Constructed via
250/// [`Decoder::new`] or [`Decoder::from_view`]. `Decoder<'a>: Send + Sync`.
251#[derive(Debug)]
252pub struct Decoder<'a> {
253    pub(crate) bytes: &'a [u8],
254    pub(crate) info: Info,
255    pub(crate) warnings: Arc<[Warning]>,
256    pub(crate) backend: Backend,
257    pub(crate) plan: PreparedDecodePlan,
258    pub(crate) progressive_plan: Option<PreparedProgressivePlan>,
259    lossless_plan: Option<PreparedLosslessPlan>,
260    pub(crate) cpu_entropy_checkpoints: Mutex<CpuCheckpointCache>,
261}
262
263#[derive(Debug, Clone)]
264struct PreparedLosslessPlan {
265    predictor: u8,
266    bit_depth: u8,
267    dc_table: Arc<HuffmanTable>,
268    dimensions: (u32, u32),
269    scan_offset: usize,
270}
271
272#[derive(Clone, Copy, Debug, Eq, PartialEq)]
273enum LosslessColorSampling {
274    S444,
275    S422,
276    S420,
277}
278
279impl<'a> Decoder<'a> {
280    /// Parse the headers without decoding pixels. The parser walks headers up
281    /// to the first SOS and then performs a lightweight marker scan so
282    /// `Info::scan_count` reflects all scans in the file.
283    ///
284    /// # Errors
285    /// Returns any structural, unsupported-SOF, or sanity-check error
286    /// encountered before the Start-of-Scan marker. See [`JpegError`].
287    pub fn inspect(input: &'a [u8]) -> Result<Info, JpegError> {
288        Self::inspect_with_options(input, DecodeOptions::default())
289    }
290
291    /// Parse headers with explicit decode options, without decoding pixels.
292    ///
293    /// The options are applied to the returned [`Info`] exactly as they would
294    /// be for [`Self::new_with_options`].
295    pub fn inspect_with_options(
296        input: &'a [u8],
297        options: DecodeOptions,
298    ) -> Result<Info, JpegError> {
299        let mut info = parse_info(input)?;
300        options.apply_to_info(&mut info);
301        Ok(info)
302    }
303
304    /// Build a decoder with explicit decode options.
305    pub fn new_with_options(input: &'a [u8], options: DecodeOptions) -> Result<Self, JpegError> {
306        let view = JpegView::parse_with_options(input, options)?;
307        DEFAULT_CONTEXT.with(|ctx| Self::from_view_in_context(view, &mut ctx.borrow_mut()))
308    }
309
310    /// Build a decoder ready for `decode_into`. Parses the full header, pre-
311    /// builds every referenced Huffman table, and validates that the stream is
312    /// one of the SOFs this release implements.
313    ///
314    /// # Errors
315    /// - Any parse error encountered before SOS (see [`Self::inspect`]).
316    /// - [`JpegError::NotImplemented`] for SOFs that parse but are not yet
317    ///   decodable for the requested shape (for example Progressive12 or
318    ///   unsupported Lossless predictors).
319    /// - [`JpegError::MissingHuffmanTable`] if the scan references a DC/AC
320    ///   table slot that was never defined by a DHT segment.
321    pub fn new(input: &'a [u8]) -> Result<Self, JpegError> {
322        Self::new_with_options(input, DecodeOptions::default())
323    }
324
325    /// Build a decoder from a previously parsed [`JpegView`].
326    pub fn from_view(view: JpegView<'a>) -> Result<Self, JpegError> {
327        DEFAULT_CONTEXT.with(|ctx| Self::from_view_in_context(view, &mut ctx.borrow_mut()))
328    }
329
330    /// Build a decoder from a previously parsed [`JpegView`], reusing shared
331    /// compiled DHT/DQT state from `ctx` when table contents repeat.
332    pub fn from_view_in_context(
333        view: JpegView<'a>,
334        ctx: &mut DecoderContext,
335    ) -> Result<Self, JpegError> {
336        let JpegView {
337            bytes,
338            header,
339            info,
340            options,
341        } = view;
342        let backend = Backend::detect();
343        let (info, warnings, plan, progressive_plan, lossless_plan) = if matches!(
344            info.sof_kind,
345            SofKind::Progressive8 | SofKind::Progressive12
346        ) {
347            let progressive_plan = Self::build_progressive_plan(&header, &info, ctx)?;
348            let plan = Self::build_progressive_host_output_plan(&header, &info, ctx)?;
349            (
350                info,
351                Arc::<[Warning]>::from(header.warnings.as_slice()),
352                plan,
353                Some(progressive_plan),
354                None,
355            )
356        } else if info.sof_kind == SofKind::Lossless {
357            let (plan, lossless_plan) = Self::build_lossless_plan(&header, &info, ctx)?;
358            (
359                info,
360                Arc::<[Warning]>::from(header.warnings.as_slice()),
361                plan,
362                None,
363                Some(lossless_plan),
364            )
365        } else if options == DecodeOptions::default() {
366            if let Some(scan_offset) = header.sos_offset {
367                let header_prefix = &bytes[..scan_offset];
368                let (info, warnings, plan) = ctx.resolve_decode_plan(header_prefix, |ctx| {
369                    let plan = Self::build_prepared_plan(&header, &info, ctx)?;
370                    Ok((
371                        info.clone(),
372                        Arc::<[Warning]>::from(header.warnings.as_slice()),
373                        plan,
374                    ))
375                })?;
376                (info, warnings, plan, None, None)
377            } else {
378                let plan = Self::build_prepared_plan(&header, &info, ctx)?;
379                (
380                    info,
381                    Arc::<[Warning]>::from(header.warnings.as_slice()),
382                    plan,
383                    None,
384                    None,
385                )
386            }
387        } else {
388            let plan = Self::build_prepared_plan(&header, &info, ctx)?;
389            (
390                info,
391                Arc::<[Warning]>::from(header.warnings.as_slice()),
392                plan,
393                None,
394                None,
395            )
396        };
397        Ok(Self {
398            bytes,
399            info,
400            warnings,
401            backend,
402            plan,
403            progressive_plan,
404            lossless_plan,
405            cpu_entropy_checkpoints: Mutex::new(CpuCheckpointCache::default()),
406        })
407    }
408
409    fn build_prepared_plan(
410        header: &ParsedHeader,
411        info: &Info,
412        ctx: &mut DecoderContext,
413    ) -> Result<PreparedDecodePlan, JpegError> {
414        match info.sof_kind {
415            SofKind::Baseline8 | SofKind::Extended8 | SofKind::Extended12 => {}
416            other => return Err(JpegError::NotImplemented { sof: other }),
417        }
418        if info.sof_kind == SofKind::Extended12
419            && !matches!(
420                info.color_space,
421                ColorSpace::Grayscale
422                    | ColorSpace::YCbCr
423                    | ColorSpace::Rgb
424                    | ColorSpace::Cmyk
425                    | ColorSpace::Ycck
426            )
427        {
428            return Err(JpegError::NotImplemented { sof: info.sof_kind });
429        }
430        match info.color_space {
431            ColorSpace::Grayscale
432            | ColorSpace::YCbCr
433            | ColorSpace::Rgb
434            | ColorSpace::Cmyk
435            | ColorSpace::Ycck => {}
436        }
437
438        let mut dc_tables: [Option<Arc<HuffmanTable>>; 4] = Default::default();
439        let mut ac_tables: [Option<Arc<HuffmanTable>>; 4] = Default::default();
440        let scan = header.scan.as_ref().ok_or(JpegError::MissingMarker {
441            marker: MarkerKind::Sos,
442        })?;
443        if header.scan_count != 1 {
444            return Err(JpegError::InvalidSequentialScanCount {
445                sof: info.sof_kind,
446                count: header.scan_count,
447            });
448        }
449        validate_leading_component_sampling(header, info)?;
450        // Every component must declare H,V in 1..=4 per T.81 §B.2.2, and max_h
451        // must actually divide every component's H (same for V). Malformed
452        // streams can set H=0 (div-by-zero in upsample ratio), non-divisors
453        // (arbitrary ratios M2 handles), or ratios that don't produce planes
454        // that cover the image width.
455        for (h, v) in header.sampling.iter() {
456            if h == 0 || v == 0 || h > 4 || v > 4 {
457                return Err(JpegError::NotImplemented { sof: info.sof_kind });
458            }
459            if !header.sampling.max_h.is_multiple_of(h) || !header.sampling.max_v.is_multiple_of(v)
460            {
461                return Err(JpegError::NotImplemented { sof: info.sof_kind });
462            }
463        }
464        for comp in &scan.components {
465            let di = comp.dc_table as usize;
466            let ai = comp.ac_table as usize;
467            if dc_tables[di].is_none() {
468                let raw = header.huffman_tables.dc[di].as_ref().ok_or(
469                    JpegError::MissingHuffmanTable {
470                        component: comp.id,
471                        class: 0,
472                        id: comp.dc_table,
473                    },
474                )?;
475                dc_tables[di] = Some(ctx.resolve_huffman_table(raw)?);
476            }
477            if ac_tables[ai].is_none() {
478                let raw = header.huffman_tables.ac[ai].as_ref().ok_or(
479                    JpegError::MissingHuffmanTable {
480                        component: comp.id,
481                        class: 1,
482                        id: comp.ac_table,
483                    },
484                )?;
485                ac_tables[ai] = Some(ctx.resolve_huffman_table(raw)?);
486            }
487        }
488
489        build_decode_plan(header, info, &dc_tables, &ac_tables, ctx)
490    }
491
492    fn build_lossless_plan(
493        header: &ParsedHeader,
494        info: &Info,
495        ctx: &mut DecoderContext,
496    ) -> Result<(PreparedDecodePlan, PreparedLosslessPlan), JpegError> {
497        if info.sof_kind != SofKind::Lossless {
498            return Err(JpegError::NotImplemented { sof: info.sof_kind });
499        }
500        if header.scan_count != 1 {
501            return Err(JpegError::NotImplemented { sof: info.sof_kind });
502        }
503        let scan = header.scan.as_ref().ok_or(JpegError::MissingMarker {
504            marker: MarkerKind::Sos,
505        })?;
506        if !(1..=7).contains(&scan.ss) {
507            return Err(JpegError::UnsupportedPredictor { predictor: scan.ss });
508        }
509        if scan.se != 0 || scan.ah != 0 || scan.al != 0 {
510            return Err(JpegError::NotImplemented { sof: info.sof_kind });
511        }
512        let expected_components = match (info.color_space, info.bit_depth) {
513            (ColorSpace::Grayscale, 8 | 16) => 1,
514            (ColorSpace::Rgb, 8 | 16) => 3,
515            (ColorSpace::YCbCr, 8 | 16) => 3,
516            _ => return Err(JpegError::NotImplemented { sof: info.sof_kind }),
517        };
518        if scan.components.len() != expected_components {
519            return Err(JpegError::UnsupportedComponentCount {
520                count: scan.components.len() as u8,
521            });
522        }
523        let empty_raw = RawHuffmanTable {
524            bits: [0; 16],
525            values: HuffmanValues::default(),
526        };
527        let empty_huffman = ctx.resolve_huffman_table(&empty_raw)?;
528        let mut components = Vec::with_capacity(scan.components.len());
529        let mut first_dc_table = None;
530        for scan_component in scan.components.iter().copied() {
531            let component_index = find_component_index(&header.component_ids, scan_component.id)
532                .ok_or(JpegError::UnknownScanComponent {
533                    offset: header.sos_offset.unwrap_or_default(),
534                    component: scan_component.id,
535                })?;
536            let (h, v) =
537                header
538                    .sampling
539                    .component(component_index)
540                    .ok_or(JpegError::MissingMarker {
541                        marker: MarkerKind::Sof,
542                    })?;
543            let raw_dc = header.huffman_tables.dc[scan_component.dc_table as usize]
544                .as_ref()
545                .ok_or(JpegError::MissingHuffmanTable {
546                    component: scan_component.id,
547                    class: 0,
548                    id: scan_component.dc_table,
549                })?;
550            let dc_table = ctx.resolve_huffman_table(raw_dc)?;
551            first_dc_table.get_or_insert_with(|| Arc::clone(&dc_table));
552            components.push(PreparedComponentPlan {
553                h,
554                v,
555                output_index: component_index,
556                quant: ctx.resolve_quant_table([1; 64]),
557                dc_table,
558                ac_table: Arc::clone(&empty_huffman),
559            });
560        }
561        if matches!(info.color_space, ColorSpace::Rgb | ColorSpace::YCbCr)
562            && lossless_color_sampling(info).is_none()
563        {
564            return Err(JpegError::NotImplemented { sof: info.sof_kind });
565        }
566        let plan = PreparedDecodePlan {
567            components,
568            sampling: info.sampling,
569            color_space: info.color_space,
570            restart_interval: header.restart_interval,
571            dimensions: info.dimensions,
572            scan_offset: header.sos_offset.ok_or(JpegError::MissingMarker {
573                marker: MarkerKind::Sos,
574            })?,
575            scratch_bytes: compute_lossless_scratch_bytes(info, DEFAULT_MAX_DECODE_BYTES)?,
576        };
577        let lossless = PreparedLosslessPlan {
578            predictor: scan.ss,
579            bit_depth: info.bit_depth,
580            dc_table: first_dc_table.ok_or(JpegError::MissingMarker {
581                marker: MarkerKind::Sos,
582            })?,
583            dimensions: info.dimensions,
584            scan_offset: plan.scan_offset,
585        };
586        Ok((plan, lossless))
587    }
588
589    fn build_progressive_plan(
590        header: &ParsedHeader,
591        info: &Info,
592        ctx: &mut DecoderContext,
593    ) -> Result<PreparedProgressivePlan, JpegError> {
594        if !matches!(
595            info.sof_kind,
596            SofKind::Progressive8 | SofKind::Progressive12
597        ) {
598            return Err(JpegError::NotImplemented { sof: info.sof_kind });
599        }
600        match (info.sof_kind, info.color_space) {
601            (
602                SofKind::Progressive8 | SofKind::Progressive12,
603                ColorSpace::Grayscale | ColorSpace::YCbCr | ColorSpace::Rgb,
604            ) => {}
605            (SofKind::Progressive12, ColorSpace::Cmyk | ColorSpace::Ycck) => {}
606            (_, color_space) => return Err(JpegError::UnsupportedColorSpace { color_space }),
607        }
608        validate_sampling_factors(header, info)?;
609        if header.progressive_scans.is_empty() {
610            return Err(JpegError::MissingMarker {
611                marker: MarkerKind::Sos,
612            });
613        }
614
615        let max_h = u32::from(header.sampling.max_h);
616        let max_v = u32::from(header.sampling.max_v);
617        let mcu_cols = info.dimensions.0.div_ceil(8 * max_h);
618        let mcu_rows = info.dimensions.1.div_ceil(8 * max_v);
619        let mut components = Vec::with_capacity(header.component_ids.len());
620        for (output_index, &id) in header.component_ids.iter().enumerate() {
621            let (h, v) =
622                header
623                    .sampling
624                    .component(output_index)
625                    .ok_or(JpegError::MissingMarker {
626                        marker: MarkerKind::Sof,
627                    })?;
628            let quant_id =
629                *header
630                    .quant_table_ids
631                    .get(output_index)
632                    .ok_or(JpegError::MissingMarker {
633                        marker: MarkerKind::Sof,
634                    })? as usize;
635            let quant = *header
636                .quant_tables
637                .entries
638                .get(quant_id)
639                .and_then(|q| q.as_ref())
640                .ok_or(JpegError::MissingQuantTable {
641                    component: id,
642                    table_id: quant_id as u8,
643                })?;
644            components.push(PreparedProgressiveComponentPlan {
645                h,
646                v,
647                output_index,
648                quant: ctx.resolve_quant_table(quant),
649                block_cols: mcu_cols * u32::from(h),
650                block_rows: mcu_rows * u32::from(v),
651                sample_width: info
652                    .dimensions
653                    .0
654                    .saturating_mul(u32::from(h))
655                    .div_ceil(max_h),
656                sample_height: info
657                    .dimensions
658                    .1
659                    .saturating_mul(u32::from(v))
660                    .div_ceil(max_v),
661            });
662        }
663
664        let mut scans = Vec::with_capacity(header.progressive_scans.len());
665        for parsed in &header.progressive_scans {
666            let mut scan_components = Vec::with_capacity(parsed.scan.components.len());
667            for component in &parsed.scan.components {
668                let component_index = find_component_index(&header.component_ids, component.id)
669                    .ok_or(JpegError::UnknownScanComponent {
670                        offset: parsed.entropy_offset,
671                        component: component.id,
672                    })?;
673                let quant_id = *header.quant_table_ids.get(component_index).ok_or(
674                    JpegError::MissingMarker {
675                        marker: MarkerKind::Sof,
676                    },
677                )?;
678                let _ = parsed
679                    .quant_tables
680                    .entries
681                    .get(quant_id as usize)
682                    .and_then(|q| q.as_ref())
683                    .ok_or(JpegError::MissingQuantTable {
684                        component: component.id,
685                        table_id: quant_id,
686                    })?;
687                let dc_table = if parsed.scan.ss == 0 {
688                    Some(resolve_progressive_huffman(
689                        ctx,
690                        &parsed.huffman_tables.dc,
691                        component.id,
692                        0,
693                        component.dc_table,
694                    )?)
695                } else {
696                    None
697                };
698                let ac_table = if parsed.scan.ss > 0 {
699                    Some(resolve_progressive_huffman(
700                        ctx,
701                        &parsed.huffman_tables.ac,
702                        component.id,
703                        1,
704                        component.ac_table,
705                    )?)
706                } else {
707                    None
708                };
709                scan_components.push(PreparedProgressiveScanComponent {
710                    component_index,
711                    dc_table,
712                    ac_table,
713                });
714            }
715            scans.push(PreparedProgressiveScan {
716                components: scan_components,
717                ss: parsed.scan.ss,
718                se: parsed.scan.se,
719                ah: parsed.scan.ah,
720                al: parsed.scan.al,
721                entropy_offset: parsed.entropy_offset,
722                restart_interval: parsed.restart_interval,
723            });
724        }
725
726        let scratch_bytes =
727            compute_progressive_scratch_bytes(&components, info.dimensions.0 as usize)?;
728        Ok(PreparedProgressivePlan {
729            components,
730            scans,
731            sampling: info.sampling,
732            color_space: info.color_space,
733            dimensions: info.dimensions,
734            mcu_cols,
735            mcu_rows,
736            scratch_bytes,
737        })
738    }
739
740    fn build_progressive_host_output_plan(
741        header: &ParsedHeader,
742        info: &Info,
743        ctx: &mut DecoderContext,
744    ) -> Result<PreparedDecodePlan, JpegError> {
745        let empty_raw = RawHuffmanTable {
746            bits: [0; 16],
747            values: HuffmanValues::default(),
748        };
749        let empty_huffman = ctx.resolve_huffman_table(&empty_raw)?;
750        let mut components = Vec::with_capacity(header.component_ids.len());
751        for (output_index, &id) in header.component_ids.iter().enumerate() {
752            let (h, v) =
753                header
754                    .sampling
755                    .component(output_index)
756                    .ok_or(JpegError::MissingMarker {
757                        marker: MarkerKind::Sof,
758                    })?;
759            let quant_id =
760                *header
761                    .quant_table_ids
762                    .get(output_index)
763                    .ok_or(JpegError::MissingMarker {
764                        marker: MarkerKind::Sof,
765                    })? as usize;
766            let quant = *header
767                .quant_tables
768                .entries
769                .get(quant_id)
770                .and_then(|q| q.as_ref())
771                .ok_or(JpegError::MissingQuantTable {
772                    component: id,
773                    table_id: quant_id as u8,
774                })?;
775            components.push(PreparedComponentPlan {
776                h,
777                v,
778                output_index,
779                quant: ctx.resolve_quant_table(quant),
780                dc_table: Arc::clone(&empty_huffman),
781                ac_table: Arc::clone(&empty_huffman),
782            });
783        }
784        Ok(PreparedDecodePlan {
785            components,
786            sampling: info.sampling,
787            color_space: info.color_space,
788            restart_interval: header.restart_interval,
789            dimensions: info.dimensions,
790            scan_offset: header.sos_offset.ok_or(JpegError::MissingMarker {
791                marker: MarkerKind::Sos,
792            })?,
793            scratch_bytes: compute_decode_scratch_bytes(
794                info.dimensions,
795                info.sampling,
796                DEFAULT_MAX_DECODE_BYTES,
797            )?,
798        })
799    }
800
801    /// The parsed header as a public [`Info`].
802    pub fn info(&self) -> &Info {
803        &self.info
804    }
805
806    /// Return a byte-preserving passthrough candidate for this decoded stream.
807    pub fn passthrough_candidate(&self) -> Option<PassthroughCandidate<'a>> {
808        jpeg_passthrough_syntax(&self.info).map(|transfer_syntax| {
809            PassthroughCandidate::new(
810                self.bytes,
811                transfer_syntax,
812                CompressedPayloadKind::JpegInterchange,
813                self.info.to_core_info(),
814            )
815        })
816    }
817
818    /// Build a restart-marker byte-offset index for the first scan.
819    ///
820    /// Offsets are absolute byte positions in the original JPEG byte slice.
821    /// Returns `Ok(None)` when the stream has no non-zero DRI marker.
822    pub fn restart_index(&self) -> Result<Option<RestartIndex>, JpegError> {
823        restart_index_for_stream(
824            self.bytes,
825            Some(self.plan.scan_offset),
826            &self.info,
827            self.plan.restart_interval,
828        )
829    }
830
831    /// Decode the full image into the caller's buffer.
832    ///
833    /// # Errors
834    /// - [`JpegError::OutputBufferTooSmall`] or [`JpegError::InvalidStride`]
835    ///   if the provided buffer/stride cannot hold the image at `fmt`.
836    /// - [`JpegError::NotImplemented`] if `fmt` requests a raw output the
837    ///   current release does not emit (e.g. `RawYCbCr8`).
838    /// - Any entropy- or structural-decode error from the scan walker.
839    pub fn decode_into(
840        &self,
841        out: &mut [u8],
842        stride: usize,
843        fmt: PixelFormat,
844    ) -> Result<DecodeOutcome, JpegError> {
845        DEFAULT_SCRATCH
846            .with(|pool| self.decode_into_with_scratch(&mut pool.borrow_mut(), out, stride, fmt))
847    }
848
849    /// Decode the full image into a freshly allocated tightly-packed buffer.
850    ///
851    /// This is the owned-output counterpart to [`Self::decode_into`]. It
852    /// avoids pre-zeroing the destination buffer, which matters on WSI-sized
853    /// RGB outputs where the allocation itself can otherwise dominate the
854    /// benchmark.
855    pub fn decode(&self, fmt: PixelFormat) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
856        DEFAULT_SCRATCH.with(|pool| self.decode_with_scratch(&mut pool.borrow_mut(), fmt))
857    }
858
859    /// Decode the full image into the caller's buffer using the core
860    /// `PixelFormat` + `Downscale` contract.
861    pub fn decode_scaled_into(
862        &self,
863        out: &mut [u8],
864        stride: usize,
865        fmt: PixelFormat,
866        scale: Downscale,
867    ) -> Result<DecodeOutcome, JpegError> {
868        DEFAULT_SCRATCH.with(|pool| {
869            self.decode_scaled_into_with_scratch(&mut pool.borrow_mut(), out, stride, fmt, scale)
870        })
871    }
872
873    /// Decode the full image into a freshly allocated tightly-packed buffer
874    /// using the core `PixelFormat` + `Downscale` contract.
875    pub fn decode_scaled(
876        &self,
877        fmt: PixelFormat,
878        scale: Downscale,
879    ) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
880        DEFAULT_SCRATCH
881            .with(|pool| self.decode_scaled_with_scratch(&mut pool.borrow_mut(), fmt, scale))
882    }
883
884    /// [`Self::decode`] with caller-owned scratch.
885    pub fn decode_with_scratch(
886        &self,
887        pool: &mut ScratchPool,
888        fmt: PixelFormat,
889    ) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
890        self.decode_scaled_with_scratch(pool, fmt, Downscale::None)
891    }
892
893    /// [`Self::decode_scaled`] with caller-owned scratch.
894    pub fn decode_scaled_with_scratch(
895        &self,
896        pool: &mut ScratchPool,
897        fmt: PixelFormat,
898        scale: Downscale,
899    ) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
900        let legacy = output_format_from_parts(self.info.sof_kind, fmt, scale)?;
901        let downscale = legacy.downscale();
902        let (width, height) = scaled_dimensions(self.info.dimensions, downscale);
903        let (stride, len) = checked_output_geometry(width, height, legacy.bytes_per_pixel())?;
904        let mut out = allocate_output_buffer(len);
905        let outcome = self.decode_scaled_into_with_scratch(pool, &mut out, stride, fmt, scale)?;
906        Ok((out, outcome))
907    }
908
909    /// Decode the full image into the caller's buffer, reusing the supplied
910    /// [`ScratchPool`]. On a long-running tile batch this eliminates the
911    /// per-tile allocation of stripe buffers, the DC predictor, and the
912    /// chroma upsample rows — the realistic WSI reader shape. The first
913    /// call against a fresh pool does the allocation; subsequent calls at
914    /// the same-or-smaller shape reuse the underlying `Vec`s.
915    ///
916    /// # Errors
917    /// Identical to [`Self::decode_into`].
918    pub fn decode_into_with_scratch(
919        &self,
920        pool: &mut ScratchPool,
921        out: &mut [u8],
922        stride: usize,
923        fmt: PixelFormat,
924    ) -> Result<DecodeOutcome, JpegError> {
925        self.decode_scaled_into_with_scratch(pool, out, stride, fmt, Downscale::None)
926    }
927
928    fn decode_into_output_format_with_scratch(
929        &self,
930        pool: &mut ScratchPool,
931        out: &mut [u8],
932        stride: usize,
933        fmt: OutputFormat,
934    ) -> Result<DecodeOutcome, JpegError> {
935        let profile_enabled = jpeg_profile_stages_enabled();
936        let total_start = profile_enabled.then(Instant::now);
937        let downscale = fmt.downscale();
938        let (w, h) = scaled_dimensions(self.info.dimensions, downscale);
939        let scratch_bytes = self.decode_scratch_bytes(DEFAULT_MAX_DECODE_BYTES)?;
940        let bpp = fmt.bytes_per_pixel();
941        validate_buffer(out, stride, w, h, bpp)?;
942        let decode_start = profile_enabled.then(Instant::now);
943        let result = match fmt {
944            OutputFormat::Rgb8 | OutputFormat::Rgb8Scaled { .. } => {
945                if self.lossless_plan.is_some() {
946                    return match self.info.color_space {
947                        ColorSpace::YCbCr => self.decode_lossless_ycbcr8_region_scaled_into(
948                            out,
949                            stride,
950                            Rect::full(self.info.dimensions),
951                            downscale,
952                        ),
953                        _ => self.decode_lossless_rgb8_region_scaled_into(
954                            out,
955                            stride,
956                            Rect::full(self.info.dimensions),
957                            downscale,
958                        ),
959                    };
960                }
961                let mut writer = Rgb8Writer::new_with_backend(out, stride, w, self.backend);
962                self.decode_rgb_with_writer(
963                    pool,
964                    &mut writer,
965                    downscale,
966                    Rect::full(self.info.dimensions),
967                )
968            }
969            OutputFormat::Rgba8 { alpha } | OutputFormat::Rgba8Scaled { alpha, .. } => {
970                if self.lossless_plan.is_some() {
971                    return self.decode_lossless_rgba8_region_into(
972                        out,
973                        stride,
974                        Rect::full(self.info.dimensions),
975                        downscale,
976                        alpha,
977                    );
978                }
979                let mut writer = Rgba8Writer::new_with_backend(out, stride, w, alpha, self.backend);
980                self.decode_with_writer(
981                    pool,
982                    &mut writer,
983                    downscale,
984                    Rect::full(self.info.dimensions),
985                )
986            }
987            OutputFormat::Gray8 | OutputFormat::Gray8Scaled { .. } => {
988                if self.lossless_plan.is_some() {
989                    return self.decode_lossless_gray8_region_scaled_into(
990                        out,
991                        stride,
992                        Rect::full(self.info.dimensions),
993                        downscale,
994                    );
995                }
996                let mut writer = Gray8Writer::new(out, stride, w);
997                self.decode_with_writer(
998                    pool,
999                    &mut writer,
1000                    downscale,
1001                    Rect::full(self.info.dimensions),
1002                )
1003            }
1004            OutputFormat::Gray16 => {
1005                if self.lossless_plan.is_some() {
1006                    return self.decode_lossless_gray16_region_scaled_into(
1007                        out,
1008                        stride,
1009                        Rect::full(self.info.dimensions),
1010                        downscale,
1011                    );
1012                }
1013                if self.info.sof_kind == SofKind::Progressive12 {
1014                    return self.decode_progressive12_gray16_region_scaled_into(
1015                        out,
1016                        stride,
1017                        Rect::full(self.info.dimensions),
1018                        downscale,
1019                    );
1020                }
1021                self.decode_extended12_gray16_into(out, stride)
1022            }
1023            OutputFormat::Gray16Scaled { .. } => {
1024                if self.lossless_plan.is_some() {
1025                    return self.decode_lossless_gray16_region_scaled_into(
1026                        out,
1027                        stride,
1028                        Rect::full(self.info.dimensions),
1029                        downscale,
1030                    );
1031                }
1032                if self.info.sof_kind == SofKind::Progressive12 {
1033                    return self.decode_progressive12_gray16_region_scaled_into(
1034                        out,
1035                        stride,
1036                        Rect::full(self.info.dimensions),
1037                        downscale,
1038                    );
1039                }
1040                self.decode_extended12_gray16_region_scaled_into(
1041                    out,
1042                    stride,
1043                    Rect::full(self.info.dimensions),
1044                    downscale,
1045                )
1046            }
1047            OutputFormat::Rgb16 => {
1048                if self.lossless_plan.is_some() {
1049                    return match self.info.color_space {
1050                        ColorSpace::YCbCr => self.decode_lossless_ycbcr16_region_scaled_into(
1051                            out,
1052                            stride,
1053                            Rect::full(self.info.dimensions),
1054                            downscale,
1055                        ),
1056                        _ => self.decode_lossless_rgb16_region_scaled_into(
1057                            out,
1058                            stride,
1059                            Rect::full(self.info.dimensions),
1060                            downscale,
1061                        ),
1062                    };
1063                }
1064                if self.info.sof_kind == SofKind::Progressive12 {
1065                    return self.decode_progressive12_rgb16_region_scaled_into(
1066                        out,
1067                        stride,
1068                        Rect::full(self.info.dimensions),
1069                        downscale,
1070                    );
1071                }
1072                self.decode_extended12_rgb16_into(out, stride)
1073            }
1074            OutputFormat::Rgb16Scaled { .. } => {
1075                if self.lossless_plan.is_some() {
1076                    return match self.info.color_space {
1077                        ColorSpace::YCbCr => self.decode_lossless_ycbcr16_region_scaled_into(
1078                            out,
1079                            stride,
1080                            Rect::full(self.info.dimensions),
1081                            downscale,
1082                        ),
1083                        _ => self.decode_lossless_rgb16_region_scaled_into(
1084                            out,
1085                            stride,
1086                            Rect::full(self.info.dimensions),
1087                            downscale,
1088                        ),
1089                    };
1090                }
1091                if self.info.sof_kind == SofKind::Progressive12 {
1092                    return self.decode_progressive12_rgb16_region_scaled_into(
1093                        out,
1094                        stride,
1095                        Rect::full(self.info.dimensions),
1096                        downscale,
1097                    );
1098                }
1099                self.decode_extended12_rgb16_region_scaled_into(
1100                    out,
1101                    stride,
1102                    Rect::full(self.info.dimensions),
1103                    downscale,
1104                )
1105            }
1106            OutputFormat::Rgba16 { alpha } | OutputFormat::Rgba16Scaled { alpha, .. } => {
1107                if self.lossless_plan.is_some() {
1108                    return self.decode_lossless_rgba16_region_scaled_into(
1109                        out,
1110                        stride,
1111                        Rect::full(self.info.dimensions),
1112                        downscale,
1113                        alpha,
1114                    );
1115                }
1116                if matches!(
1117                    self.info.sof_kind,
1118                    SofKind::Extended12 | SofKind::Progressive12
1119                ) {
1120                    return self.decode_12bit_rgba16_region_scaled_into(
1121                        out,
1122                        stride,
1123                        Rect::full(self.info.dimensions),
1124                        downscale,
1125                        alpha,
1126                    );
1127                }
1128                Err(JpegError::NotImplemented {
1129                    sof: self.info.sof_kind,
1130                })
1131            }
1132        };
1133        if let (Some(total_start), Some(decode_start), Ok(outcome)) =
1134            (total_start, decode_start, &result)
1135        {
1136            let source_width_s = self.info.dimensions.0.to_string();
1137            let source_height_s = self.info.dimensions.1.to_string();
1138            let output_width_s = w.to_string();
1139            let output_height_s = h.to_string();
1140            let stride_s = stride.to_string();
1141            let bpp_s = bpp.to_string();
1142            let output_bytes_s = stride.saturating_mul(h as usize).to_string();
1143            let scratch_bytes_s = scratch_bytes.to_string();
1144            let warning_count_s = outcome.warnings.len().to_string();
1145            let decode_us = duration_us_string(decode_start.elapsed());
1146            let total_us = duration_us_string(total_start.elapsed());
1147            emit_jpeg_profile_row(
1148                "decode",
1149                "cpu",
1150                &[
1151                    ("mode", "full"),
1152                    ("fmt", output_format_profile_name(fmt)),
1153                    ("downscale", downscale_profile_name(downscale)),
1154                    ("source_width", source_width_s.as_str()),
1155                    ("source_height", source_height_s.as_str()),
1156                    ("output_width", output_width_s.as_str()),
1157                    ("output_height", output_height_s.as_str()),
1158                    ("stride", stride_s.as_str()),
1159                    ("bpp", bpp_s.as_str()),
1160                    ("scratch_bytes", scratch_bytes_s.as_str()),
1161                    ("output_bytes", output_bytes_s.as_str()),
1162                    ("decode_us", decode_us.as_str()),
1163                    ("total_us", total_us.as_str()),
1164                    ("warnings", warning_count_s.as_str()),
1165                ],
1166            );
1167        }
1168        result
1169    }
1170
1171    /// [`Self::decode_scaled_into`] with caller-owned scratch.
1172    pub fn decode_scaled_into_with_scratch(
1173        &self,
1174        pool: &mut ScratchPool,
1175        out: &mut [u8],
1176        stride: usize,
1177        fmt: PixelFormat,
1178        scale: Downscale,
1179    ) -> Result<DecodeOutcome, JpegError> {
1180        self.decode_into_output_format_with_scratch(
1181            pool,
1182            out,
1183            stride,
1184            output_format_from_parts(self.info.sof_kind, fmt, scale)?,
1185        )
1186    }
1187
1188    /// Decode the full image into rows delivered to `sink`.
1189    ///
1190    /// DCT-backed and 8-bit lossless color paths emit interleaved RGB8 rows.
1191    /// Lossless 16-bit grayscale SOF3 emits little-endian Gray16 rows, and
1192    /// supported lossless 16-bit color SOF3 emits little-endian Rgb16 rows.
1193    pub fn decode_rows<S>(&self, sink: &mut S) -> Result<DecodeOutcome, JpegError>
1194    where
1195        S: RowSink<u8, Error = JpegError>,
1196    {
1197        DEFAULT_SCRATCH.with(|pool| self.decode_rows_with_scratch(&mut pool.borrow_mut(), sink))
1198    }
1199
1200    /// [`Self::decode_rows`] with caller-owned scratch. See
1201    /// [`Self::decode_into_with_scratch`] for the reuse contract.
1202    pub fn decode_rows_with_scratch<S>(
1203        &self,
1204        pool: &mut ScratchPool,
1205        sink: &mut S,
1206    ) -> Result<DecodeOutcome, JpegError>
1207    where
1208        S: RowSink<u8, Error = JpegError>,
1209    {
1210        if self.lossless_plan.is_some() {
1211            return self.decode_lossless_rows_with_scratch(pool, sink);
1212        }
1213        let width = self.info.dimensions.0 as usize;
1214        let rows = pool.take_sink_rows(width);
1215        let mut writer = SinkWriter::new(sink, rows, self.backend);
1216        let result = self.decode_rgb_with_writer(
1217            pool,
1218            &mut writer,
1219            DownscaleFactor::Full,
1220            Rect::full(self.info.dimensions),
1221        );
1222        pool.restore_sink_rows(writer.into_rows());
1223        result
1224    }
1225
1226    fn decode_lossless_rows_with_scratch<S>(
1227        &self,
1228        pool: &mut ScratchPool,
1229        sink: &mut S,
1230    ) -> Result<DecodeOutcome, JpegError>
1231    where
1232        S: RowSink<u8, Error = JpegError>,
1233    {
1234        let plan = self
1235            .lossless_plan
1236            .as_ref()
1237            .ok_or(JpegError::NotImplemented {
1238                sof: self.info.sof_kind,
1239            })?;
1240        if !(1..=7).contains(&plan.predictor) {
1241            return Err(JpegError::UnsupportedPredictor {
1242                predictor: plan.predictor,
1243            });
1244        }
1245
1246        let width = self.info.dimensions.0 as usize;
1247        match (self.info.color_space, plan.bit_depth) {
1248            (ColorSpace::Grayscale, 8) => {
1249                let mut rows = pool.take_sink_rows(width);
1250                let result = self.decode_lossless_gray8_rows(
1251                    sink,
1252                    &mut pool.lossless_prev_row,
1253                    &mut pool.lossless_curr_row,
1254                    &mut rows.top_row,
1255                );
1256                pool.restore_sink_rows(rows);
1257                result
1258            }
1259            (ColorSpace::Grayscale, 16) => self.decode_lossless_gray16_rows(
1260                sink,
1261                &mut pool.lossless_prev_row,
1262                &mut pool.lossless_curr_row,
1263            ),
1264            (ColorSpace::Rgb, 8) => self.decode_lossless_color8_rows(
1265                sink,
1266                &mut pool.lossless_prev_row,
1267                &mut pool.lossless_curr_row,
1268                None,
1269                ColorSpace::Rgb,
1270            ),
1271            (ColorSpace::YCbCr, 8) => {
1272                let mut rows = pool.take_sink_rows(width);
1273                let result = self.decode_lossless_color8_rows(
1274                    sink,
1275                    &mut pool.lossless_prev_row,
1276                    &mut pool.lossless_curr_row,
1277                    Some(&mut rows.top_row),
1278                    ColorSpace::YCbCr,
1279                );
1280                pool.restore_sink_rows(rows);
1281                result
1282            }
1283            (ColorSpace::Rgb, 16) => self.decode_lossless_color16_rows(
1284                sink,
1285                &mut pool.lossless_prev_row,
1286                &mut pool.lossless_curr_row,
1287                None,
1288                ColorSpace::Rgb,
1289            ),
1290            (ColorSpace::YCbCr, 16) => {
1291                let mut rows = pool.take_sink_rows(width);
1292                rows.top_row.resize(width.saturating_mul(6), 0);
1293                let result = self.decode_lossless_color16_rows(
1294                    sink,
1295                    &mut pool.lossless_prev_row,
1296                    &mut pool.lossless_curr_row,
1297                    Some(&mut rows.top_row),
1298                    ColorSpace::YCbCr,
1299                );
1300                pool.restore_sink_rows(rows);
1301                result
1302            }
1303            (_, depth) if depth != 8 && depth != 16 => {
1304                Err(JpegError::UnsupportedBitDepth { depth })
1305            }
1306            _ => Err(JpegError::NotImplemented {
1307                sof: self.info.sof_kind,
1308            }),
1309        }
1310    }
1311
1312    /// Decode the full image into component rows.
1313    pub fn decode_component_rows_with_scratch<W>(
1314        &self,
1315        pool: &mut ScratchPool,
1316        writer: &mut W,
1317    ) -> Result<DecodeOutcome, JpegError>
1318    where
1319        W: ComponentRowWriter,
1320    {
1321        self.decode_region_component_rows_with_scratch(
1322            pool,
1323            writer,
1324            Rect::full(self.info.dimensions),
1325            Downscale::None,
1326        )
1327    }
1328
1329    /// Decode `roi` into component rows, optionally at a reduced scale.
1330    pub fn decode_region_component_rows_with_scratch<W>(
1331        &self,
1332        pool: &mut ScratchPool,
1333        writer: &mut W,
1334        roi: Rect,
1335        scale: Downscale,
1336    ) -> Result<DecodeOutcome, JpegError>
1337    where
1338        W: ComponentRowWriter,
1339    {
1340        if !roi.is_within(self.info.dimensions) {
1341            return Err(JpegError::RectOutOfBounds {
1342                rect: roi,
1343                width: self.info.dimensions.0,
1344                height: self.info.dimensions.1,
1345            });
1346        }
1347
1348        let downscale = jpeg_downscale(scale);
1349        let scaled_roi = scaled_rect_covering(roi, downscale)?;
1350        let mut adapter = ComponentWriterAdapter { inner: writer };
1351
1352        if roi == Rect::full(self.info.dimensions) {
1353            self.decode_with_writer(pool, &mut adapter, downscale, roi)
1354        } else {
1355            let (source_x0, source_width) =
1356                self.source_window_for_output_rect(downscale, scaled_roi);
1357            let mut cropped = CroppedWriter::new(adapter, scaled_roi, source_x0, source_width);
1358            self.decode_with_writer(pool, &mut cropped, downscale, roi)
1359        }
1360    }
1361
1362    /// Decode a rectangular region of the image into the caller's buffer.
1363    ///
1364    /// `roi` is expressed in source-image coordinates. If `fmt` requests a
1365    /// downscaled output, the written pixels cover the corresponding bounding
1366    /// box in the scaled image grid.
1367    pub fn decode_region_into(
1368        &self,
1369        out: &mut [u8],
1370        stride: usize,
1371        fmt: PixelFormat,
1372        roi: Rect,
1373    ) -> Result<DecodeOutcome, JpegError> {
1374        DEFAULT_SCRATCH.with(|pool| {
1375            self.decode_region_into_with_scratch(&mut pool.borrow_mut(), out, stride, fmt, roi)
1376        })
1377    }
1378
1379    /// Decode `roi` into a freshly allocated tightly-packed buffer.
1380    pub fn decode_region(
1381        &self,
1382        fmt: PixelFormat,
1383        roi: Rect,
1384    ) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
1385        DEFAULT_SCRATCH
1386            .with(|pool| self.decode_region_with_scratch(&mut pool.borrow_mut(), fmt, roi))
1387    }
1388
1389    /// Decode `roi` into a freshly allocated tightly-packed buffer using the
1390    /// core `PixelFormat` + `Downscale` contract.
1391    pub fn decode_region_scaled(
1392        &self,
1393        fmt: PixelFormat,
1394        roi: Rect,
1395        scale: Downscale,
1396    ) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
1397        DEFAULT_SCRATCH.with(|pool| {
1398            self.decode_region_scaled_with_scratch(&mut pool.borrow_mut(), fmt, roi, scale)
1399        })
1400    }
1401
1402    /// [`Self::decode_region`] with caller-owned scratch.
1403    pub fn decode_region_with_scratch(
1404        &self,
1405        pool: &mut ScratchPool,
1406        fmt: PixelFormat,
1407        roi: Rect,
1408    ) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
1409        self.decode_region_scaled_with_scratch(pool, fmt, roi, Downscale::None)
1410    }
1411
1412    /// [`Self::decode_region_scaled`] with caller-owned scratch.
1413    pub fn decode_region_scaled_with_scratch(
1414        &self,
1415        pool: &mut ScratchPool,
1416        fmt: PixelFormat,
1417        roi: Rect,
1418        scale: Downscale,
1419    ) -> Result<(Vec<u8>, DecodeOutcome), JpegError> {
1420        let legacy = output_format_from_parts(self.info.sof_kind, fmt, scale)?;
1421        let scaled_roi = scaled_rect_covering(roi, legacy.downscale())?;
1422        let (stride, len) =
1423            checked_output_geometry(scaled_roi.w, scaled_roi.h, legacy.bytes_per_pixel())?;
1424        let mut out = allocate_output_buffer(len);
1425        let outcome =
1426            self.decode_region_scaled_into_with_scratch(pool, &mut out, stride, fmt, roi, scale)?;
1427        Ok((out, outcome))
1428    }
1429
1430    /// [`Self::decode_region_into`] with caller-owned scratch.
1431    pub fn decode_region_into_with_scratch(
1432        &self,
1433        pool: &mut ScratchPool,
1434        out: &mut [u8],
1435        stride: usize,
1436        fmt: PixelFormat,
1437        roi: Rect,
1438    ) -> Result<DecodeOutcome, JpegError> {
1439        self.decode_region_scaled_into_with_scratch(pool, out, stride, fmt, roi, Downscale::None)
1440    }
1441
1442    fn decode_region_into_output_format_with_scratch(
1443        &self,
1444        pool: &mut ScratchPool,
1445        out: &mut [u8],
1446        stride: usize,
1447        fmt: OutputFormat,
1448        roi: Rect,
1449    ) -> Result<DecodeOutcome, JpegError> {
1450        let profile_enabled = jpeg_profile_stages_enabled();
1451        let total_start = profile_enabled.then(Instant::now);
1452        if !roi.is_within(self.info.dimensions) {
1453            return Err(JpegError::RectOutOfBounds {
1454                rect: roi,
1455                width: self.info.dimensions.0,
1456                height: self.info.dimensions.1,
1457            });
1458        }
1459
1460        if roi == Rect::full(self.info.dimensions) {
1461            return self.decode_into_output_format_with_scratch(pool, out, stride, fmt);
1462        }
1463
1464        let downscale = fmt.downscale();
1465        let scaled_roi = scaled_rect_covering(roi, downscale)?;
1466        let scratch_bytes = self.decode_scratch_bytes(DEFAULT_MAX_DECODE_BYTES)?;
1467        validate_buffer(
1468            out,
1469            stride,
1470            scaled_roi.w,
1471            scaled_roi.h,
1472            fmt.bytes_per_pixel(),
1473        )?;
1474
1475        let decode_start = profile_enabled.then(Instant::now);
1476        let result = match fmt {
1477            OutputFormat::Rgb8 | OutputFormat::Rgb8Scaled { .. } => {
1478                if self.lossless_plan.is_some() {
1479                    return match self.info.color_space {
1480                        ColorSpace::YCbCr => self
1481                            .decode_lossless_ycbcr8_region_scaled_into(out, stride, roi, downscale),
1482                        _ => self
1483                            .decode_lossless_rgb8_region_scaled_into(out, stride, roi, downscale),
1484                    };
1485                }
1486                if fmt == OutputFormat::Rgb8
1487                    && downscale == DownscaleFactor::Full
1488                    && self.progressive_plan.is_none()
1489                    && self.plan.matches_fast_tile_shape()
1490                {
1491                    let mut writer =
1492                        Rgb8Writer::new_with_backend(out, stride, scaled_roi.w, self.backend);
1493                    let scan_bytes = &self.bytes[self.plan.scan_offset..];
1494                    let checkpoint = self.checkpoint_for_mcu(
1495                        scan_bytes,
1496                        fast_tile_region_first_decode_mcu(&self.plan, roi, DownscaleFactor::Full),
1497                    )?;
1498                    let scan_warnings = decode_scan_fast_tile_rgb_region(
1499                        &self.plan,
1500                        self.backend,
1501                        scan_bytes,
1502                        pool,
1503                        &mut writer,
1504                        roi,
1505                        checkpoint.as_ref(),
1506                    )?;
1507                    Ok(DecodeOutcome {
1508                        decoded: roi,
1509                        warnings: merged_warnings(&self.warnings, scan_warnings),
1510                    })
1511                } else if matches!(fmt, OutputFormat::Rgb8Scaled { .. })
1512                    && self.progressive_plan.is_none()
1513                    && self.plan.matches_fast_tile_shape()
1514                {
1515                    let mut writer =
1516                        Rgb8Writer::new_with_backend(out, stride, scaled_roi.w, self.backend);
1517                    let scan_bytes = &self.bytes[self.plan.scan_offset..];
1518                    let checkpoint = self.checkpoint_for_mcu(
1519                        scan_bytes,
1520                        fast_tile_region_first_decode_mcu(&self.plan, scaled_roi, downscale),
1521                    )?;
1522                    let scan_warnings = decode_scan_fast_tile_rgb_region_scaled(
1523                        &self.plan,
1524                        self.backend,
1525                        scan_bytes,
1526                        pool,
1527                        &mut writer,
1528                        scaled_roi,
1529                        downscale,
1530                        checkpoint.as_ref(),
1531                    )?;
1532                    Ok(DecodeOutcome {
1533                        decoded: scaled_roi,
1534                        warnings: merged_warnings(&self.warnings, scan_warnings),
1535                    })
1536                } else {
1537                    let base =
1538                        Rgb8Writer::new_with_backend(out, stride, scaled_roi.w, self.backend);
1539                    let (source_x0, source_width) =
1540                        self.source_window_for_output_rect(downscale, scaled_roi);
1541                    let mut writer = CroppedWriter::new(base, scaled_roi, source_x0, source_width);
1542                    self.decode_rgb_with_writer(pool, &mut writer, downscale, roi)
1543                }
1544            }
1545            OutputFormat::Rgba8 { alpha } | OutputFormat::Rgba8Scaled { alpha, .. } => {
1546                if self.lossless_plan.is_some() {
1547                    return self
1548                        .decode_lossless_rgba8_region_into(out, stride, roi, downscale, alpha);
1549                }
1550                let base =
1551                    Rgba8Writer::new_with_backend(out, stride, scaled_roi.w, alpha, self.backend);
1552                let (source_x0, source_width) =
1553                    self.source_window_for_output_rect(downscale, scaled_roi);
1554                let mut writer = CroppedWriter::new(base, scaled_roi, source_x0, source_width);
1555                self.decode_with_writer(pool, &mut writer, downscale, roi)
1556            }
1557            OutputFormat::Gray8 | OutputFormat::Gray8Scaled { .. } => {
1558                if self.lossless_plan.is_some() {
1559                    return self
1560                        .decode_lossless_gray8_region_scaled_into(out, stride, roi, downscale);
1561                }
1562                let base = Gray8Writer::new(out, stride, scaled_roi.w);
1563                let (source_x0, source_width) =
1564                    self.source_window_for_output_rect(downscale, scaled_roi);
1565                let mut writer = CroppedWriter::new(base, scaled_roi, source_x0, source_width);
1566                self.decode_with_writer(pool, &mut writer, downscale, roi)
1567            }
1568            OutputFormat::Gray16 => {
1569                if self.lossless_plan.is_some() {
1570                    return self
1571                        .decode_lossless_gray16_region_scaled_into(out, stride, roi, downscale);
1572                }
1573                if self.info.sof_kind == SofKind::Progressive12 {
1574                    return self.decode_progressive12_gray16_region_scaled_into(
1575                        out, stride, roi, downscale,
1576                    );
1577                }
1578                self.decode_extended12_gray16_region_into(out, stride, roi)
1579            }
1580            OutputFormat::Gray16Scaled { .. } => {
1581                if self.lossless_plan.is_some() {
1582                    return self
1583                        .decode_lossless_gray16_region_scaled_into(out, stride, roi, downscale);
1584                }
1585                if self.info.sof_kind == SofKind::Progressive12 {
1586                    return self.decode_progressive12_gray16_region_scaled_into(
1587                        out, stride, roi, downscale,
1588                    );
1589                }
1590                self.decode_extended12_gray16_region_scaled_into(out, stride, roi, downscale)
1591            }
1592            OutputFormat::Rgb16 => {
1593                if self.lossless_plan.is_some() {
1594                    return match self.info.color_space {
1595                        ColorSpace::YCbCr => self.decode_lossless_ycbcr16_region_scaled_into(
1596                            out, stride, roi, downscale,
1597                        ),
1598                        _ => self
1599                            .decode_lossless_rgb16_region_scaled_into(out, stride, roi, downscale),
1600                    };
1601                }
1602                if self.info.sof_kind == SofKind::Progressive12 {
1603                    return self.decode_progressive12_rgb16_region_scaled_into(
1604                        out, stride, roi, downscale,
1605                    );
1606                }
1607                self.decode_extended12_rgb16_region_into(out, stride, roi)
1608            }
1609            OutputFormat::Rgb16Scaled { .. } => {
1610                if self.lossless_plan.is_some() {
1611                    return match self.info.color_space {
1612                        ColorSpace::YCbCr => self.decode_lossless_ycbcr16_region_scaled_into(
1613                            out, stride, roi, downscale,
1614                        ),
1615                        _ => self
1616                            .decode_lossless_rgb16_region_scaled_into(out, stride, roi, downscale),
1617                    };
1618                }
1619                if self.info.sof_kind == SofKind::Progressive12 {
1620                    return self.decode_progressive12_rgb16_region_scaled_into(
1621                        out, stride, roi, downscale,
1622                    );
1623                }
1624                self.decode_extended12_rgb16_region_scaled_into(out, stride, roi, downscale)
1625            }
1626            OutputFormat::Rgba16 { alpha } | OutputFormat::Rgba16Scaled { alpha, .. } => {
1627                if self.lossless_plan.is_some() {
1628                    return self.decode_lossless_rgba16_region_scaled_into(
1629                        out, stride, roi, downscale, alpha,
1630                    );
1631                }
1632                if matches!(
1633                    self.info.sof_kind,
1634                    SofKind::Extended12 | SofKind::Progressive12
1635                ) {
1636                    return self.decode_12bit_rgba16_region_scaled_into(
1637                        out, stride, roi, downscale, alpha,
1638                    );
1639                }
1640                Err(JpegError::NotImplemented {
1641                    sof: self.info.sof_kind,
1642                })
1643            }
1644        };
1645        if let (Some(total_start), Some(decode_start), Ok(outcome)) =
1646            (total_start, decode_start, &result)
1647        {
1648            let source_width_s = self.info.dimensions.0.to_string();
1649            let source_height_s = self.info.dimensions.1.to_string();
1650            let roi_x_s = roi.x.to_string();
1651            let roi_y_s = roi.y.to_string();
1652            let roi_w_s = roi.w.to_string();
1653            let roi_h_s = roi.h.to_string();
1654            let output_width_s = scaled_roi.w.to_string();
1655            let output_height_s = scaled_roi.h.to_string();
1656            let stride_s = stride.to_string();
1657            let bpp_s = fmt.bytes_per_pixel().to_string();
1658            let output_bytes_s = stride.saturating_mul(scaled_roi.h as usize).to_string();
1659            let scratch_bytes_s = scratch_bytes.to_string();
1660            let warning_count_s = outcome.warnings.len().to_string();
1661            let decode_us = duration_us_string(decode_start.elapsed());
1662            let total_us = duration_us_string(total_start.elapsed());
1663            let mode = if downscale == DownscaleFactor::Full {
1664                "region"
1665            } else {
1666                "region_scaled"
1667            };
1668            emit_jpeg_profile_row(
1669                "decode",
1670                "cpu",
1671                &[
1672                    ("mode", mode),
1673                    ("fmt", output_format_profile_name(fmt)),
1674                    ("downscale", downscale_profile_name(downscale)),
1675                    ("source_width", source_width_s.as_str()),
1676                    ("source_height", source_height_s.as_str()),
1677                    ("roi_x", roi_x_s.as_str()),
1678                    ("roi_y", roi_y_s.as_str()),
1679                    ("roi_w", roi_w_s.as_str()),
1680                    ("roi_h", roi_h_s.as_str()),
1681                    ("output_width", output_width_s.as_str()),
1682                    ("output_height", output_height_s.as_str()),
1683                    ("stride", stride_s.as_str()),
1684                    ("bpp", bpp_s.as_str()),
1685                    ("scratch_bytes", scratch_bytes_s.as_str()),
1686                    ("output_bytes", output_bytes_s.as_str()),
1687                    ("decode_us", decode_us.as_str()),
1688                    ("total_us", total_us.as_str()),
1689                    ("warnings", warning_count_s.as_str()),
1690                ],
1691            );
1692        }
1693        result
1694    }
1695
1696    /// Decode `roi` into the caller's buffer using the core `PixelFormat` +
1697    /// `Downscale` contract.
1698    pub fn decode_region_scaled_into(
1699        &self,
1700        out: &mut [u8],
1701        stride: usize,
1702        fmt: PixelFormat,
1703        roi: Rect,
1704        scale: Downscale,
1705    ) -> Result<DecodeOutcome, JpegError> {
1706        DEFAULT_SCRATCH.with(|pool| {
1707            self.decode_region_scaled_into_with_scratch(
1708                &mut pool.borrow_mut(),
1709                out,
1710                stride,
1711                fmt,
1712                roi,
1713                scale,
1714            )
1715        })
1716    }
1717
1718    /// [`Self::decode_region_scaled_into`] with caller-owned scratch.
1719    pub fn decode_region_scaled_into_with_scratch(
1720        &self,
1721        pool: &mut ScratchPool,
1722        out: &mut [u8],
1723        stride: usize,
1724        fmt: PixelFormat,
1725        roi: Rect,
1726        scale: Downscale,
1727    ) -> Result<DecodeOutcome, JpegError> {
1728        self.decode_region_into_output_format_with_scratch(
1729            pool,
1730            out,
1731            stride,
1732            output_format_from_parts(self.info.sof_kind, fmt, scale)?,
1733            roi,
1734        )
1735    }
1736
1737    /// Decode the full image into RGBA with a caller-chosen alpha byte.
1738    pub fn decode_rgba8_into_with_alpha(
1739        &self,
1740        out: &mut [u8],
1741        stride: usize,
1742        alpha: u8,
1743    ) -> Result<DecodeOutcome, JpegError> {
1744        DEFAULT_SCRATCH.with(|pool| {
1745            self.decode_rgba8_into_with_alpha_with_scratch(
1746                &mut pool.borrow_mut(),
1747                out,
1748                stride,
1749                alpha,
1750            )
1751        })
1752    }
1753
1754    /// [`Self::decode_rgba8_into_with_alpha`] with caller-owned scratch.
1755    pub fn decode_rgba8_into_with_alpha_with_scratch(
1756        &self,
1757        pool: &mut ScratchPool,
1758        out: &mut [u8],
1759        stride: usize,
1760        alpha: u8,
1761    ) -> Result<DecodeOutcome, JpegError> {
1762        self.decode_into_output_format_with_scratch(
1763            pool,
1764            out,
1765            stride,
1766            OutputFormat::Rgba8 { alpha },
1767        )
1768    }
1769
1770    /// Decode a region into RGBA with a caller-chosen alpha byte.
1771    pub fn decode_region_rgba8_into_with_alpha(
1772        &self,
1773        out: &mut [u8],
1774        stride: usize,
1775        roi: Rect,
1776        alpha: u8,
1777    ) -> Result<DecodeOutcome, JpegError> {
1778        DEFAULT_SCRATCH.with(|pool| {
1779            self.decode_region_rgba8_into_with_alpha_with_scratch(
1780                &mut pool.borrow_mut(),
1781                out,
1782                stride,
1783                roi,
1784                alpha,
1785            )
1786        })
1787    }
1788
1789    /// [`Self::decode_region_rgba8_into_with_alpha`] with caller-owned scratch.
1790    pub fn decode_region_rgba8_into_with_alpha_with_scratch(
1791        &self,
1792        pool: &mut ScratchPool,
1793        out: &mut [u8],
1794        stride: usize,
1795        roi: Rect,
1796        alpha: u8,
1797    ) -> Result<DecodeOutcome, JpegError> {
1798        self.decode_region_into_output_format_with_scratch(
1799            pool,
1800            out,
1801            stride,
1802            OutputFormat::Rgba8 { alpha },
1803            roi,
1804        )
1805    }
1806}
1807
1808/// One-shot parse-plus-decode of an independent JPEG tile into the caller's
1809/// buffer, reusing a pre-allocated [`ScratchPool`]. This is the primitive
1810/// WSI tile-batch readers want: one function call per tile, with all
1811/// heap state external.
1812///
1813/// Parallelism is the caller's responsibility for this primitive. For
1814/// production batch decode, use [`decode_tiles_into`].
1815///
1816/// # Example
1817///
1818/// ```no_run
1819/// use j2k_jpeg::{decode_tile_into, PixelFormat, ScratchPool};
1820///
1821/// let bytes: &[u8] = &[];
1822/// let mut out = vec![0u8; 256 * 256 * 3];
1823/// let mut pool = ScratchPool::new();
1824/// decode_tile_into(bytes, &mut pool, &mut out, 256 * 3, PixelFormat::Rgb8)?;
1825/// # Ok::<(), j2k_jpeg::JpegError>(())
1826/// ```
1827///
1828/// # Errors
1829/// Forwarded from [`Decoder::new`] (parse) and
1830/// [`Decoder::decode_into_with_scratch`] (decode).
1831pub fn decode_tile_into(
1832    bytes: &[u8],
1833    pool: &mut ScratchPool,
1834    out: &mut [u8],
1835    stride: usize,
1836    fmt: PixelFormat,
1837) -> Result<DecodeOutcome, JpegError> {
1838    DEFAULT_CONTEXT.with(|ctx| {
1839        decode_tile_into_in_context(bytes, &mut ctx.borrow_mut(), pool, out, stride, fmt)
1840    })
1841}
1842
1843/// One-shot parse-plus-decode of an independent JPEG tile into the caller's
1844/// buffer, reusing both caller-owned [`DecoderContext`] and caller-owned
1845/// [`ScratchPool`].
1846pub fn decode_tile_into_in_context(
1847    bytes: &[u8],
1848    ctx: &mut DecoderContext,
1849    pool: &mut ScratchPool,
1850    out: &mut [u8],
1851    stride: usize,
1852    fmt: PixelFormat,
1853) -> Result<DecodeOutcome, JpegError> {
1854    decode_tile_into_in_context_with_options(
1855        bytes,
1856        ctx,
1857        pool,
1858        out,
1859        stride,
1860        fmt,
1861        DecodeOptions::default(),
1862    )
1863}
1864
1865/// One-shot parse-plus-decode of an independent JPEG tile into the caller's
1866/// buffer, reusing both caller-owned [`DecoderContext`] and caller-owned
1867/// [`ScratchPool`], with explicit JPEG decode options.
1868pub fn decode_tile_into_in_context_with_options(
1869    bytes: &[u8],
1870    ctx: &mut DecoderContext,
1871    pool: &mut ScratchPool,
1872    out: &mut [u8],
1873    stride: usize,
1874    fmt: PixelFormat,
1875    options: DecodeOptions,
1876) -> Result<DecodeOutcome, JpegError> {
1877    let dec = Decoder::from_view_in_context(JpegView::parse_with_options(bytes, options)?, ctx)?;
1878    dec.decode_into_with_scratch(pool, out, stride, fmt)
1879}
1880
1881pub(crate) fn decode_prepared_jpeg_tile_rgb8_in_context(
1882    input: &PreparedJpeg<'_>,
1883    ctx: &mut DecoderContext,
1884    pool: &mut ScratchPool,
1885    out: &mut [u8],
1886    stride: usize,
1887    options: DecodeOptions,
1888) -> Result<DecodedTile, JpegError> {
1889    let view = JpegView::parse_with_options(input.as_bytes(), options)?;
1890    let dimensions = view.info().dimensions;
1891    let dec = Decoder::from_view_in_context(view, ctx)?;
1892    let outcome = dec.decode_into_with_scratch(pool, out, stride, PixelFormat::Rgb8)?;
1893    Ok(DecodedTile {
1894        dimensions,
1895        decoded: outcome.decoded,
1896        warnings: outcome.warnings,
1897    })
1898}
1899
1900/// Decode prepared TIFF/WSI JPEG tiles into caller-owned RGB8 output buffers.
1901///
1902/// Returned results preserve the caller's input order. Each job carries its
1903/// own [`DecodeOptions`], allowing container metadata to resolve RGB/YCbCr
1904/// interpretation independently per tile.
1905#[must_use]
1906pub fn decode_prepared_jpeg_tiles_rgb8(
1907    jobs: &mut [PreparedJpegTileJob<'_, '_>],
1908) -> Vec<Result<DecodedTile, JpegError>> {
1909    crate::JpegBatchSession::new_one_shot(TileBatchOptions::default())
1910        .decode_prepared_jpeg_tiles_rgb8(jobs)
1911}
1912
1913/// Decode independent JPEG tiles into caller-owned output buffers using a
1914/// scoped CPU worker pool.
1915///
1916/// Each worker owns one [`DecoderContext`] and one [`ScratchPool`], so repeated
1917/// tiles reuse parsed table state and heap scratch within that worker without
1918/// sharing mutable decoder state across threads. Returned outcomes preserve
1919/// the caller's input order.
1920///
1921/// # Errors
1922/// Returns [`TileBatchError`] with the first failing tile index in input order.
1923pub fn decode_tiles_into(
1924    jobs: &mut [TileDecodeJob<'_, '_>],
1925    fmt: PixelFormat,
1926    options: TileBatchOptions,
1927) -> Result<Vec<DecodeOutcome>, TileBatchError> {
1928    decode_tiles_into_with_options(jobs, fmt, DecodeOptions::default(), options)
1929}
1930
1931/// Decode independent JPEG tiles into caller-owned output buffers using a
1932/// scoped CPU worker pool and explicit JPEG decode options.
1933///
1934/// Use this variant when container metadata has already resolved ambiguous
1935/// three-component JPEG data to RGB or YCbCr via [`DecodeOptions`].
1936///
1937/// # Errors
1938/// Returns [`TileBatchError`] with the first failing tile index in input order.
1939pub fn decode_tiles_into_with_options(
1940    jobs: &mut [TileDecodeJob<'_, '_>],
1941    fmt: PixelFormat,
1942    decode_options: DecodeOptions,
1943    options: TileBatchOptions,
1944) -> Result<Vec<DecodeOutcome>, TileBatchError> {
1945    crate::JpegBatchSession::new_one_shot(options).decode_tiles_into_with_options(
1946        jobs,
1947        fmt,
1948        decode_options,
1949    )
1950}
1951
1952/// Decode independent JPEG tiles at reduced resolution into caller-owned
1953/// output buffers using a scoped CPU worker pool.
1954///
1955/// Each worker owns one [`DecoderContext`] and one [`ScratchPool`], so repeated
1956/// tiles reuse parsed table state and heap scratch within that worker without
1957/// sharing mutable decoder state across threads. Returned outcomes preserve
1958/// the caller's input order.
1959///
1960/// # Errors
1961/// Returns [`TileBatchError`] with the first failing tile index in input order.
1962pub fn decode_tiles_scaled_into(
1963    jobs: &mut [TileScaledDecodeJob<'_, '_>],
1964    fmt: PixelFormat,
1965    options: TileBatchOptions,
1966) -> Result<Vec<DecodeOutcome>, TileBatchError> {
1967    decode_tiles_scaled_into_with_options(jobs, fmt, DecodeOptions::default(), options)
1968}
1969
1970/// Decode independent JPEG tiles at reduced resolution into caller-owned
1971/// output buffers using a scoped CPU worker pool and explicit JPEG decode
1972/// options.
1973///
1974/// # Errors
1975/// Returns [`TileBatchError`] with the first failing tile index in input order.
1976pub fn decode_tiles_scaled_into_with_options(
1977    jobs: &mut [TileScaledDecodeJob<'_, '_>],
1978    fmt: PixelFormat,
1979    decode_options: DecodeOptions,
1980    options: TileBatchOptions,
1981) -> Result<Vec<DecodeOutcome>, TileBatchError> {
1982    crate::JpegBatchSession::new_one_shot(options).decode_tiles_scaled_into_with_options(
1983        jobs,
1984        fmt,
1985        decode_options,
1986    )
1987}
1988
1989/// Decode independent JPEG tile regions at reduced resolution into
1990/// caller-owned output buffers using a scoped CPU worker pool.
1991///
1992/// Each worker owns one [`DecoderContext`] and one [`ScratchPool`], so repeated
1993/// tiles reuse parsed table state and heap scratch within that worker without
1994/// sharing mutable decoder state across threads. Returned outcomes preserve
1995/// the caller's input order.
1996///
1997/// # Errors
1998/// Returns [`TileBatchError`] with the first failing tile index in input order.
1999pub fn decode_tiles_region_scaled_into(
2000    jobs: &mut [TileRegionScaledDecodeJob<'_, '_>],
2001    fmt: PixelFormat,
2002    options: TileBatchOptions,
2003) -> Result<Vec<DecodeOutcome>, TileBatchError> {
2004    decode_tiles_region_scaled_into_with_options(jobs, fmt, DecodeOptions::default(), options)
2005}
2006
2007/// Decode independent JPEG tile regions at reduced resolution into
2008/// caller-owned output buffers using a scoped CPU worker pool and explicit JPEG
2009/// decode options.
2010///
2011/// # Errors
2012/// Returns [`TileBatchError`] with the first failing tile index in input order.
2013pub fn decode_tiles_region_scaled_into_with_options(
2014    jobs: &mut [TileRegionScaledDecodeJob<'_, '_>],
2015    fmt: PixelFormat,
2016    decode_options: DecodeOptions,
2017    options: TileBatchOptions,
2018) -> Result<Vec<DecodeOutcome>, TileBatchError> {
2019    crate::JpegBatchSession::new_one_shot(options).decode_tiles_region_scaled_into_with_options(
2020        jobs,
2021        fmt,
2022        decode_options,
2023    )
2024}
2025
2026/// One-shot parse-plus-region-decode of an independent JPEG tile into the
2027/// caller's buffer, reusing both caller-owned [`DecoderContext`] and
2028/// caller-owned [`ScratchPool`].
2029pub fn decode_tile_region_into_in_context(
2030    bytes: &[u8],
2031    ctx: &mut DecoderContext,
2032    pool: &mut ScratchPool,
2033    out: &mut [u8],
2034    stride: usize,
2035    fmt: PixelFormat,
2036    roi: Rect,
2037) -> Result<DecodeOutcome, JpegError> {
2038    decode_tile_region_into_in_context_with_options(
2039        bytes,
2040        ctx,
2041        pool,
2042        out,
2043        stride,
2044        fmt,
2045        roi,
2046        DecodeOptions::default(),
2047    )
2048}
2049
2050/// One-shot parse-plus-region-decode of an independent JPEG tile into the
2051/// caller's buffer, reusing caller-owned state and explicit JPEG decode
2052/// options.
2053#[allow(clippy::too_many_arguments)]
2054pub fn decode_tile_region_into_in_context_with_options(
2055    bytes: &[u8],
2056    ctx: &mut DecoderContext,
2057    pool: &mut ScratchPool,
2058    out: &mut [u8],
2059    stride: usize,
2060    fmt: PixelFormat,
2061    roi: Rect,
2062    options: DecodeOptions,
2063) -> Result<DecodeOutcome, JpegError> {
2064    let dec = Decoder::from_view_in_context(JpegView::parse_with_options(bytes, options)?, ctx)?;
2065    dec.decode_region_into_with_scratch(pool, out, stride, fmt, roi)
2066}
2067
2068/// One-shot parse-plus-scaled-decode of an independent JPEG tile into the
2069/// caller's buffer, reusing both caller-owned [`DecoderContext`] and
2070/// caller-owned [`ScratchPool`].
2071pub fn decode_tile_scaled_into_in_context(
2072    bytes: &[u8],
2073    ctx: &mut DecoderContext,
2074    pool: &mut ScratchPool,
2075    out: &mut [u8],
2076    stride: usize,
2077    fmt: PixelFormat,
2078    scale: Downscale,
2079) -> Result<DecodeOutcome, JpegError> {
2080    decode_tile_scaled_into_in_context_with_options(
2081        bytes,
2082        ctx,
2083        pool,
2084        out,
2085        stride,
2086        fmt,
2087        scale,
2088        DecodeOptions::default(),
2089    )
2090}
2091
2092/// One-shot parse-plus-scaled-decode of an independent JPEG tile into the
2093/// caller's buffer, reusing caller-owned state and explicit JPEG decode
2094/// options.
2095#[allow(clippy::too_many_arguments)]
2096pub fn decode_tile_scaled_into_in_context_with_options(
2097    bytes: &[u8],
2098    ctx: &mut DecoderContext,
2099    pool: &mut ScratchPool,
2100    out: &mut [u8],
2101    stride: usize,
2102    fmt: PixelFormat,
2103    scale: Downscale,
2104    options: DecodeOptions,
2105) -> Result<DecodeOutcome, JpegError> {
2106    let dec = Decoder::from_view_in_context(JpegView::parse_with_options(bytes, options)?, ctx)?;
2107    dec.decode_scaled_into_with_scratch(pool, out, stride, fmt, scale)
2108}
2109
2110/// One-shot parse-plus-region-scaled-decode of an independent JPEG tile into
2111/// the caller's buffer, reusing both caller-owned [`DecoderContext`] and
2112/// caller-owned [`ScratchPool`].
2113#[allow(clippy::too_many_arguments)]
2114pub fn decode_tile_region_scaled_into_in_context(
2115    bytes: &[u8],
2116    ctx: &mut DecoderContext,
2117    pool: &mut ScratchPool,
2118    out: &mut [u8],
2119    stride: usize,
2120    fmt: PixelFormat,
2121    roi: Rect,
2122    scale: Downscale,
2123) -> Result<DecodeOutcome, JpegError> {
2124    decode_tile_region_scaled_into_in_context_with_options(
2125        bytes,
2126        ctx,
2127        pool,
2128        out,
2129        stride,
2130        fmt,
2131        roi,
2132        scale,
2133        DecodeOptions::default(),
2134    )
2135}
2136
2137/// One-shot parse-plus-region-scaled-decode of an independent JPEG tile into
2138/// the caller's buffer, reusing caller-owned state and explicit JPEG decode
2139/// options.
2140#[allow(clippy::too_many_arguments)]
2141pub fn decode_tile_region_scaled_into_in_context_with_options(
2142    bytes: &[u8],
2143    ctx: &mut DecoderContext,
2144    pool: &mut ScratchPool,
2145    out: &mut [u8],
2146    stride: usize,
2147    fmt: PixelFormat,
2148    roi: Rect,
2149    scale: Downscale,
2150    options: DecodeOptions,
2151) -> Result<DecodeOutcome, JpegError> {
2152    let dec = Decoder::from_view_in_context(JpegView::parse_with_options(bytes, options)?, ctx)?;
2153    dec.decode_region_scaled_into_with_scratch(pool, out, stride, fmt, roi, scale)
2154}
2155
2156impl Decoder<'_> {
2157    /// One-shot parse-plus-row-decode of a JPEG tile using caller-owned shared
2158    /// table context and caller-owned scratch.
2159    pub fn decode_tile<S>(
2160        bytes: &[u8],
2161        ctx: &mut DecoderContext,
2162        pool: &mut ScratchPool,
2163        sink: &mut S,
2164    ) -> Result<DecodeOutcome, JpegError>
2165    where
2166        S: RowSink<u8, Error = JpegError>,
2167    {
2168        let dec = Decoder::from_view_in_context(JpegView::parse(bytes)?, ctx)?;
2169        dec.decode_rows_with_scratch(pool, sink)
2170    }
2171}
2172
2173impl Decoder<'_> {
2174    fn decode_scratch_bytes(&self, cap: usize) -> Result<usize, JpegError> {
2175        let scratch_bytes = self
2176            .progressive_plan
2177            .as_ref()
2178            .map_or(self.plan.scratch_bytes, |plan| plan.scratch_bytes);
2179        if scratch_bytes > cap {
2180            return Err(JpegError::MemoryCapExceeded {
2181                requested: scratch_bytes,
2182                cap,
2183            });
2184        }
2185        Ok(scratch_bytes)
2186    }
2187
2188    fn checkpoint_for_mcu(
2189        &self,
2190        scan_bytes: &[u8],
2191        target_mcu: u32,
2192    ) -> Result<Option<DeviceCheckpoint>, JpegError> {
2193        if self.plan.restart_interval.is_some() || target_mcu < CPU_ROI_CHECKPOINT_MIN_TARGET_MCUS {
2194            return Ok(None);
2195        }
2196
2197        let mut cache = self
2198            .cpu_entropy_checkpoints
2199            .lock()
2200            .expect("CPU entropy checkpoint cache mutex poisoned");
2201        checkpoint_before_mcu(
2202            &self.plan,
2203            scan_bytes,
2204            CPU_ROI_CHECKPOINT_CADENCE_MCUS,
2205            target_mcu,
2206            &mut cache,
2207        )
2208    }
2209
2210    fn source_window_for_output_rect(
2211        &self,
2212        downscale: DownscaleFactor,
2213        output_rect: Rect,
2214    ) -> (u32, u32) {
2215        if self.progressive_plan.is_some() {
2216            return (0, scaled_dimensions(self.info.dimensions, downscale).0);
2217        }
2218        let layout = stripe_region_layout(&self.plan, downscale, output_rect);
2219        (layout.source_x0, layout.source_width)
2220    }
2221
2222    fn decode_with_writer<W: OutputWriter>(
2223        &self,
2224        pool: &mut ScratchPool,
2225        writer: &mut W,
2226        downscale: DownscaleFactor,
2227        decoded: Rect,
2228    ) -> Result<DecodeOutcome, JpegError> {
2229        let _ = self.decode_scratch_bytes(DEFAULT_MAX_DECODE_BYTES)?;
2230        let profile_enabled = jpeg_profile_stages_enabled();
2231        if let Some(plan) = &self.progressive_plan {
2232            let scan_start = profile_enabled.then(Instant::now);
2233            let scan_warnings = if downscale == DownscaleFactor::Full {
2234                decode_progressive(plan, self.backend, self.bytes, writer)?
2235            } else {
2236                let mut scaled =
2237                    ProgressiveDownscaleWriter::new(writer, downscale, self.info.dimensions);
2238                decode_progressive(plan, self.backend, self.bytes, &mut scaled)?
2239            };
2240            if let Some(start) = scan_start {
2241                emit_decode_scan_profile(
2242                    "progressive",
2243                    self.info.dimensions,
2244                    decoded,
2245                    downscale,
2246                    start.elapsed(),
2247                );
2248            }
2249            return Ok(DecodeOutcome {
2250                decoded,
2251                warnings: merged_warnings(&self.warnings, scan_warnings),
2252            });
2253        }
2254        let output_rect = scaled_rect_covering(decoded, downscale)?;
2255        let scan_bytes = &self.bytes[self.plan.scan_offset..];
2256        let scan_start = profile_enabled.then(Instant::now);
2257        let scan_warnings = decode_scan_baseline(
2258            &self.plan,
2259            self.backend,
2260            scan_bytes,
2261            pool,
2262            writer,
2263            downscale,
2264            output_rect,
2265        )?;
2266        if let Some(start) = scan_start {
2267            emit_decode_scan_profile(
2268                "baseline",
2269                self.info.dimensions,
2270                decoded,
2271                downscale,
2272                start.elapsed(),
2273            );
2274        }
2275        Ok(DecodeOutcome {
2276            decoded,
2277            warnings: merged_warnings(&self.warnings, scan_warnings),
2278        })
2279    }
2280
2281    fn decode_rgb_with_writer<W: OutputWriter + InterleavedRgbWriter>(
2282        &self,
2283        pool: &mut ScratchPool,
2284        writer: &mut W,
2285        downscale: DownscaleFactor,
2286        decoded: Rect,
2287    ) -> Result<DecodeOutcome, JpegError> {
2288        let _ = self.decode_scratch_bytes(DEFAULT_MAX_DECODE_BYTES)?;
2289        let profile_enabled = jpeg_profile_stages_enabled();
2290        if let Some(plan) = &self.progressive_plan {
2291            let scan_start = profile_enabled.then(Instant::now);
2292            let scan_warnings = if downscale == DownscaleFactor::Full {
2293                decode_progressive(plan, self.backend, self.bytes, writer)?
2294            } else {
2295                let mut scaled =
2296                    ProgressiveDownscaleWriter::new(writer, downscale, self.info.dimensions);
2297                decode_progressive(plan, self.backend, self.bytes, &mut scaled)?
2298            };
2299            if let Some(start) = scan_start {
2300                emit_decode_scan_profile(
2301                    "progressive_rgb",
2302                    self.info.dimensions,
2303                    decoded,
2304                    downscale,
2305                    start.elapsed(),
2306                );
2307            }
2308            return Ok(DecodeOutcome {
2309                decoded,
2310                warnings: merged_warnings(&self.warnings, scan_warnings),
2311            });
2312        }
2313        let output_rect = scaled_rect_covering(decoded, downscale)?;
2314        let scan_bytes = &self.bytes[self.plan.scan_offset..];
2315        let scan_start = profile_enabled.then(Instant::now);
2316        let (scan_path, scan_warnings) =
2317            if downscale == DownscaleFactor::Full && self.plan.matches_fast_tile_shape() {
2318                (
2319                    "fast420_rgb",
2320                    decode_scan_fast_tile_rgb(&self.plan, self.backend, scan_bytes, pool, writer)?,
2321                )
2322            } else if downscale == DownscaleFactor::Full
2323                && decoded == Rect::full(self.info.dimensions)
2324                && self.plan.matches_fast_rgb444_shape()
2325            {
2326                (
2327                    "fast444_rgb",
2328                    decode_scan_fast_rgb_444(&self.plan, self.backend, scan_bytes, pool, writer)?,
2329                )
2330            } else {
2331                (
2332                    "baseline_rgb",
2333                    decode_scan_baseline_rgb(
2334                        &self.plan,
2335                        self.backend,
2336                        scan_bytes,
2337                        pool,
2338                        writer,
2339                        downscale,
2340                        output_rect,
2341                    )?,
2342                )
2343            };
2344        if let Some(start) = scan_start {
2345            emit_decode_scan_profile(
2346                scan_path,
2347                self.info.dimensions,
2348                decoded,
2349                downscale,
2350                start.elapsed(),
2351            );
2352        }
2353        Ok(DecodeOutcome {
2354            decoded,
2355            warnings: merged_warnings(&self.warnings, scan_warnings),
2356        })
2357    }
2358
2359    fn decode_lossless_gray8_into(
2360        &self,
2361        out: &mut [u8],
2362        stride: usize,
2363    ) -> Result<DecodeOutcome, JpegError> {
2364        let plan = self
2365            .lossless_plan
2366            .as_ref()
2367            .ok_or(JpegError::NotImplemented {
2368                sof: self.info.sof_kind,
2369            })?;
2370        if plan.bit_depth != 8 {
2371            return Err(JpegError::UnsupportedBitDepth {
2372                depth: plan.bit_depth,
2373            });
2374        }
2375        if !(1..=7).contains(&plan.predictor) {
2376            return Err(JpegError::UnsupportedPredictor {
2377                predictor: plan.predictor,
2378            });
2379        }
2380
2381        let (width, height) = plan.dimensions;
2382        let scan_bytes = &self.bytes[plan.scan_offset..];
2383        let mut br = BitReader::new(scan_bytes);
2384        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
2385        let total_samples = width.saturating_mul(height);
2386        let mut samples_since_restart = 0u32;
2387        let mut expected_rst = 0u8;
2388        for y in 0..height as usize {
2389            for x in 0..width as usize {
2390                let sample_index = y as u32 * width + x as u32;
2391                if restart > 0 && samples_since_restart == restart {
2392                    consume_lossless_restart(
2393                        &mut br,
2394                        sample_index,
2395                        total_samples,
2396                        &mut expected_rst,
2397                    )?;
2398                    samples_since_restart = 0;
2399                }
2400                let predictor = if restart > 0 && samples_since_restart == 0 {
2401                    128
2402                } else {
2403                    lossless_predictor_value(plan.predictor, out, stride, x, y)
2404                };
2405                let diff = plan.dc_table.decode_fast_dc(&mut br)?;
2406                let sample = <u8 as LosslessSample>::from_i32(predictor + diff)?;
2407                out[y * stride + x] = sample;
2408                samples_since_restart += 1;
2409            }
2410        }
2411
2412        let scan_warnings = finish_scan(&mut br, true)?;
2413        Ok(DecodeOutcome {
2414            decoded: Rect::full(self.info.dimensions),
2415            warnings: merged_warnings(&self.warnings, scan_warnings),
2416        })
2417    }
2418
2419    fn decode_lossless_gray8_region_scaled_into(
2420        &self,
2421        out: &mut [u8],
2422        stride: usize,
2423        roi: Rect,
2424        downscale: DownscaleFactor,
2425    ) -> Result<DecodeOutcome, JpegError> {
2426        if roi == Rect::full(self.info.dimensions) && downscale == DownscaleFactor::Full {
2427            return self.decode_lossless_gray8_into(out, stride);
2428        }
2429
2430        let (width, height) = self.info.dimensions;
2431        let full_stride = width as usize;
2432        let mut full =
2433            allocate_output_buffer(checked_scratch_len(&[full_stride, height as usize])?);
2434        let mut outcome = self.decode_lossless_gray8_into(&mut full, full_stride)?;
2435        let output_rect = scaled_rect_covering(roi, downscale)?;
2436        copy_gray8_scaled_rect(
2437            &full,
2438            (width, height),
2439            output_rect,
2440            downscale.denominator(),
2441            out,
2442            stride,
2443        );
2444        outcome.decoded = roi;
2445        Ok(outcome)
2446    }
2447
2448    fn decode_lossless_gray_rows<P, S>(
2449        &self,
2450        sink: &mut S,
2451        prev_row: &mut Vec<u8>,
2452        curr_row: &mut Vec<u8>,
2453        mut emit_row: impl FnMut(&mut S, u32, &[u8]) -> Result<(), JpegError>,
2454    ) -> Result<DecodeOutcome, JpegError>
2455    where
2456        P: LosslessSample,
2457        S: RowSink<u8, Error = JpegError>,
2458    {
2459        let plan = self
2460            .lossless_plan
2461            .as_ref()
2462            .ok_or(JpegError::NotImplemented {
2463                sof: self.info.sof_kind,
2464            })?;
2465        if plan.bit_depth != P::BIT_DEPTH {
2466            return Err(JpegError::UnsupportedBitDepth {
2467                depth: plan.bit_depth,
2468            });
2469        }
2470        if self.info.color_space != ColorSpace::Grayscale {
2471            return Err(JpegError::NotImplemented {
2472                sof: self.info.sof_kind,
2473            });
2474        }
2475        if !(1..=7).contains(&plan.predictor) {
2476            return Err(JpegError::UnsupportedPredictor {
2477                predictor: plan.predictor,
2478            });
2479        }
2480
2481        let (width, height) = plan.dimensions;
2482        let width = width as usize;
2483        let row_len = width.saturating_mul(P::BYTES);
2484        prev_row.resize(row_len, 0);
2485        curr_row.resize(row_len, 0);
2486
2487        let scan_bytes = &self.bytes[plan.scan_offset..];
2488        let mut br = BitReader::new(scan_bytes);
2489        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
2490        let total_samples = plan.dimensions.0.saturating_mul(height);
2491        let mut samples_since_restart = 0u32;
2492        let mut expected_rst = 0u8;
2493        for y in 0..height as usize {
2494            for x in 0..width {
2495                let sample_index = y as u32 * plan.dimensions.0 + x as u32;
2496                if restart > 0 && samples_since_restart == restart {
2497                    consume_lossless_restart(
2498                        &mut br,
2499                        sample_index,
2500                        total_samples,
2501                        &mut expected_rst,
2502                    )?;
2503                    samples_since_restart = 0;
2504                }
2505                let predictor = if restart > 0 && samples_since_restart == 0 {
2506                    P::RESTART_PREDICTOR
2507                } else {
2508                    lossless_predictor_gray_rows::<P>(plan.predictor, curr_row, prev_row, x, y)
2509                };
2510                let diff = plan.dc_table.decode_fast_dc(&mut br)?;
2511                let sample = P::from_i32(predictor + diff)?;
2512                sample.write_le(&mut curr_row[x * P::BYTES..]);
2513                samples_since_restart += 1;
2514            }
2515            emit_row(sink, y as u32, &curr_row[..row_len])?;
2516            core::mem::swap(prev_row, curr_row);
2517        }
2518
2519        let scan_warnings = finish_scan(&mut br, true)?;
2520        Ok(DecodeOutcome {
2521            decoded: Rect::full(self.info.dimensions),
2522            warnings: merged_warnings(&self.warnings, scan_warnings),
2523        })
2524    }
2525
2526    fn decode_lossless_gray8_rows<S>(
2527        &self,
2528        sink: &mut S,
2529        prev_row: &mut Vec<u8>,
2530        curr_row: &mut Vec<u8>,
2531        rgb_row: &mut [u8],
2532    ) -> Result<DecodeOutcome, JpegError>
2533    where
2534        S: RowSink<u8, Error = JpegError>,
2535    {
2536        self.decode_lossless_gray_rows::<u8, S>(sink, prev_row, curr_row, |sink, y, gray_row| {
2537            let rgb_len = gray_row.len().saturating_mul(3);
2538            if rgb_row.len() < rgb_len {
2539                return Err(JpegError::OutputBufferTooSmall {
2540                    required: rgb_len,
2541                    provided: rgb_row.len(),
2542                });
2543            }
2544            for (pixel, &sample) in rgb_row[..rgb_len].chunks_exact_mut(3).zip(gray_row.iter()) {
2545                pixel.copy_from_slice(&[sample, sample, sample]);
2546            }
2547            sink.write_row(y, &rgb_row[..rgb_len])
2548        })
2549    }
2550
2551    fn decode_lossless_rgb8_into(
2552        &self,
2553        out: &mut [u8],
2554        stride: usize,
2555    ) -> Result<DecodeOutcome, JpegError> {
2556        match lossless_color_sampling(&self.info) {
2557            Some(LosslessColorSampling::S444) => {
2558                self.decode_lossless_color8_components_into(out, stride, ColorSpace::Rgb)
2559            }
2560            Some(LosslessColorSampling::S422 | LosslessColorSampling::S420) => {
2561                self.decode_lossless_color8_sampled_into(out, stride, ColorSpace::Rgb)
2562            }
2563            None => Err(JpegError::NotImplemented {
2564                sof: self.info.sof_kind,
2565            }),
2566        }
2567    }
2568
2569    fn decode_lossless_color_components_into<P>(
2570        &self,
2571        out: &mut [u8],
2572        stride: usize,
2573        color_space: ColorSpace,
2574    ) -> Result<DecodeOutcome, JpegError>
2575    where
2576        P: LosslessSample,
2577    {
2578        let plan = self
2579            .lossless_plan
2580            .as_ref()
2581            .ok_or(JpegError::NotImplemented {
2582                sof: self.info.sof_kind,
2583            })?;
2584        if plan.bit_depth != P::BIT_DEPTH {
2585            return Err(JpegError::UnsupportedBitDepth {
2586                depth: plan.bit_depth,
2587            });
2588        }
2589        if self.info.color_space != color_space {
2590            return Err(JpegError::NotImplemented {
2591                sof: self.info.sof_kind,
2592            });
2593        }
2594        if self.plan.components.len() != 3 {
2595            return Err(JpegError::UnsupportedComponentCount {
2596                count: self.plan.components.len() as u8,
2597            });
2598        }
2599        if !(1..=7).contains(&plan.predictor) {
2600            return Err(JpegError::UnsupportedPredictor {
2601                predictor: plan.predictor,
2602            });
2603        }
2604
2605        let (width, height) = plan.dimensions;
2606        let scan_bytes = &self.bytes[plan.scan_offset..];
2607        let mut br = BitReader::new(scan_bytes);
2608        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
2609        let total_pixels = width.saturating_mul(height);
2610        let mut pixels_since_restart = 0u32;
2611        let mut expected_rst = 0u8;
2612        for y in 0..height as usize {
2613            for x in 0..width as usize {
2614                let pixel_index = y as u32 * width + x as u32;
2615                if restart > 0 && pixels_since_restart == restart {
2616                    consume_lossless_restart(
2617                        &mut br,
2618                        pixel_index,
2619                        total_pixels,
2620                        &mut expected_rst,
2621                    )?;
2622                    pixels_since_restart = 0;
2623                }
2624                for component in &self.plan.components {
2625                    if component.output_index >= 3 {
2626                        return Err(JpegError::UnsupportedComponentCount {
2627                            count: self.plan.components.len() as u8,
2628                        });
2629                    }
2630                    let predictor = if restart > 0 && pixels_since_restart == 0 {
2631                        P::RESTART_PREDICTOR
2632                    } else {
2633                        lossless_predictor_color_into::<P>(
2634                            plan.predictor,
2635                            out,
2636                            stride,
2637                            x,
2638                            y,
2639                            component.output_index,
2640                        )
2641                    };
2642                    let diff = component.dc_table.decode_fast_dc(&mut br)?;
2643                    let sample = P::from_i32(predictor + diff)?;
2644                    let offset = y * stride + (x * 3 + component.output_index) * P::BYTES;
2645                    sample.write_le(&mut out[offset..]);
2646                }
2647                pixels_since_restart += 1;
2648            }
2649        }
2650
2651        let scan_warnings = finish_scan(&mut br, true)?;
2652        Ok(DecodeOutcome {
2653            decoded: Rect::full(self.info.dimensions),
2654            warnings: merged_warnings(&self.warnings, scan_warnings),
2655        })
2656    }
2657
2658    fn decode_lossless_color8_components_into(
2659        &self,
2660        out: &mut [u8],
2661        stride: usize,
2662        color_space: ColorSpace,
2663    ) -> Result<DecodeOutcome, JpegError> {
2664        self.decode_lossless_color_components_into::<u8>(out, stride, color_space)
2665    }
2666
2667    fn decode_lossless_color_sampled_into<P>(
2668        &self,
2669        out: &mut [u8],
2670        stride: usize,
2671        color_space: ColorSpace,
2672        write_output: impl FnOnce(
2673            &mut [u8],
2674            usize,
2675            ColorSpace,
2676            LosslessColorSampling,
2677            (usize, usize),
2678            LosslessColorPlanes<'_, P>,
2679        ),
2680    ) -> Result<DecodeOutcome, JpegError>
2681    where
2682        P: LosslessSample,
2683    {
2684        let plan = self
2685            .lossless_plan
2686            .as_ref()
2687            .ok_or(JpegError::NotImplemented {
2688                sof: self.info.sof_kind,
2689            })?;
2690        if plan.bit_depth != P::BIT_DEPTH {
2691            return Err(JpegError::UnsupportedBitDepth {
2692                depth: plan.bit_depth,
2693            });
2694        }
2695        if self.info.color_space != color_space {
2696            return Err(JpegError::NotImplemented {
2697                sof: self.info.sof_kind,
2698            });
2699        }
2700        let sampling = lossless_color_sampling(&self.info).ok_or(JpegError::NotImplemented {
2701            sof: self.info.sof_kind,
2702        })?;
2703        if !matches!(
2704            sampling,
2705            LosslessColorSampling::S422 | LosslessColorSampling::S420
2706        ) {
2707            return Err(JpegError::NotImplemented {
2708                sof: self.info.sof_kind,
2709            });
2710        }
2711        if self.plan.components.len() != 3 {
2712            return Err(JpegError::UnsupportedComponentCount {
2713                count: self.plan.components.len() as u8,
2714            });
2715        }
2716        if !(1..=7).contains(&plan.predictor) {
2717            return Err(JpegError::UnsupportedPredictor {
2718                predictor: plan.predictor,
2719            });
2720        }
2721
2722        let (width, height) = plan.dimensions;
2723        let width = width as usize;
2724        let height = height as usize;
2725        let chroma_width = width.div_ceil(self.info.sampling.max_h as usize);
2726        let chroma_height = height.div_ceil(self.info.sampling.max_v as usize);
2727        let mut c0 = vec![P::default(); width * height];
2728        let mut c1 = vec![P::default(); chroma_width * chroma_height];
2729        let mut c2 = vec![P::default(); chroma_width * chroma_height];
2730
2731        let scan_bytes = &self.bytes[plan.scan_offset..];
2732        let mut br = BitReader::new(scan_bytes);
2733        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
2734        let total_mcus = (chroma_width * chroma_height) as u32;
2735        let mut mcus_since_restart = 0u32;
2736        let mut expected_rst = 0u8;
2737        for mcu_y in 0..chroma_height {
2738            for mcu_x in 0..chroma_width {
2739                let mcu_index = (mcu_y * chroma_width + mcu_x) as u32;
2740                if restart > 0 && mcus_since_restart == restart {
2741                    consume_lossless_restart(&mut br, mcu_index, total_mcus, &mut expected_rst)?;
2742                    mcus_since_restart = 0;
2743                }
2744                for component in &self.plan.components {
2745                    let (plane, plane_width, plane_height) = match component.output_index {
2746                        0 => (&mut c0, width, height),
2747                        1 => (&mut c1, chroma_width, chroma_height),
2748                        2 => (&mut c2, chroma_width, chroma_height),
2749                        _ => {
2750                            return Err(JpegError::UnsupportedComponentCount {
2751                                count: self.plan.components.len() as u8,
2752                            });
2753                        }
2754                    };
2755                    for local_y in 0..component.v as usize {
2756                        for local_x in 0..component.h as usize {
2757                            let x = mcu_x * component.h as usize + local_x;
2758                            let y = mcu_y * component.v as usize + local_y;
2759                            if x >= plane_width || y >= plane_height {
2760                                continue;
2761                            }
2762                            decode_lossless_plane_sample(
2763                                &mut br,
2764                                &component.dc_table,
2765                                plan.predictor,
2766                                plane,
2767                                plane_width,
2768                                LosslessPlaneSample {
2769                                    x,
2770                                    y,
2771                                    restart_first_sample: restart > 0
2772                                        && mcus_since_restart == 0
2773                                        && local_x == 0
2774                                        && local_y == 0,
2775                                },
2776                            )?;
2777                        }
2778                    }
2779                }
2780                mcus_since_restart += 1;
2781            }
2782        }
2783
2784        let scan_warnings = finish_scan(&mut br, true)?;
2785        write_output(
2786            out,
2787            stride,
2788            color_space,
2789            sampling,
2790            (width, height),
2791            LosslessColorPlanes {
2792                c0: &c0,
2793                c1: &c1,
2794                c2: &c2,
2795            },
2796        );
2797        Ok(DecodeOutcome {
2798            decoded: Rect::full(self.info.dimensions),
2799            warnings: merged_warnings(&self.warnings, scan_warnings),
2800        })
2801    }
2802
2803    fn decode_lossless_color8_sampled_into(
2804        &self,
2805        out: &mut [u8],
2806        stride: usize,
2807        color_space: ColorSpace,
2808    ) -> Result<DecodeOutcome, JpegError> {
2809        self.decode_lossless_color_sampled_into::<u8>(
2810            out,
2811            stride,
2812            color_space,
2813            write_lossless_color8_sampled_output,
2814        )
2815    }
2816
2817    fn decode_lossless_ycbcr8_into(
2818        &self,
2819        out: &mut [u8],
2820        stride: usize,
2821    ) -> Result<DecodeOutcome, JpegError> {
2822        match lossless_color_sampling(&self.info) {
2823            Some(LosslessColorSampling::S444) => {
2824                let outcome =
2825                    self.decode_lossless_color8_components_into(out, stride, ColorSpace::YCbCr)?;
2826                convert_ycbcr8_to_rgb8_in_place(out, stride, self.info.dimensions);
2827                Ok(outcome)
2828            }
2829            Some(LosslessColorSampling::S422 | LosslessColorSampling::S420) => {
2830                self.decode_lossless_color8_sampled_into(out, stride, ColorSpace::YCbCr)
2831            }
2832            None => Err(JpegError::NotImplemented {
2833                sof: self.info.sof_kind,
2834            }),
2835        }
2836    }
2837
2838    fn decode_lossless_rgb8_region_scaled_into(
2839        &self,
2840        out: &mut [u8],
2841        stride: usize,
2842        roi: Rect,
2843        downscale: DownscaleFactor,
2844    ) -> Result<DecodeOutcome, JpegError> {
2845        if roi == Rect::full(self.info.dimensions) && downscale == DownscaleFactor::Full {
2846            return self.decode_lossless_rgb8_into(out, stride);
2847        }
2848
2849        let (width, height) = self.info.dimensions;
2850        let full_stride = width as usize * 3;
2851        let mut full =
2852            allocate_output_buffer(checked_scratch_len(&[full_stride, height as usize])?);
2853        let mut outcome = self.decode_lossless_rgb8_into(&mut full, full_stride)?;
2854        let output_rect = scaled_rect_covering(roi, downscale)?;
2855        copy_rgb8_scaled_rect(
2856            &full,
2857            (width, height),
2858            output_rect,
2859            downscale.denominator(),
2860            out,
2861            stride,
2862        );
2863        outcome.decoded = roi;
2864        Ok(outcome)
2865    }
2866
2867    fn decode_lossless_ycbcr8_region_scaled_into(
2868        &self,
2869        out: &mut [u8],
2870        stride: usize,
2871        roi: Rect,
2872        downscale: DownscaleFactor,
2873    ) -> Result<DecodeOutcome, JpegError> {
2874        if roi == Rect::full(self.info.dimensions) && downscale == DownscaleFactor::Full {
2875            return self.decode_lossless_ycbcr8_into(out, stride);
2876        }
2877
2878        let (width, height) = self.info.dimensions;
2879        let full_stride = width as usize * 3;
2880        let mut full =
2881            allocate_output_buffer(checked_scratch_len(&[full_stride, height as usize])?);
2882        let mut outcome = self.decode_lossless_ycbcr8_into(&mut full, full_stride)?;
2883        let output_rect = scaled_rect_covering(roi, downscale)?;
2884        copy_rgb8_scaled_rect(
2885            &full,
2886            (width, height),
2887            output_rect,
2888            downscale.denominator(),
2889            out,
2890            stride,
2891        );
2892        outcome.decoded = roi;
2893        Ok(outcome)
2894    }
2895
2896    fn decode_lossless_rgba8_region_into(
2897        &self,
2898        out: &mut [u8],
2899        stride: usize,
2900        roi: Rect,
2901        downscale: DownscaleFactor,
2902        alpha: u8,
2903    ) -> Result<DecodeOutcome, JpegError> {
2904        let output_rect = scaled_rect_covering(roi, downscale)?;
2905        let rgb_stride = output_rect.w as usize * 3;
2906        let mut rgb =
2907            allocate_output_buffer(checked_scratch_len(&[rgb_stride, output_rect.h as usize])?);
2908        let outcome = match self.info.color_space {
2909            ColorSpace::YCbCr => {
2910                self.decode_lossless_ycbcr8_region_scaled_into(&mut rgb, rgb_stride, roi, downscale)
2911            }
2912            _ => self.decode_lossless_rgb8_region_scaled_into(&mut rgb, rgb_stride, roi, downscale),
2913        }?;
2914        copy_rgb8_to_rgba8(
2915            &rgb,
2916            rgb_stride,
2917            output_rect.w,
2918            output_rect.h,
2919            out,
2920            stride,
2921            alpha,
2922        );
2923        Ok(outcome)
2924    }
2925
2926    fn decode_lossless_color_rows<P, S>(
2927        &self,
2928        sink: &mut S,
2929        prev_row: &mut Vec<u8>,
2930        curr_row: &mut Vec<u8>,
2931        conversion_row: Option<&mut [u8]>,
2932        color_space: ColorSpace,
2933        convert_row: impl Fn(&[u8], &mut [u8]),
2934    ) -> Result<DecodeOutcome, JpegError>
2935    where
2936        P: LosslessSample,
2937        S: RowSink<u8, Error = JpegError>,
2938    {
2939        let plan = self
2940            .lossless_plan
2941            .as_ref()
2942            .ok_or(JpegError::NotImplemented {
2943                sof: self.info.sof_kind,
2944            })?;
2945        if plan.bit_depth != P::BIT_DEPTH {
2946            return Err(JpegError::UnsupportedBitDepth {
2947                depth: plan.bit_depth,
2948            });
2949        }
2950        if self.info.color_space != color_space {
2951            return Err(JpegError::NotImplemented {
2952                sof: self.info.sof_kind,
2953            });
2954        }
2955        if self.plan.components.len() != 3 {
2956            return Err(JpegError::UnsupportedComponentCount {
2957                count: self.plan.components.len() as u8,
2958            });
2959        }
2960        if !(1..=7).contains(&plan.predictor) {
2961            return Err(JpegError::UnsupportedPredictor {
2962                predictor: plan.predictor,
2963            });
2964        }
2965
2966        let (width, height) = plan.dimensions;
2967        let width = width as usize;
2968        let row_len = width.saturating_mul(3 * P::BYTES);
2969        prev_row.resize(row_len, 0);
2970        curr_row.resize(row_len, 0);
2971
2972        let scan_bytes = &self.bytes[plan.scan_offset..];
2973        let mut br = BitReader::new(scan_bytes);
2974        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
2975        let total_pixels = plan.dimensions.0.saturating_mul(height);
2976        let mut pixels_since_restart = 0u32;
2977        let mut expected_rst = 0u8;
2978        let mut conversion_row = conversion_row;
2979        for y in 0..height as usize {
2980            for x in 0..width {
2981                let pixel_index = y as u32 * plan.dimensions.0 + x as u32;
2982                if restart > 0 && pixels_since_restart == restart {
2983                    consume_lossless_restart(
2984                        &mut br,
2985                        pixel_index,
2986                        total_pixels,
2987                        &mut expected_rst,
2988                    )?;
2989                    pixels_since_restart = 0;
2990                }
2991                for component in &self.plan.components {
2992                    if component.output_index >= 3 {
2993                        return Err(JpegError::UnsupportedComponentCount {
2994                            count: self.plan.components.len() as u8,
2995                        });
2996                    }
2997                    let predictor = if restart > 0 && pixels_since_restart == 0 {
2998                        P::RESTART_PREDICTOR
2999                    } else {
3000                        lossless_predictor_color_rows::<P>(
3001                            plan.predictor,
3002                            curr_row,
3003                            prev_row,
3004                            x,
3005                            y,
3006                            component.output_index,
3007                        )
3008                    };
3009                    let diff = component.dc_table.decode_fast_dc(&mut br)?;
3010                    let sample = P::from_i32(predictor + diff)?;
3011                    let offset = (x * 3 + component.output_index) * P::BYTES;
3012                    sample.write_le(&mut curr_row[offset..]);
3013                }
3014                pixels_since_restart += 1;
3015            }
3016            let row = if color_space == ColorSpace::YCbCr {
3017                let row = conversion_row
3018                    .as_deref_mut()
3019                    .ok_or(JpegError::OutputBufferTooSmall {
3020                        required: row_len,
3021                        provided: 0,
3022                    })?;
3023                if row.len() < row_len {
3024                    return Err(JpegError::OutputBufferTooSmall {
3025                        required: row_len,
3026                        provided: row.len(),
3027                    });
3028                }
3029                convert_row(&curr_row[..row_len], &mut row[..row_len]);
3030                &row[..row_len]
3031            } else {
3032                &curr_row[..row_len]
3033            };
3034            sink.write_row(y as u32, row)?;
3035            core::mem::swap(prev_row, curr_row);
3036        }
3037
3038        let scan_warnings = finish_scan(&mut br, true)?;
3039        Ok(DecodeOutcome {
3040            decoded: Rect::full(self.info.dimensions),
3041            warnings: merged_warnings(&self.warnings, scan_warnings),
3042        })
3043    }
3044
3045    fn decode_lossless_color8_rows<S>(
3046        &self,
3047        sink: &mut S,
3048        prev_row: &mut Vec<u8>,
3049        curr_row: &mut Vec<u8>,
3050        conversion_row: Option<&mut [u8]>,
3051        color_space: ColorSpace,
3052    ) -> Result<DecodeOutcome, JpegError>
3053    where
3054        S: RowSink<u8, Error = JpegError>,
3055    {
3056        self.decode_lossless_color_rows::<u8, S>(
3057            sink,
3058            prev_row,
3059            curr_row,
3060            conversion_row,
3061            color_space,
3062            copy_ycbcr8_row_to_rgb8,
3063        )
3064    }
3065
3066    fn decode_lossless_gray16_rows<S>(
3067        &self,
3068        sink: &mut S,
3069        prev_row: &mut Vec<u8>,
3070        curr_row: &mut Vec<u8>,
3071    ) -> Result<DecodeOutcome, JpegError>
3072    where
3073        S: RowSink<u8, Error = JpegError>,
3074    {
3075        self.decode_lossless_gray_rows::<u16, S>(sink, prev_row, curr_row, |sink, y, row| {
3076            sink.write_row(y, row)
3077        })
3078    }
3079
3080    fn decode_lossless_color16_rows<S>(
3081        &self,
3082        sink: &mut S,
3083        prev_row: &mut Vec<u8>,
3084        curr_row: &mut Vec<u8>,
3085        conversion_row: Option<&mut [u8]>,
3086        color_space: ColorSpace,
3087    ) -> Result<DecodeOutcome, JpegError>
3088    where
3089        S: RowSink<u8, Error = JpegError>,
3090    {
3091        self.decode_lossless_color_rows::<u16, S>(
3092            sink,
3093            prev_row,
3094            curr_row,
3095            conversion_row,
3096            color_space,
3097            copy_ycbcr16_row_to_rgb16,
3098        )
3099    }
3100
3101    fn decode_lossless_rgb16_into(
3102        &self,
3103        out: &mut [u8],
3104        stride: usize,
3105    ) -> Result<DecodeOutcome, JpegError> {
3106        match lossless_color_sampling(&self.info) {
3107            Some(LosslessColorSampling::S444) => {
3108                self.decode_lossless_color16_components_into(out, stride, ColorSpace::Rgb)
3109            }
3110            Some(LosslessColorSampling::S422) => {
3111                self.decode_lossless_color16_sampled_into(out, stride, ColorSpace::Rgb)
3112            }
3113            Some(LosslessColorSampling::S420) => {
3114                self.decode_lossless_color16_sampled_into(out, stride, ColorSpace::Rgb)
3115            }
3116            None => Err(JpegError::NotImplemented {
3117                sof: self.info.sof_kind,
3118            }),
3119        }
3120    }
3121
3122    fn decode_lossless_color16_components_into(
3123        &self,
3124        out: &mut [u8],
3125        stride: usize,
3126        color_space: ColorSpace,
3127    ) -> Result<DecodeOutcome, JpegError> {
3128        self.decode_lossless_color_components_into::<u16>(out, stride, color_space)
3129    }
3130
3131    fn decode_lossless_color16_sampled_into(
3132        &self,
3133        out: &mut [u8],
3134        stride: usize,
3135        color_space: ColorSpace,
3136    ) -> Result<DecodeOutcome, JpegError> {
3137        self.decode_lossless_color_sampled_into::<u16>(
3138            out,
3139            stride,
3140            color_space,
3141            write_lossless_color16_sampled_output,
3142        )
3143    }
3144
3145    fn decode_lossless_ycbcr16_into(
3146        &self,
3147        out: &mut [u8],
3148        stride: usize,
3149    ) -> Result<DecodeOutcome, JpegError> {
3150        match lossless_color_sampling(&self.info) {
3151            Some(LosslessColorSampling::S444) => {
3152                let outcome =
3153                    self.decode_lossless_color16_components_into(out, stride, ColorSpace::YCbCr)?;
3154                convert_ycbcr16_to_rgb16_in_place(out, stride, self.info.dimensions);
3155                Ok(outcome)
3156            }
3157            Some(LosslessColorSampling::S422) => {
3158                self.decode_lossless_color16_sampled_into(out, stride, ColorSpace::YCbCr)
3159            }
3160            Some(LosslessColorSampling::S420) => {
3161                self.decode_lossless_color16_sampled_into(out, stride, ColorSpace::YCbCr)
3162            }
3163            None => Err(JpegError::NotImplemented {
3164                sof: self.info.sof_kind,
3165            }),
3166        }
3167    }
3168
3169    fn decode_lossless_rgb16_region_scaled_into(
3170        &self,
3171        out: &mut [u8],
3172        stride: usize,
3173        roi: Rect,
3174        downscale: DownscaleFactor,
3175    ) -> Result<DecodeOutcome, JpegError> {
3176        if roi == Rect::full(self.info.dimensions) && downscale == DownscaleFactor::Full {
3177            return self.decode_lossless_rgb16_into(out, stride);
3178        }
3179
3180        let (width, height) = self.info.dimensions;
3181        let full_stride = width as usize * 6;
3182        let mut full =
3183            allocate_output_buffer(checked_scratch_len(&[full_stride, height as usize])?);
3184        let mut outcome = self.decode_lossless_rgb16_into(&mut full, full_stride)?;
3185        let output_rect = scaled_rect_covering(roi, downscale)?;
3186        copy_rgb16_scaled_rect(
3187            &full,
3188            (width, height),
3189            output_rect,
3190            downscale.denominator(),
3191            out,
3192            stride,
3193        );
3194        outcome.decoded = roi;
3195        Ok(outcome)
3196    }
3197
3198    fn decode_lossless_ycbcr16_region_scaled_into(
3199        &self,
3200        out: &mut [u8],
3201        stride: usize,
3202        roi: Rect,
3203        downscale: DownscaleFactor,
3204    ) -> Result<DecodeOutcome, JpegError> {
3205        if roi == Rect::full(self.info.dimensions) && downscale == DownscaleFactor::Full {
3206            return self.decode_lossless_ycbcr16_into(out, stride);
3207        }
3208
3209        let (width, height) = self.info.dimensions;
3210        let full_stride = width as usize * 6;
3211        let mut full =
3212            allocate_output_buffer(checked_scratch_len(&[full_stride, height as usize])?);
3213        let mut outcome = self.decode_lossless_ycbcr16_into(&mut full, full_stride)?;
3214        let output_rect = scaled_rect_covering(roi, downscale)?;
3215        copy_rgb16_scaled_rect(
3216            &full,
3217            (width, height),
3218            output_rect,
3219            downscale.denominator(),
3220            out,
3221            stride,
3222        );
3223        outcome.decoded = roi;
3224        Ok(outcome)
3225    }
3226
3227    fn decode_lossless_rgba16_region_scaled_into(
3228        &self,
3229        out: &mut [u8],
3230        stride: usize,
3231        roi: Rect,
3232        downscale: DownscaleFactor,
3233        alpha: u16,
3234    ) -> Result<DecodeOutcome, JpegError> {
3235        let output_rect = scaled_rect_covering(roi, downscale)?;
3236        let rgb_stride = output_rect.w as usize * 6;
3237        let mut rgb =
3238            allocate_output_buffer(checked_scratch_len(&[rgb_stride, output_rect.h as usize])?);
3239        let outcome = match self.info.color_space {
3240            ColorSpace::YCbCr => self
3241                .decode_lossless_ycbcr16_region_scaled_into(&mut rgb, rgb_stride, roi, downscale),
3242            _ => {
3243                self.decode_lossless_rgb16_region_scaled_into(&mut rgb, rgb_stride, roi, downscale)
3244            }
3245        }?;
3246        copy_rgb16_to_rgba16(
3247            &rgb,
3248            rgb_stride,
3249            output_rect.w,
3250            output_rect.h,
3251            out,
3252            stride,
3253            alpha,
3254        );
3255        Ok(outcome)
3256    }
3257
3258    fn decode_12bit_rgba16_region_scaled_into(
3259        &self,
3260        out: &mut [u8],
3261        stride: usize,
3262        roi: Rect,
3263        downscale: DownscaleFactor,
3264        alpha: u16,
3265    ) -> Result<DecodeOutcome, JpegError> {
3266        let output_rect = scaled_rect_covering(roi, downscale)?;
3267        let rgb_stride = output_rect.w as usize * 6;
3268        let mut rgb =
3269            allocate_output_buffer(checked_scratch_len(&[rgb_stride, output_rect.h as usize])?);
3270        let outcome = if self.info.sof_kind == SofKind::Progressive12 {
3271            self.decode_progressive12_rgb16_region_scaled_into(&mut rgb, rgb_stride, roi, downscale)
3272        } else {
3273            self.decode_extended12_rgb16_region_scaled_into(&mut rgb, rgb_stride, roi, downscale)
3274        }?;
3275        copy_rgb16_to_rgba16(
3276            &rgb,
3277            rgb_stride,
3278            output_rect.w,
3279            output_rect.h,
3280            out,
3281            stride,
3282            alpha,
3283        );
3284        Ok(outcome)
3285    }
3286
3287    fn decode_lossless_gray16_into(
3288        &self,
3289        out: &mut [u8],
3290        stride: usize,
3291    ) -> Result<DecodeOutcome, JpegError> {
3292        let plan = self
3293            .lossless_plan
3294            .as_ref()
3295            .ok_or(JpegError::NotImplemented {
3296                sof: self.info.sof_kind,
3297            })?;
3298        if plan.bit_depth != 16 {
3299            return Err(JpegError::UnsupportedBitDepth {
3300                depth: plan.bit_depth,
3301            });
3302        }
3303        if !(1..=7).contains(&plan.predictor) {
3304            return Err(JpegError::UnsupportedPredictor {
3305                predictor: plan.predictor,
3306            });
3307        }
3308
3309        let (width, height) = plan.dimensions;
3310        let scan_bytes = &self.bytes[plan.scan_offset..];
3311        let mut br = BitReader::new(scan_bytes);
3312        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
3313        let total_samples = width.saturating_mul(height);
3314        let mut samples_since_restart = 0u32;
3315        let mut expected_rst = 0u8;
3316        for y in 0..height as usize {
3317            for x in 0..width as usize {
3318                let sample_index = y as u32 * width + x as u32;
3319                if restart > 0 && samples_since_restart == restart {
3320                    consume_lossless_restart(
3321                        &mut br,
3322                        sample_index,
3323                        total_samples,
3324                        &mut expected_rst,
3325                    )?;
3326                    samples_since_restart = 0;
3327                }
3328                let predictor = if restart > 0 && samples_since_restart == 0 {
3329                    32768
3330                } else {
3331                    lossless_predictor_value_u16(plan.predictor, out, stride, x, y)
3332                };
3333                let diff = plan.dc_table.decode_fast_dc(&mut br)?;
3334                let sample = <u16 as LosslessSample>::from_i32(predictor + diff)?;
3335                let offset = y * stride + x * 2;
3336                sample.write_le(&mut out[offset..offset + 2]);
3337                samples_since_restart += 1;
3338            }
3339        }
3340
3341        let scan_warnings = finish_scan(&mut br, true)?;
3342        Ok(DecodeOutcome {
3343            decoded: Rect::full(self.info.dimensions),
3344            warnings: merged_warnings(&self.warnings, scan_warnings),
3345        })
3346    }
3347
3348    fn decode_lossless_gray16_region_scaled_into(
3349        &self,
3350        out: &mut [u8],
3351        stride: usize,
3352        roi: Rect,
3353        downscale: DownscaleFactor,
3354    ) -> Result<DecodeOutcome, JpegError> {
3355        if roi == Rect::full(self.info.dimensions) && downscale == DownscaleFactor::Full {
3356            return self.decode_lossless_gray16_into(out, stride);
3357        }
3358
3359        let (width, height) = self.info.dimensions;
3360        let full_stride = width as usize * 2;
3361        let mut full =
3362            allocate_output_buffer(checked_scratch_len(&[full_stride, height as usize])?);
3363        let mut outcome = self.decode_lossless_gray16_into(&mut full, full_stride)?;
3364        let output_rect = scaled_rect_covering(roi, downscale)?;
3365        copy_gray16_scaled_rect(
3366            &full,
3367            (width, height),
3368            output_rect,
3369            downscale.denominator(),
3370            out,
3371            stride,
3372        );
3373        outcome.decoded = roi;
3374        Ok(outcome)
3375    }
3376
3377    fn decode_progressive12_gray16_region_scaled_into(
3378        &self,
3379        out: &mut [u8],
3380        stride: usize,
3381        roi: Rect,
3382        downscale: DownscaleFactor,
3383    ) -> Result<DecodeOutcome, JpegError> {
3384        self.decode_progressive12_region_into(out, stride, roi, downscale, Extended12Output::Gray16)
3385    }
3386
3387    fn decode_progressive12_rgb16_region_scaled_into(
3388        &self,
3389        out: &mut [u8],
3390        stride: usize,
3391        roi: Rect,
3392        downscale: DownscaleFactor,
3393    ) -> Result<DecodeOutcome, JpegError> {
3394        self.decode_progressive12_region_into(out, stride, roi, downscale, Extended12Output::Rgb16)
3395    }
3396
3397    fn decode_progressive12_region_into(
3398        &self,
3399        out: &mut [u8],
3400        stride: usize,
3401        roi: Rect,
3402        downscale: DownscaleFactor,
3403        output: Extended12Output,
3404    ) -> Result<DecodeOutcome, JpegError> {
3405        let plan = self
3406            .progressive_plan
3407            .as_ref()
3408            .ok_or(JpegError::NotImplemented {
3409                sof: self.info.sof_kind,
3410            })?;
3411        if self.info.sof_kind != SofKind::Progressive12
3412            || !matches!(
3413                self.info.color_space,
3414                ColorSpace::Grayscale
3415                    | ColorSpace::YCbCr
3416                    | ColorSpace::Rgb
3417                    | ColorSpace::Cmyk
3418                    | ColorSpace::Ycck
3419            )
3420        {
3421            return Err(JpegError::NotImplemented {
3422                sof: self.info.sof_kind,
3423            });
3424        }
3425        if !roi.is_within(self.info.dimensions) {
3426            return Err(JpegError::RectOutOfBounds {
3427                rect: roi,
3428                width: self.info.dimensions.0,
3429                height: self.info.dimensions.1,
3430            });
3431        }
3432        if matches!(output, Extended12Output::Rgb16) {
3433            match self.info.color_space {
3434                ColorSpace::Rgb => {
3435                    let sampling = progressive_color_sampling(plan, self.info.sof_kind)?;
3436                    return match sampling {
3437                        Extended12ColorSampling::S444 => self
3438                            .decode_progressive12_color444_region_into(
3439                                out,
3440                                stride,
3441                                roi,
3442                                downscale,
3443                                Extended12RgbProjection::Identity,
3444                            ),
3445                        Extended12ColorSampling::S422 | Extended12ColorSampling::S420 => self
3446                            .decode_progressive12_color_subsampled_region_into(
3447                                out,
3448                                stride,
3449                                roi,
3450                                downscale,
3451                                sampling,
3452                                Extended12RgbProjection::Identity,
3453                            ),
3454                    };
3455                }
3456                ColorSpace::YCbCr => {
3457                    let sampling = progressive_color_sampling(plan, self.info.sof_kind)?;
3458                    return match sampling {
3459                        Extended12ColorSampling::S444 => self
3460                            .decode_progressive12_color444_region_into(
3461                                out,
3462                                stride,
3463                                roi,
3464                                downscale,
3465                                Extended12RgbProjection::YCbCr,
3466                            ),
3467                        Extended12ColorSampling::S422 | Extended12ColorSampling::S420 => self
3468                            .decode_progressive12_color_subsampled_region_into(
3469                                out,
3470                                stride,
3471                                roi,
3472                                downscale,
3473                                sampling,
3474                                Extended12RgbProjection::YCbCr,
3475                            ),
3476                    };
3477                }
3478                ColorSpace::Cmyk | ColorSpace::Ycck => {
3479                    let sampling = progressive_four_component_sampling(plan, self.info.sof_kind)?;
3480                    return self.decode_progressive12_four_component_region_into(
3481                        out, stride, roi, downscale, sampling,
3482                    );
3483                }
3484                ColorSpace::Grayscale => {}
3485            }
3486        }
3487        if self.info.color_space != ColorSpace::Grayscale || plan.components.len() != 1 {
3488            return Err(JpegError::NotImplemented {
3489                sof: self.info.sof_kind,
3490            });
3491        }
3492
3493        let output_rect = scaled_rect_covering(roi, downscale)?;
3494        let dct_blocks = decode_progressive_dct_blocks(plan, self.bytes)?;
3495        let component = &plan.components[0];
3496        let component_coeffs = &dct_blocks.quantized[0];
3497        let (width, height) = self.info.dimensions;
3498        let mut dequant = [0i16; 64];
3499        let mut pixels = [0u16; 64];
3500        let write_region = Extended12WriteRegion {
3501            output_rect,
3502            dimensions: (width, height),
3503            downscale,
3504            output,
3505        };
3506
3507        for block_y in 0..component.block_rows as usize {
3508            for block_x in 0..component.block_cols as usize {
3509                let block_index = block_y * component.block_cols as usize + block_x;
3510                dequantize_progressive12_block(
3511                    &component_coeffs[block_index],
3512                    &component.quant,
3513                    &mut dequant,
3514                );
3515                if dequant[1..].iter().all(|&coeff| coeff == 0) {
3516                    pixels.fill(crate::idct::idct_islow_12bit_dc_only_sample(dequant[0]));
3517                } else {
3518                    crate::idct::idct_islow_12bit(&dequant, &mut pixels);
3519                }
3520                write_extended12_block_region(
3521                    out,
3522                    stride,
3523                    write_region,
3524                    ((block_x as u32) * 8, (block_y as u32) * 8),
3525                    &pixels,
3526                );
3527            }
3528        }
3529
3530        Ok(DecodeOutcome {
3531            decoded: roi,
3532            warnings: self.warnings.to_vec(),
3533        })
3534    }
3535
3536    fn decode_progressive12_color444_region_into(
3537        &self,
3538        out: &mut [u8],
3539        stride: usize,
3540        roi: Rect,
3541        downscale: DownscaleFactor,
3542        projection: Extended12RgbProjection,
3543    ) -> Result<DecodeOutcome, JpegError> {
3544        let plan = self
3545            .progressive_plan
3546            .as_ref()
3547            .ok_or(JpegError::NotImplemented {
3548                sof: self.info.sof_kind,
3549            })?;
3550        if plan.components.len() != 3
3551            || plan.sampling.max_h != 1
3552            || plan.sampling.max_v != 1
3553            || plan
3554                .components
3555                .iter()
3556                .any(|component| component.h != 1 || component.v != 1 || component.output_index > 2)
3557        {
3558            return Err(JpegError::NotImplemented {
3559                sof: self.info.sof_kind,
3560            });
3561        }
3562
3563        let output_rect = scaled_rect_covering(roi, downscale)?;
3564        let dct_blocks = decode_progressive_dct_blocks(plan, self.bytes)?;
3565        let (width, height) = self.info.dimensions;
3566        let component_indices = progressive_color_component_indices(plan)?;
3567        let block_cols = plan.components[component_indices[0]].block_cols as usize;
3568        let block_rows = plan.components[component_indices[0]].block_rows as usize;
3569        let mut dequant = [[0i16; 64]; 3];
3570        let mut pixels = [[0u16; 64]; 3];
3571        let write_region = Extended12WriteRegion {
3572            output_rect,
3573            dimensions: (width, height),
3574            downscale,
3575            output: Extended12Output::Rgb16,
3576        };
3577
3578        for block_y in 0..block_rows {
3579            for block_x in 0..block_cols {
3580                for output_index in 0..3 {
3581                    let component_index = component_indices[output_index];
3582                    let component = &plan.components[component_index];
3583                    let component_coeffs = &dct_blocks.quantized[component_index];
3584                    let block_index = block_y * component.block_cols as usize + block_x;
3585                    dequantize_progressive12_block(
3586                        &component_coeffs[block_index],
3587                        &component.quant,
3588                        &mut dequant[output_index],
3589                    );
3590                    if dequant[output_index][1..].iter().all(|&coeff| coeff == 0) {
3591                        pixels[output_index].fill(crate::idct::idct_islow_12bit_dc_only_sample(
3592                            dequant[output_index][0],
3593                        ));
3594                    } else {
3595                        crate::idct::idct_islow_12bit(
3596                            &dequant[output_index],
3597                            &mut pixels[output_index],
3598                        );
3599                    }
3600                }
3601                write_extended12_rgb_block_region(
3602                    out,
3603                    stride,
3604                    write_region,
3605                    projection,
3606                    ((block_x as u32) * 8, (block_y as u32) * 8),
3607                    &pixels,
3608                );
3609            }
3610        }
3611
3612        Ok(DecodeOutcome {
3613            decoded: roi,
3614            warnings: self.warnings.to_vec(),
3615        })
3616    }
3617
3618    fn decode_progressive12_color_subsampled_region_into(
3619        &self,
3620        out: &mut [u8],
3621        stride: usize,
3622        roi: Rect,
3623        downscale: DownscaleFactor,
3624        sampling: Extended12ColorSampling,
3625        projection: Extended12RgbProjection,
3626    ) -> Result<DecodeOutcome, JpegError> {
3627        let plan = self
3628            .progressive_plan
3629            .as_ref()
3630            .ok_or(JpegError::NotImplemented {
3631                sof: self.info.sof_kind,
3632            })?;
3633        debug_assert!(matches!(
3634            sampling,
3635            Extended12ColorSampling::S422 | Extended12ColorSampling::S420
3636        ));
3637        if progressive_color_sampling(plan, self.info.sof_kind)? != sampling {
3638            return Err(JpegError::NotImplemented {
3639                sof: self.info.sof_kind,
3640            });
3641        }
3642
3643        let output_rect = scaled_rect_covering(roi, downscale)?;
3644        let dct_blocks = decode_progressive_dct_blocks(plan, self.bytes)?;
3645        let planes = render_progressive12_color_planes(plan, &dct_blocks.quantized)?;
3646        let write_region = Extended12WriteRegion {
3647            output_rect,
3648            dimensions: self.info.dimensions,
3649            downscale,
3650            output: Extended12Output::Rgb16,
3651        };
3652        match sampling {
3653            Extended12ColorSampling::S444 => unreachable!("4:4:4 path is handled directly"),
3654            Extended12ColorSampling::S422 => write_extended12_color422_planes_region(
3655                out,
3656                stride,
3657                write_region,
3658                projection,
3659                &planes,
3660            ),
3661            Extended12ColorSampling::S420 => write_extended12_color420_planes_region(
3662                out,
3663                stride,
3664                write_region,
3665                projection,
3666                &planes,
3667            ),
3668        }
3669
3670        Ok(DecodeOutcome {
3671            decoded: roi,
3672            warnings: self.warnings.to_vec(),
3673        })
3674    }
3675
3676    fn decode_progressive12_four_component_region_into(
3677        &self,
3678        out: &mut [u8],
3679        stride: usize,
3680        roi: Rect,
3681        downscale: DownscaleFactor,
3682        sampling: Extended12ColorSampling,
3683    ) -> Result<DecodeOutcome, JpegError> {
3684        let plan = self
3685            .progressive_plan
3686            .as_ref()
3687            .ok_or(JpegError::NotImplemented {
3688                sof: self.info.sof_kind,
3689            })?;
3690        if progressive_four_component_sampling(plan, self.info.sof_kind)? != sampling {
3691            return Err(JpegError::NotImplemented {
3692                sof: self.info.sof_kind,
3693            });
3694        }
3695
3696        let output_rect = scaled_rect_covering(roi, downscale)?;
3697        let dct_blocks = decode_progressive_dct_blocks(plan, self.bytes)?;
3698        let planes = render_progressive12_four_component_planes(plan, &dct_blocks.quantized)?;
3699        write_extended12_four_component_planes_region(
3700            out,
3701            stride,
3702            Extended12WriteRegion {
3703                output_rect,
3704                dimensions: self.info.dimensions,
3705                downscale,
3706                output: Extended12Output::Rgb16,
3707            },
3708            self.info.color_space,
3709            sampling,
3710            &planes,
3711        );
3712
3713        Ok(DecodeOutcome {
3714            decoded: roi,
3715            warnings: self.warnings.to_vec(),
3716        })
3717    }
3718
3719    fn decode_extended12_gray16_into(
3720        &self,
3721        out: &mut [u8],
3722        stride: usize,
3723    ) -> Result<DecodeOutcome, JpegError> {
3724        self.decode_extended12_region_into(
3725            out,
3726            stride,
3727            Rect::full(self.info.dimensions),
3728            DownscaleFactor::Full,
3729            Extended12Output::Gray16,
3730        )
3731    }
3732
3733    fn decode_extended12_gray16_region_into(
3734        &self,
3735        out: &mut [u8],
3736        stride: usize,
3737        roi: Rect,
3738    ) -> Result<DecodeOutcome, JpegError> {
3739        self.decode_extended12_region_into(
3740            out,
3741            stride,
3742            roi,
3743            DownscaleFactor::Full,
3744            Extended12Output::Gray16,
3745        )
3746    }
3747
3748    fn decode_extended12_gray16_region_scaled_into(
3749        &self,
3750        out: &mut [u8],
3751        stride: usize,
3752        roi: Rect,
3753        downscale: DownscaleFactor,
3754    ) -> Result<DecodeOutcome, JpegError> {
3755        self.decode_extended12_region_into(out, stride, roi, downscale, Extended12Output::Gray16)
3756    }
3757
3758    fn decode_extended12_rgb16_into(
3759        &self,
3760        out: &mut [u8],
3761        stride: usize,
3762    ) -> Result<DecodeOutcome, JpegError> {
3763        self.decode_extended12_region_into(
3764            out,
3765            stride,
3766            Rect::full(self.info.dimensions),
3767            DownscaleFactor::Full,
3768            Extended12Output::Rgb16,
3769        )
3770    }
3771
3772    fn decode_extended12_rgb16_region_into(
3773        &self,
3774        out: &mut [u8],
3775        stride: usize,
3776        roi: Rect,
3777    ) -> Result<DecodeOutcome, JpegError> {
3778        self.decode_extended12_region_into(
3779            out,
3780            stride,
3781            roi,
3782            DownscaleFactor::Full,
3783            Extended12Output::Rgb16,
3784        )
3785    }
3786
3787    fn decode_extended12_rgb16_region_scaled_into(
3788        &self,
3789        out: &mut [u8],
3790        stride: usize,
3791        roi: Rect,
3792        downscale: DownscaleFactor,
3793    ) -> Result<DecodeOutcome, JpegError> {
3794        self.decode_extended12_region_into(out, stride, roi, downscale, Extended12Output::Rgb16)
3795    }
3796
3797    fn decode_extended12_region_into(
3798        &self,
3799        out: &mut [u8],
3800        stride: usize,
3801        roi: Rect,
3802        downscale: DownscaleFactor,
3803        output: Extended12Output,
3804    ) -> Result<DecodeOutcome, JpegError> {
3805        if self.info.sof_kind != SofKind::Extended12 {
3806            return Err(JpegError::NotImplemented {
3807                sof: self.info.sof_kind,
3808            });
3809        }
3810        if !roi.is_within(self.info.dimensions) {
3811            return Err(JpegError::RectOutOfBounds {
3812                rect: roi,
3813                width: self.info.dimensions.0,
3814                height: self.info.dimensions.1,
3815            });
3816        }
3817        if matches!(output, Extended12Output::Rgb16) {
3818            match self.info.color_space {
3819                ColorSpace::Rgb => {
3820                    let sampling = extended12_color_sampling(&self.plan, self.info.sof_kind)?;
3821                    return match sampling {
3822                        Extended12ColorSampling::S444 => self
3823                            .decode_extended12_color444_region_into(
3824                                out,
3825                                stride,
3826                                roi,
3827                                downscale,
3828                                Extended12RgbProjection::Identity,
3829                            ),
3830                        Extended12ColorSampling::S422 | Extended12ColorSampling::S420 => self
3831                            .decode_extended12_color_subsampled_region_into(
3832                                out,
3833                                stride,
3834                                roi,
3835                                downscale,
3836                                sampling,
3837                                Extended12RgbProjection::Identity,
3838                            ),
3839                    };
3840                }
3841                ColorSpace::YCbCr => {
3842                    let sampling = extended12_color_sampling(&self.plan, self.info.sof_kind)?;
3843                    return match sampling {
3844                        Extended12ColorSampling::S444 => self
3845                            .decode_extended12_color444_region_into(
3846                                out,
3847                                stride,
3848                                roi,
3849                                downscale,
3850                                Extended12RgbProjection::YCbCr,
3851                            ),
3852                        Extended12ColorSampling::S422 | Extended12ColorSampling::S420 => self
3853                            .decode_extended12_color_subsampled_region_into(
3854                                out,
3855                                stride,
3856                                roi,
3857                                downscale,
3858                                sampling,
3859                                Extended12RgbProjection::YCbCr,
3860                            ),
3861                    };
3862                }
3863                ColorSpace::Cmyk | ColorSpace::Ycck => {
3864                    let sampling =
3865                        extended12_four_component_sampling(&self.plan, self.info.sof_kind)?;
3866                    return match sampling {
3867                        Extended12ColorSampling::S444 => self
3868                            .decode_extended12_four_component444_region_into(
3869                                out, stride, roi, downscale,
3870                            ),
3871                        Extended12ColorSampling::S422 | Extended12ColorSampling::S420 => self
3872                            .decode_extended12_four_component_subsampled_region_into(
3873                                out, stride, roi, downscale, sampling,
3874                            ),
3875                    };
3876                }
3877                ColorSpace::Grayscale => {}
3878            }
3879        }
3880        if self.info.color_space != ColorSpace::Grayscale || self.plan.components.len() != 1 {
3881            return Err(JpegError::NotImplemented {
3882                sof: self.info.sof_kind,
3883            });
3884        }
3885
3886        let output_rect = scaled_rect_covering(roi, downscale)?;
3887        let scan_bytes = &self.bytes[self.plan.scan_offset..];
3888        let component = &self.plan.components[0];
3889        let (width, height) = self.info.dimensions;
3890        let mcu_cols = width.div_ceil(8);
3891        let mcu_rows = height.div_ceil(8);
3892        let mut br = BitReader::new(scan_bytes);
3893        let mut prev_dc = 0i32;
3894        let mut coeff = CoefficientBlock::default();
3895        let mut pixels = [0u16; 64];
3896        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
3897        let mut mcus_since_restart = 0u32;
3898        let mut expected_rst = 0u8;
3899        let total_mcus = mcu_cols * mcu_rows;
3900        let write_region = Extended12WriteRegion {
3901            output_rect,
3902            dimensions: (width, height),
3903            downscale,
3904            output,
3905        };
3906
3907        for mcu_y in 0..mcu_rows {
3908            for mcu_x in 0..mcu_cols {
3909                let current_mcu = mcu_y * mcu_cols + mcu_x;
3910                if restart > 0 && mcus_since_restart == restart {
3911                    consume_extended12_restart(
3912                        &mut br,
3913                        current_mcu,
3914                        total_mcus,
3915                        &mut expected_rst,
3916                    )?;
3917                    prev_dc = 0;
3918                    mcus_since_restart = 0;
3919                }
3920                decode_extended12_block_pixels(
3921                    &mut br,
3922                    component,
3923                    &mut prev_dc,
3924                    &mut coeff,
3925                    &mut pixels,
3926                )?;
3927                write_extended12_block_region(
3928                    out,
3929                    stride,
3930                    write_region,
3931                    (mcu_x * 8, mcu_y * 8),
3932                    &pixels,
3933                );
3934                mcus_since_restart += 1;
3935            }
3936        }
3937
3938        let scan_warnings = finish_scan(&mut br, true)?;
3939        Ok(DecodeOutcome {
3940            decoded: roi,
3941            warnings: merged_warnings(&self.warnings, scan_warnings),
3942        })
3943    }
3944
3945    fn decode_extended12_color444_region_into(
3946        &self,
3947        out: &mut [u8],
3948        stride: usize,
3949        roi: Rect,
3950        downscale: DownscaleFactor,
3951        projection: Extended12RgbProjection,
3952    ) -> Result<DecodeOutcome, JpegError> {
3953        validate_extended12_color444_plan(&self.plan, self.info.sof_kind)?;
3954
3955        let output_rect = scaled_rect_covering(roi, downscale)?;
3956        let scan_bytes = &self.bytes[self.plan.scan_offset..];
3957        let (width, height) = self.info.dimensions;
3958        let mcu_cols = width.div_ceil(8);
3959        let mcu_rows = height.div_ceil(8);
3960        let mut br = BitReader::new(scan_bytes);
3961        let mut prev_dc = [0i32; 3];
3962        let mut coeffs: [CoefficientBlock; 3] =
3963            core::array::from_fn(|_| CoefficientBlock::default());
3964        let mut pixels = [[0u16; 64]; 3];
3965        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
3966        let mut mcus_since_restart = 0u32;
3967        let mut expected_rst = 0u8;
3968        let total_mcus = mcu_cols * mcu_rows;
3969        let write_region = Extended12WriteRegion {
3970            output_rect,
3971            dimensions: (width, height),
3972            downscale,
3973            output: Extended12Output::Rgb16,
3974        };
3975
3976        for mcu_y in 0..mcu_rows {
3977            for mcu_x in 0..mcu_cols {
3978                let current_mcu = mcu_y * mcu_cols + mcu_x;
3979                if restart > 0 && mcus_since_restart == restart {
3980                    consume_extended12_restart(
3981                        &mut br,
3982                        current_mcu,
3983                        total_mcus,
3984                        &mut expected_rst,
3985                    )?;
3986                    prev_dc.fill(0);
3987                    mcus_since_restart = 0;
3988                }
3989                for component in &self.plan.components {
3990                    let output_index = component.output_index;
3991                    decode_extended12_block_pixels(
3992                        &mut br,
3993                        component,
3994                        &mut prev_dc[output_index],
3995                        &mut coeffs[output_index],
3996                        &mut pixels[output_index],
3997                    )?;
3998                }
3999                write_extended12_rgb_block_region(
4000                    out,
4001                    stride,
4002                    write_region,
4003                    projection,
4004                    (mcu_x * 8, mcu_y * 8),
4005                    &pixels,
4006                );
4007                mcus_since_restart += 1;
4008            }
4009        }
4010
4011        let scan_warnings = finish_scan(&mut br, true)?;
4012        Ok(DecodeOutcome {
4013            decoded: roi,
4014            warnings: merged_warnings(&self.warnings, scan_warnings),
4015        })
4016    }
4017
4018    fn decode_extended12_color_subsampled_region_into(
4019        &self,
4020        out: &mut [u8],
4021        stride: usize,
4022        roi: Rect,
4023        downscale: DownscaleFactor,
4024        sampling: Extended12ColorSampling,
4025        projection: Extended12RgbProjection,
4026    ) -> Result<DecodeOutcome, JpegError> {
4027        debug_assert!(matches!(
4028            sampling,
4029            Extended12ColorSampling::S422 | Extended12ColorSampling::S420
4030        ));
4031        if extended12_color_sampling(&self.plan, self.info.sof_kind)? != sampling {
4032            return Err(JpegError::NotImplemented {
4033                sof: self.info.sof_kind,
4034            });
4035        }
4036
4037        let output_rect = scaled_rect_covering(roi, downscale)?;
4038        let scan_bytes = &self.bytes[self.plan.scan_offset..];
4039        let (planes, scan_warnings) =
4040            decode_extended12_color_planes(&self.plan, scan_bytes, self.info.sof_kind)?;
4041        let write_region = Extended12WriteRegion {
4042            output_rect,
4043            dimensions: self.info.dimensions,
4044            downscale,
4045            output: Extended12Output::Rgb16,
4046        };
4047        match sampling {
4048            Extended12ColorSampling::S444 => unreachable!("4:4:4 path is handled directly"),
4049            Extended12ColorSampling::S422 => write_extended12_color422_planes_region(
4050                out,
4051                stride,
4052                write_region,
4053                projection,
4054                &planes,
4055            ),
4056            Extended12ColorSampling::S420 => write_extended12_color420_planes_region(
4057                out,
4058                stride,
4059                write_region,
4060                projection,
4061                &planes,
4062            ),
4063        }
4064
4065        Ok(DecodeOutcome {
4066            decoded: roi,
4067            warnings: merged_warnings(&self.warnings, scan_warnings),
4068        })
4069    }
4070
4071    fn decode_extended12_four_component444_region_into(
4072        &self,
4073        out: &mut [u8],
4074        stride: usize,
4075        roi: Rect,
4076        downscale: DownscaleFactor,
4077    ) -> Result<DecodeOutcome, JpegError> {
4078        validate_extended12_four_component444_plan(&self.plan, self.info.sof_kind)?;
4079
4080        let output_rect = scaled_rect_covering(roi, downscale)?;
4081        let scan_bytes = &self.bytes[self.plan.scan_offset..];
4082        let (width, height) = self.info.dimensions;
4083        let mcu_cols = width.div_ceil(8);
4084        let mcu_rows = height.div_ceil(8);
4085        let mut br = BitReader::new(scan_bytes);
4086        let mut prev_dc = [0i32; 4];
4087        let mut coeffs: [CoefficientBlock; 4] =
4088            core::array::from_fn(|_| CoefficientBlock::default());
4089        let mut pixels = [[0u16; 64]; 4];
4090        let restart = u32::from(self.plan.restart_interval.unwrap_or(0));
4091        let mut mcus_since_restart = 0u32;
4092        let mut expected_rst = 0u8;
4093        let total_mcus = mcu_cols * mcu_rows;
4094        let write_region = Extended12WriteRegion {
4095            output_rect,
4096            dimensions: (width, height),
4097            downscale,
4098            output: Extended12Output::Rgb16,
4099        };
4100
4101        for mcu_y in 0..mcu_rows {
4102            for mcu_x in 0..mcu_cols {
4103                let current_mcu = mcu_y * mcu_cols + mcu_x;
4104                if restart > 0 && mcus_since_restart == restart {
4105                    consume_extended12_restart(
4106                        &mut br,
4107                        current_mcu,
4108                        total_mcus,
4109                        &mut expected_rst,
4110                    )?;
4111                    prev_dc.fill(0);
4112                    mcus_since_restart = 0;
4113                }
4114                for component in &self.plan.components {
4115                    let output_index = component.output_index;
4116                    decode_extended12_block_pixels(
4117                        &mut br,
4118                        component,
4119                        &mut prev_dc[output_index],
4120                        &mut coeffs[output_index],
4121                        &mut pixels[output_index],
4122                    )?;
4123                }
4124                write_extended12_four_component_block_region(
4125                    out,
4126                    stride,
4127                    write_region,
4128                    self.info.color_space,
4129                    (mcu_x * 8, mcu_y * 8),
4130                    &pixels,
4131                );
4132                mcus_since_restart += 1;
4133            }
4134        }
4135
4136        let scan_warnings = finish_scan(&mut br, true)?;
4137        Ok(DecodeOutcome {
4138            decoded: roi,
4139            warnings: merged_warnings(&self.warnings, scan_warnings),
4140        })
4141    }
4142
4143    fn decode_extended12_four_component_subsampled_region_into(
4144        &self,
4145        out: &mut [u8],
4146        stride: usize,
4147        roi: Rect,
4148        downscale: DownscaleFactor,
4149        sampling: Extended12ColorSampling,
4150    ) -> Result<DecodeOutcome, JpegError> {
4151        debug_assert!(matches!(
4152            sampling,
4153            Extended12ColorSampling::S422 | Extended12ColorSampling::S420
4154        ));
4155        if extended12_four_component_sampling(&self.plan, self.info.sof_kind)? != sampling {
4156            return Err(JpegError::NotImplemented {
4157                sof: self.info.sof_kind,
4158            });
4159        }
4160
4161        let output_rect = scaled_rect_covering(roi, downscale)?;
4162        let scan_bytes = &self.bytes[self.plan.scan_offset..];
4163        let (planes, scan_warnings) =
4164            decode_extended12_four_component_planes(&self.plan, scan_bytes, self.info.sof_kind)?;
4165        write_extended12_four_component_planes_region(
4166            out,
4167            stride,
4168            Extended12WriteRegion {
4169                output_rect,
4170                dimensions: self.info.dimensions,
4171                downscale,
4172                output: Extended12Output::Rgb16,
4173            },
4174            self.info.color_space,
4175            sampling,
4176            &planes,
4177        );
4178
4179        Ok(DecodeOutcome {
4180            decoded: roi,
4181            warnings: merged_warnings(&self.warnings, scan_warnings),
4182        })
4183    }
4184}
4185
4186#[derive(Debug, Clone, Copy)]
4187enum Extended12Output {
4188    Gray16,
4189    Rgb16,
4190}
4191
4192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4193enum Extended12ColorSampling {
4194    S444,
4195    S422,
4196    S420,
4197}
4198
4199fn lossless_color_sampling(info: &Info) -> Option<LosslessColorSampling> {
4200    if info.sampling.len() != 3 {
4201        return None;
4202    }
4203    match (
4204        info.sampling.max_h,
4205        info.sampling.max_v,
4206        info.sampling.components(),
4207    ) {
4208        (1, 1, &[(1, 1), (1, 1), (1, 1)]) => Some(LosslessColorSampling::S444),
4209        (2, 1, &[(2, 1), (1, 1), (1, 1)])
4210            if matches!(info.bit_depth, 8 | 16) && info.dimensions.0.is_multiple_of(2) =>
4211        {
4212            Some(LosslessColorSampling::S422)
4213        }
4214        (2, 2, &[(2, 2), (1, 1), (1, 1)])
4215            if matches!(info.bit_depth, 8 | 16)
4216                && info.dimensions.0.is_multiple_of(2)
4217                && info.dimensions.1.is_multiple_of(2) =>
4218        {
4219            Some(LosslessColorSampling::S420)
4220        }
4221        _ => None,
4222    }
4223}
4224
4225#[derive(Debug, Clone, Copy)]
4226enum Extended12RgbProjection {
4227    Identity,
4228    YCbCr,
4229}
4230
4231#[derive(Debug, Clone, Copy)]
4232struct Extended12WriteRegion {
4233    output_rect: Rect,
4234    dimensions: (u32, u32),
4235    downscale: DownscaleFactor,
4236    output: Extended12Output,
4237}
4238
4239struct Extended12Plane {
4240    pixels: Vec<u16>,
4241    stride: usize,
4242    width: usize,
4243}
4244
4245fn decode_extended12_block_pixels(
4246    br: &mut BitReader<'_>,
4247    component: &PreparedComponentPlan,
4248    prev_dc: &mut i32,
4249    coeff: &mut CoefficientBlock,
4250    pixels: &mut [u16; 64],
4251) -> Result<(), JpegError> {
4252    let activity = decode_block_with_activity(
4253        br,
4254        &component.dc_table,
4255        &component.ac_table,
4256        prev_dc,
4257        component.quant.as_ref(),
4258        coeff,
4259    )?;
4260    match activity {
4261        BlockActivity::DcOnly => {
4262            pixels.fill(crate::idct::idct_islow_12bit_dc_only_sample(
4263                coeff.dc_coeff(),
4264            ));
4265        }
4266        BlockActivity::BottomHalfZero | BlockActivity::General => {
4267            crate::idct::idct_islow_12bit(coeff.coefficients(), pixels);
4268        }
4269    }
4270    Ok(())
4271}
4272
4273fn decode_extended12_color_planes(
4274    plan: &PreparedDecodePlan,
4275    scan_bytes: &[u8],
4276    sof: SofKind,
4277) -> Result<([Extended12Plane; 3], Vec<Warning>), JpegError> {
4278    if plan.components.len() != 3 {
4279        return Err(JpegError::NotImplemented { sof });
4280    }
4281    let mut planes = extended12_planes_for_sequential_plan(plan, sof)?;
4282    let mut br = BitReader::new(scan_bytes);
4283    let mut prev_dc = [0i32; 3];
4284    let mut coeffs: [CoefficientBlock; 3] = core::array::from_fn(|_| CoefficientBlock::default());
4285    let mut pixels = [[0u16; 64]; 3];
4286    let mcu_cols = plan
4287        .dimensions
4288        .0
4289        .div_ceil(u32::from(plan.sampling.max_h) * 8);
4290    let mcu_rows = plan
4291        .dimensions
4292        .1
4293        .div_ceil(u32::from(plan.sampling.max_v) * 8);
4294    let restart = u32::from(plan.restart_interval.unwrap_or(0));
4295    let mut mcus_since_restart = 0u32;
4296    let mut expected_rst = 0u8;
4297    let total_mcus = mcu_cols * mcu_rows;
4298
4299    for mcu_y in 0..mcu_rows {
4300        for mcu_x in 0..mcu_cols {
4301            let current_mcu = mcu_y * mcu_cols + mcu_x;
4302            if restart > 0 && mcus_since_restart == restart {
4303                consume_extended12_restart(&mut br, current_mcu, total_mcus, &mut expected_rst)?;
4304                prev_dc.fill(0);
4305                mcus_since_restart = 0;
4306            }
4307            for component in &plan.components {
4308                let output_index = component.output_index;
4309                if output_index > 2 {
4310                    return Err(JpegError::NotImplemented { sof });
4311                }
4312                for by in 0..u32::from(component.v) {
4313                    for bx in 0..u32::from(component.h) {
4314                        decode_extended12_block_pixels(
4315                            &mut br,
4316                            component,
4317                            &mut prev_dc[output_index],
4318                            &mut coeffs[output_index],
4319                            &mut pixels[output_index],
4320                        )?;
4321                        deposit_extended12_block(
4322                            &mut planes[output_index],
4323                            (mcu_x * u32::from(component.h) + bx) as usize * 8,
4324                            (mcu_y * u32::from(component.v) + by) as usize * 8,
4325                            &pixels[output_index],
4326                        );
4327                    }
4328                }
4329            }
4330            mcus_since_restart += 1;
4331        }
4332    }
4333
4334    let scan_warnings = finish_scan(&mut br, true)?;
4335    Ok((planes, scan_warnings))
4336}
4337
4338fn decode_extended12_four_component_planes(
4339    plan: &PreparedDecodePlan,
4340    scan_bytes: &[u8],
4341    sof: SofKind,
4342) -> Result<([Extended12Plane; 4], Vec<Warning>), JpegError> {
4343    if plan.components.len() != 4 {
4344        return Err(JpegError::NotImplemented { sof });
4345    }
4346    let mut planes = extended12_four_component_planes_for_sequential_plan(plan, sof)?;
4347    let mut br = BitReader::new(scan_bytes);
4348    let mut prev_dc = [0i32; 4];
4349    let mut coeffs: [CoefficientBlock; 4] = core::array::from_fn(|_| CoefficientBlock::default());
4350    let mut pixels = [[0u16; 64]; 4];
4351    let mcu_cols = plan
4352        .dimensions
4353        .0
4354        .div_ceil(u32::from(plan.sampling.max_h) * 8);
4355    let mcu_rows = plan
4356        .dimensions
4357        .1
4358        .div_ceil(u32::from(plan.sampling.max_v) * 8);
4359    let restart = u32::from(plan.restart_interval.unwrap_or(0));
4360    let mut mcus_since_restart = 0u32;
4361    let mut expected_rst = 0u8;
4362    let total_mcus = mcu_cols * mcu_rows;
4363
4364    for mcu_y in 0..mcu_rows {
4365        for mcu_x in 0..mcu_cols {
4366            let current_mcu = mcu_y * mcu_cols + mcu_x;
4367            if restart > 0 && mcus_since_restart == restart {
4368                consume_extended12_restart(&mut br, current_mcu, total_mcus, &mut expected_rst)?;
4369                prev_dc.fill(0);
4370                mcus_since_restart = 0;
4371            }
4372            for component in &plan.components {
4373                let output_index = component.output_index;
4374                if output_index > 3 {
4375                    return Err(JpegError::NotImplemented { sof });
4376                }
4377                for by in 0..u32::from(component.v) {
4378                    for bx in 0..u32::from(component.h) {
4379                        decode_extended12_block_pixels(
4380                            &mut br,
4381                            component,
4382                            &mut prev_dc[output_index],
4383                            &mut coeffs[output_index],
4384                            &mut pixels[output_index],
4385                        )?;
4386                        deposit_extended12_block(
4387                            &mut planes[output_index],
4388                            (mcu_x * u32::from(component.h) + bx) as usize * 8,
4389                            (mcu_y * u32::from(component.v) + by) as usize * 8,
4390                            &pixels[output_index],
4391                        );
4392                    }
4393                }
4394            }
4395            mcus_since_restart += 1;
4396        }
4397    }
4398
4399    let scan_warnings = finish_scan(&mut br, true)?;
4400    Ok((planes, scan_warnings))
4401}
4402
4403fn extended12_planes_for_sequential_plan(
4404    plan: &PreparedDecodePlan,
4405    sof: SofKind,
4406) -> Result<[Extended12Plane; 3], JpegError> {
4407    let mcu_cols = plan
4408        .dimensions
4409        .0
4410        .div_ceil(u32::from(plan.sampling.max_h) * 8);
4411    let mcu_rows = plan
4412        .dimensions
4413        .1
4414        .div_ceil(u32::from(plan.sampling.max_v) * 8);
4415    let mut widths = [0usize; 3];
4416    let mut strides = [0usize; 3];
4417    let mut heights = [0usize; 3];
4418    let mut lens = [0usize; 3];
4419    for component in &plan.components {
4420        if component.output_index > 2 {
4421            return Err(JpegError::NotImplemented { sof });
4422        }
4423        widths[component.output_index] =
4424            plan.dimensions
4425                .0
4426                .saturating_mul(u32::from(component.h))
4427                .div_ceil(u32::from(plan.sampling.max_h)) as usize;
4428        strides[component.output_index] =
4429            checked_scratch_len(&[mcu_cols as usize, usize::from(component.h), 8])?;
4430        heights[component.output_index] =
4431            checked_scratch_len(&[mcu_rows as usize, usize::from(component.v), 8])?;
4432        lens[component.output_index] = checked_scratch_len(&[
4433            strides[component.output_index],
4434            heights[component.output_index],
4435            core::mem::size_of::<u16>(),
4436        ])? / core::mem::size_of::<u16>();
4437    }
4438    Ok(core::array::from_fn(|index| Extended12Plane {
4439        pixels: vec![0u16; lens[index]],
4440        stride: strides[index],
4441        width: widths[index],
4442    }))
4443}
4444
4445fn extended12_four_component_planes_for_sequential_plan(
4446    plan: &PreparedDecodePlan,
4447    sof: SofKind,
4448) -> Result<[Extended12Plane; 4], JpegError> {
4449    let mcu_cols = plan
4450        .dimensions
4451        .0
4452        .div_ceil(u32::from(plan.sampling.max_h) * 8);
4453    let mcu_rows = plan
4454        .dimensions
4455        .1
4456        .div_ceil(u32::from(plan.sampling.max_v) * 8);
4457    let mut widths = [0usize; 4];
4458    let mut strides = [0usize; 4];
4459    let mut heights = [0usize; 4];
4460    let mut lens = [0usize; 4];
4461    for component in &plan.components {
4462        if component.output_index > 3 {
4463            return Err(JpegError::NotImplemented { sof });
4464        }
4465        widths[component.output_index] =
4466            plan.dimensions
4467                .0
4468                .saturating_mul(u32::from(component.h))
4469                .div_ceil(u32::from(plan.sampling.max_h)) as usize;
4470        strides[component.output_index] =
4471            checked_scratch_len(&[mcu_cols as usize, usize::from(component.h), 8])?;
4472        heights[component.output_index] =
4473            checked_scratch_len(&[mcu_rows as usize, usize::from(component.v), 8])?;
4474        lens[component.output_index] = checked_scratch_len(&[
4475            strides[component.output_index],
4476            heights[component.output_index],
4477            core::mem::size_of::<u16>(),
4478        ])? / core::mem::size_of::<u16>();
4479    }
4480    Ok(core::array::from_fn(|index| Extended12Plane {
4481        pixels: vec![0u16; lens[index]],
4482        stride: strides[index],
4483        width: widths[index],
4484    }))
4485}
4486
4487fn render_progressive12_color_planes(
4488    plan: &PreparedProgressivePlan,
4489    coeffs: &[Vec<[i32; 64]>],
4490) -> Result<[Extended12Plane; 3], JpegError> {
4491    let mut planes = progressive12_color_planes(plan)?;
4492    let mut dequant = [0i16; 64];
4493    let mut pixels = [0u16; 64];
4494    for (component_index, component) in plan.components.iter().enumerate() {
4495        let output_index = component.output_index;
4496        if output_index > 2 {
4497            return Err(JpegError::NotImplemented {
4498                sof: SofKind::Progressive12,
4499            });
4500        }
4501        for by in 0..component.block_rows as usize {
4502            for bx in 0..component.block_cols as usize {
4503                let block_index = by * component.block_cols as usize + bx;
4504                dequantize_progressive12_block(
4505                    &coeffs[component_index][block_index],
4506                    &component.quant,
4507                    &mut dequant,
4508                );
4509                if dequant[1..].iter().all(|&coeff| coeff == 0) {
4510                    pixels.fill(crate::idct::idct_islow_12bit_dc_only_sample(dequant[0]));
4511                } else {
4512                    crate::idct::idct_islow_12bit(&dequant, &mut pixels);
4513                }
4514                deposit_extended12_block(&mut planes[output_index], bx * 8, by * 8, &pixels);
4515            }
4516        }
4517    }
4518    Ok(planes)
4519}
4520
4521fn render_progressive12_four_component_planes(
4522    plan: &PreparedProgressivePlan,
4523    coeffs: &[Vec<[i32; 64]>],
4524) -> Result<[Extended12Plane; 4], JpegError> {
4525    let mut planes = progressive12_four_component_planes(plan)?;
4526    let mut dequant = [0i16; 64];
4527    let mut pixels = [0u16; 64];
4528    for (component_index, component) in plan.components.iter().enumerate() {
4529        let output_index = component.output_index;
4530        if output_index > 3 {
4531            return Err(JpegError::NotImplemented {
4532                sof: SofKind::Progressive12,
4533            });
4534        }
4535        for by in 0..component.block_rows as usize {
4536            for bx in 0..component.block_cols as usize {
4537                let block_index = by * component.block_cols as usize + bx;
4538                dequantize_progressive12_block(
4539                    &coeffs[component_index][block_index],
4540                    &component.quant,
4541                    &mut dequant,
4542                );
4543                if dequant[1..].iter().all(|&coeff| coeff == 0) {
4544                    pixels.fill(crate::idct::idct_islow_12bit_dc_only_sample(dequant[0]));
4545                } else {
4546                    crate::idct::idct_islow_12bit(&dequant, &mut pixels);
4547                }
4548                deposit_extended12_block(&mut planes[output_index], bx * 8, by * 8, &pixels);
4549            }
4550        }
4551    }
4552    Ok(planes)
4553}
4554
4555fn progressive12_color_planes(
4556    plan: &PreparedProgressivePlan,
4557) -> Result<[Extended12Plane; 3], JpegError> {
4558    let mut widths = [0usize; 3];
4559    let mut strides = [0usize; 3];
4560    let mut heights = [0usize; 3];
4561    for component in &plan.components {
4562        if component.output_index > 2 {
4563            return Err(JpegError::NotImplemented {
4564                sof: SofKind::Progressive12,
4565            });
4566        }
4567        widths[component.output_index] = component.sample_width as usize;
4568        strides[component.output_index] = component.block_cols as usize * 8;
4569        heights[component.output_index] = component.block_rows as usize * 8;
4570    }
4571    Ok(core::array::from_fn(|index| Extended12Plane {
4572        pixels: vec![0u16; strides[index] * heights[index]],
4573        stride: strides[index],
4574        width: widths[index],
4575    }))
4576}
4577
4578fn progressive12_four_component_planes(
4579    plan: &PreparedProgressivePlan,
4580) -> Result<[Extended12Plane; 4], JpegError> {
4581    let mut widths = [0usize; 4];
4582    let mut strides = [0usize; 4];
4583    let mut heights = [0usize; 4];
4584    for component in &plan.components {
4585        if component.output_index > 3 {
4586            return Err(JpegError::NotImplemented {
4587                sof: SofKind::Progressive12,
4588            });
4589        }
4590        widths[component.output_index] = component.sample_width as usize;
4591        strides[component.output_index] = component.block_cols as usize * 8;
4592        heights[component.output_index] = component.block_rows as usize * 8;
4593    }
4594    Ok(core::array::from_fn(|index| Extended12Plane {
4595        pixels: vec![0u16; strides[index] * heights[index]],
4596        stride: strides[index],
4597        width: widths[index],
4598    }))
4599}
4600
4601fn deposit_extended12_block(plane: &mut Extended12Plane, x: usize, y: usize, block: &[u16; 64]) {
4602    for row in 0..8 {
4603        let dst_start = (y + row) * plane.stride + x;
4604        let src_start = row * 8;
4605        plane.pixels[dst_start..dst_start + 8].copy_from_slice(&block[src_start..src_start + 8]);
4606    }
4607}
4608
4609fn validate_extended12_color444_plan(
4610    plan: &PreparedDecodePlan,
4611    sof: SofKind,
4612) -> Result<(), JpegError> {
4613    if plan.components.len() != 3 || plan.sampling.max_h != 1 || plan.sampling.max_v != 1 {
4614        return Err(JpegError::NotImplemented { sof });
4615    }
4616    let mut seen = [false; 3];
4617    for component in &plan.components {
4618        if component.h != 1 || component.v != 1 || component.output_index > 2 {
4619            return Err(JpegError::NotImplemented { sof });
4620        }
4621        if seen[component.output_index] {
4622            return Err(JpegError::NotImplemented { sof });
4623        }
4624        seen[component.output_index] = true;
4625    }
4626    if seen.iter().any(|&present| !present) {
4627        return Err(JpegError::NotImplemented { sof });
4628    }
4629    Ok(())
4630}
4631
4632fn validate_extended12_four_component444_plan(
4633    plan: &PreparedDecodePlan,
4634    sof: SofKind,
4635) -> Result<(), JpegError> {
4636    if extended12_four_component_sampling(plan, sof)? != Extended12ColorSampling::S444 {
4637        return Err(JpegError::NotImplemented { sof });
4638    }
4639    Ok(())
4640}
4641
4642fn extended12_color_sampling(
4643    plan: &PreparedDecodePlan,
4644    sof: SofKind,
4645) -> Result<Extended12ColorSampling, JpegError> {
4646    if plan.components.len() != 3 {
4647        return Err(JpegError::NotImplemented { sof });
4648    }
4649    let components = color_component_sampling_from_sequential(plan, sof)?;
4650    color_sampling_from_components(plan.sampling.max_h, plan.sampling.max_v, components, sof)
4651}
4652
4653fn extended12_four_component_sampling(
4654    plan: &PreparedDecodePlan,
4655    sof: SofKind,
4656) -> Result<Extended12ColorSampling, JpegError> {
4657    if plan.components.len() != 4 {
4658        return Err(JpegError::NotImplemented { sof });
4659    }
4660    let components = four_component_sampling_from_sequential(plan, sof)?;
4661    four_component_sampling_from_components(
4662        plan.sampling.max_h,
4663        plan.sampling.max_v,
4664        components,
4665        sof,
4666    )
4667}
4668
4669fn color_component_sampling_from_sequential(
4670    plan: &PreparedDecodePlan,
4671    sof: SofKind,
4672) -> Result<[(u8, u8); 3], JpegError> {
4673    let mut components = [(0u8, 0u8); 3];
4674    let mut seen = [false; 3];
4675    for component in &plan.components {
4676        if component.output_index > 2 || seen[component.output_index] {
4677            return Err(JpegError::NotImplemented { sof });
4678        }
4679        seen[component.output_index] = true;
4680        components[component.output_index] = (component.h, component.v);
4681    }
4682    if seen.iter().any(|&present| !present) {
4683        return Err(JpegError::NotImplemented { sof });
4684    }
4685    Ok(components)
4686}
4687
4688fn four_component_sampling_from_sequential(
4689    plan: &PreparedDecodePlan,
4690    sof: SofKind,
4691) -> Result<[(u8, u8); 4], JpegError> {
4692    let mut components = [(0u8, 0u8); 4];
4693    let mut seen = [false; 4];
4694    for component in &plan.components {
4695        if component.output_index > 3 || seen[component.output_index] {
4696            return Err(JpegError::NotImplemented { sof });
4697        }
4698        seen[component.output_index] = true;
4699        components[component.output_index] = (component.h, component.v);
4700    }
4701    if seen.iter().any(|&present| !present) {
4702        return Err(JpegError::NotImplemented { sof });
4703    }
4704    Ok(components)
4705}
4706
4707fn progressive_color_sampling(
4708    plan: &PreparedProgressivePlan,
4709    sof: SofKind,
4710) -> Result<Extended12ColorSampling, JpegError> {
4711    if plan.components.len() != 3 {
4712        return Err(JpegError::NotImplemented { sof });
4713    }
4714    let components = color_component_sampling_from_progressive(plan, sof)?;
4715    color_sampling_from_components(plan.sampling.max_h, plan.sampling.max_v, components, sof)
4716}
4717
4718fn progressive_four_component_sampling(
4719    plan: &PreparedProgressivePlan,
4720    sof: SofKind,
4721) -> Result<Extended12ColorSampling, JpegError> {
4722    if plan.components.len() != 4 {
4723        return Err(JpegError::NotImplemented { sof });
4724    }
4725    let components = four_component_sampling_from_progressive(plan, sof)?;
4726    four_component_sampling_from_components(
4727        plan.sampling.max_h,
4728        plan.sampling.max_v,
4729        components,
4730        sof,
4731    )
4732}
4733
4734fn color_component_sampling_from_progressive(
4735    plan: &PreparedProgressivePlan,
4736    sof: SofKind,
4737) -> Result<[(u8, u8); 3], JpegError> {
4738    let mut components = [(0u8, 0u8); 3];
4739    let mut seen = [false; 3];
4740    for component in &plan.components {
4741        if component.output_index > 2 || seen[component.output_index] {
4742            return Err(JpegError::NotImplemented { sof });
4743        }
4744        seen[component.output_index] = true;
4745        components[component.output_index] = (component.h, component.v);
4746    }
4747    if seen.iter().any(|&present| !present) {
4748        return Err(JpegError::NotImplemented { sof });
4749    }
4750    Ok(components)
4751}
4752
4753fn four_component_sampling_from_progressive(
4754    plan: &PreparedProgressivePlan,
4755    sof: SofKind,
4756) -> Result<[(u8, u8); 4], JpegError> {
4757    let mut components = [(0u8, 0u8); 4];
4758    let mut seen = [false; 4];
4759    for component in &plan.components {
4760        if component.output_index > 3 || seen[component.output_index] {
4761            return Err(JpegError::NotImplemented { sof });
4762        }
4763        seen[component.output_index] = true;
4764        components[component.output_index] = (component.h, component.v);
4765    }
4766    if seen.iter().any(|&present| !present) {
4767        return Err(JpegError::NotImplemented { sof });
4768    }
4769    Ok(components)
4770}
4771
4772fn color_sampling_from_components(
4773    max_h: u8,
4774    max_v: u8,
4775    components: [(u8, u8); 3],
4776    sof: SofKind,
4777) -> Result<Extended12ColorSampling, JpegError> {
4778    match (max_h, max_v, components) {
4779        (1, 1, [(1, 1), (1, 1), (1, 1)]) => Ok(Extended12ColorSampling::S444),
4780        (2, 1, [(2, 1), (1, 1), (1, 1)]) => Ok(Extended12ColorSampling::S422),
4781        (2, 2, [(2, 2), (1, 1), (1, 1)]) => Ok(Extended12ColorSampling::S420),
4782        _ => Err(JpegError::NotImplemented { sof }),
4783    }
4784}
4785
4786fn four_component_sampling_from_components(
4787    max_h: u8,
4788    max_v: u8,
4789    components: [(u8, u8); 4],
4790    sof: SofKind,
4791) -> Result<Extended12ColorSampling, JpegError> {
4792    match (max_h, max_v, components) {
4793        (1, 1, [(1, 1), (1, 1), (1, 1), (1, 1)]) => Ok(Extended12ColorSampling::S444),
4794        (2, 1, [(2, 1), (1, 1), (1, 1), (1, 1)]) => Ok(Extended12ColorSampling::S422),
4795        (2, 2, [(2, 2), (1, 1), (1, 1), (1, 1)]) => Ok(Extended12ColorSampling::S420),
4796        _ => Err(JpegError::NotImplemented { sof }),
4797    }
4798}
4799
4800fn progressive_color_component_indices(
4801    plan: &PreparedProgressivePlan,
4802) -> Result<[usize; 3], JpegError> {
4803    let mut indices = [usize::MAX; 3];
4804    for (component_index, component) in plan.components.iter().enumerate() {
4805        if component.output_index < 3 {
4806            if indices[component.output_index] != usize::MAX {
4807                return Err(JpegError::NotImplemented {
4808                    sof: SofKind::Progressive12,
4809                });
4810            }
4811            indices[component.output_index] = component_index;
4812        }
4813    }
4814    if indices.contains(&usize::MAX) {
4815        return Err(JpegError::NotImplemented {
4816            sof: SofKind::Progressive12,
4817        });
4818    }
4819    Ok(indices)
4820}
4821
4822fn dequantize_progressive12_block(coeffs: &[i32; 64], quant: &[u16; 64], out: &mut [i16; 64]) {
4823    out.fill(0);
4824    for k in 0..64 {
4825        let natural_idx = usize::from(ZIGZAG[k]);
4826        let value = coeffs[natural_idx].wrapping_mul(i32::from(quant[k]));
4827        out[natural_idx] = value.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
4828    }
4829}
4830
4831fn write_extended12_rgb_block_region(
4832    out: &mut [u8],
4833    stride: usize,
4834    region: Extended12WriteRegion,
4835    projection: Extended12RgbProjection,
4836    block_origin: (u32, u32),
4837    pixels: &[[u16; 64]; 3],
4838) {
4839    let (width, height) = region.dimensions;
4840    let (x0, y0) = block_origin;
4841    let block_x1 = (x0 + 8).min(width);
4842    let block_y1 = (y0 + 8).min(height);
4843    let denom = region.downscale.denominator();
4844    let output_rect = region.output_rect;
4845    for output_y in output_rect.y..output_rect.y + output_rect.h {
4846        let source_y = output_y.saturating_mul(denom).min(height - 1);
4847        if source_y < y0 || source_y >= block_y1 {
4848            continue;
4849        }
4850        let src_row = (source_y - y0) as usize;
4851        let dst_row = (output_y - output_rect.y) as usize;
4852        for output_x in output_rect.x..output_rect.x + output_rect.w {
4853            let source_x = output_x.saturating_mul(denom).min(width - 1);
4854            if source_x < x0 || source_x >= block_x1 {
4855                continue;
4856            }
4857            let src_col = (source_x - x0) as usize;
4858            let src_index = src_row * 8 + src_col;
4859            let dst_col = (output_x - output_rect.x) as usize;
4860            let dst_start = dst_row * stride + dst_col * 6;
4861            let dst = &mut out[dst_start..dst_start + 6];
4862            let (r, g, b) = match projection {
4863                Extended12RgbProjection::Identity => (
4864                    pixels[0][src_index],
4865                    pixels[1][src_index],
4866                    pixels[2][src_index],
4867                ),
4868                Extended12RgbProjection::YCbCr => crate::color::ycbcr::ycbcr12_to_rgb16(
4869                    pixels[0][src_index],
4870                    pixels[1][src_index],
4871                    pixels[2][src_index],
4872                ),
4873            };
4874            dst[0..2].copy_from_slice(&r.to_le_bytes());
4875            dst[2..4].copy_from_slice(&g.to_le_bytes());
4876            dst[4..6].copy_from_slice(&b.to_le_bytes());
4877        }
4878    }
4879}
4880
4881fn write_extended12_four_component_block_region(
4882    out: &mut [u8],
4883    stride: usize,
4884    region: Extended12WriteRegion,
4885    color_space: ColorSpace,
4886    block_origin: (u32, u32),
4887    pixels: &[[u16; 64]; 4],
4888) {
4889    let (width, height) = region.dimensions;
4890    let (x0, y0) = block_origin;
4891    let block_x1 = (x0 + 8).min(width);
4892    let block_y1 = (y0 + 8).min(height);
4893    let denom = region.downscale.denominator();
4894    let output_rect = region.output_rect;
4895    for output_y in output_rect.y..output_rect.y + output_rect.h {
4896        let source_y = output_y.saturating_mul(denom).min(height - 1);
4897        if source_y < y0 || source_y >= block_y1 {
4898            continue;
4899        }
4900        let src_row = (source_y - y0) as usize;
4901        let dst_row = (output_y - output_rect.y) as usize;
4902        for output_x in output_rect.x..output_rect.x + output_rect.w {
4903            let source_x = output_x.saturating_mul(denom).min(width - 1);
4904            if source_x < x0 || source_x >= block_x1 {
4905                continue;
4906            }
4907            let src_col = (source_x - x0) as usize;
4908            let src_index = src_row * 8 + src_col;
4909            let (r, g, b) = match color_space {
4910                ColorSpace::Cmyk => crate::color::cmyk::inverted_cmyk12_to_rgb16(
4911                    pixels[0][src_index],
4912                    pixels[1][src_index],
4913                    pixels[2][src_index],
4914                    pixels[3][src_index],
4915                ),
4916                ColorSpace::Ycck => crate::color::cmyk::ycck12_to_rgb16(
4917                    pixels[0][src_index],
4918                    pixels[1][src_index],
4919                    pixels[2][src_index],
4920                    pixels[3][src_index],
4921                ),
4922                _ => unreachable!("12-bit four-component path only accepts CMYK/YCCK"),
4923            };
4924            let dst_col = (output_x - output_rect.x) as usize;
4925            let dst_start = dst_row * stride + dst_col * 6;
4926            let dst = &mut out[dst_start..dst_start + 6];
4927            dst[0..2].copy_from_slice(&r.to_le_bytes());
4928            dst[2..4].copy_from_slice(&g.to_le_bytes());
4929            dst[4..6].copy_from_slice(&b.to_le_bytes());
4930        }
4931    }
4932}
4933
4934fn write_extended12_color422_planes_region(
4935    out: &mut [u8],
4936    stride: usize,
4937    region: Extended12WriteRegion,
4938    projection: Extended12RgbProjection,
4939    planes: &[Extended12Plane; 3],
4940) {
4941    let (width, height) = region.dimensions;
4942    let denom = region.downscale.denominator();
4943    let output_rect = region.output_rect;
4944    for output_y in output_rect.y..output_rect.y + output_rect.h {
4945        let source_y = output_y.saturating_mul(denom).min(height - 1) as usize;
4946        let dst_row = (output_y - output_rect.y) as usize;
4947        for output_x in output_rect.x..output_rect.x + output_rect.w {
4948            let source_x = output_x.saturating_mul(denom).min(width - 1) as usize;
4949            let y = planes[0].pixels[source_y * planes[0].stride + source_x];
4950            let chroma_y = source_y.min(planes[1].pixels.len() / planes[1].stride - 1);
4951            let cb_row = &planes[1].pixels
4952                [chroma_y * planes[1].stride..chroma_y * planes[1].stride + planes[1].width];
4953            let cr_row = &planes[2].pixels
4954                [chroma_y * planes[2].stride..chroma_y * planes[2].stride + planes[2].width];
4955            let c1 = upsample_h2v1_12bit_at(cb_row, source_x);
4956            let c2 = upsample_h2v1_12bit_at(cr_row, source_x);
4957            let (r, g, b) = match projection {
4958                Extended12RgbProjection::Identity => (y, c1, c2),
4959                Extended12RgbProjection::YCbCr => crate::color::ycbcr::ycbcr12_to_rgb16(y, c1, c2),
4960            };
4961            let dst_col = (output_x - output_rect.x) as usize;
4962            let dst_start = dst_row * stride + dst_col * 6;
4963            let dst = &mut out[dst_start..dst_start + 6];
4964            dst[0..2].copy_from_slice(&r.to_le_bytes());
4965            dst[2..4].copy_from_slice(&g.to_le_bytes());
4966            dst[4..6].copy_from_slice(&b.to_le_bytes());
4967        }
4968    }
4969}
4970
4971fn write_extended12_color420_planes_region(
4972    out: &mut [u8],
4973    stride: usize,
4974    region: Extended12WriteRegion,
4975    projection: Extended12RgbProjection,
4976    planes: &[Extended12Plane; 3],
4977) {
4978    let (width, height) = region.dimensions;
4979    let denom = region.downscale.denominator();
4980    let output_rect = region.output_rect;
4981    for output_y in output_rect.y..output_rect.y + output_rect.h {
4982        let source_y = output_y.saturating_mul(denom).min(height - 1) as usize;
4983        let dst_row = (output_y - output_rect.y) as usize;
4984        for output_x in output_rect.x..output_rect.x + output_rect.w {
4985            let source_x = output_x.saturating_mul(denom).min(width - 1) as usize;
4986            let y = planes[0].pixels[source_y * planes[0].stride + source_x];
4987            let chroma_height = planes[1].pixels.len() / planes[1].stride;
4988            let chroma_y = (source_y / 2).min(chroma_height - 1);
4989            let prev_y = chroma_y.saturating_sub(1);
4990            let next_y = (chroma_y + 1).min(chroma_height - 1);
4991            let c1 = upsample_h2v2_12bit_at(
4992                extended12_plane_row(&planes[1], prev_y),
4993                extended12_plane_row(&planes[1], chroma_y),
4994                extended12_plane_row(&planes[1], next_y),
4995                source_x,
4996                !source_y.is_multiple_of(2),
4997            );
4998            let c2 = upsample_h2v2_12bit_at(
4999                extended12_plane_row(&planes[2], prev_y),
5000                extended12_plane_row(&planes[2], chroma_y),
5001                extended12_plane_row(&planes[2], next_y),
5002                source_x,
5003                !source_y.is_multiple_of(2),
5004            );
5005            let (r, g, b) = match projection {
5006                Extended12RgbProjection::Identity => (y, c1, c2),
5007                Extended12RgbProjection::YCbCr => crate::color::ycbcr::ycbcr12_to_rgb16(y, c1, c2),
5008            };
5009            let dst_col = (output_x - output_rect.x) as usize;
5010            let dst_start = dst_row * stride + dst_col * 6;
5011            let dst = &mut out[dst_start..dst_start + 6];
5012            dst[0..2].copy_from_slice(&r.to_le_bytes());
5013            dst[2..4].copy_from_slice(&g.to_le_bytes());
5014            dst[4..6].copy_from_slice(&b.to_le_bytes());
5015        }
5016    }
5017}
5018
5019fn write_extended12_four_component_planes_region(
5020    out: &mut [u8],
5021    stride: usize,
5022    region: Extended12WriteRegion,
5023    color_space: ColorSpace,
5024    sampling: Extended12ColorSampling,
5025    planes: &[Extended12Plane; 4],
5026) {
5027    let (width, height) = region.dimensions;
5028    let denom = region.downscale.denominator();
5029    let output_rect = region.output_rect;
5030    for output_y in output_rect.y..output_rect.y + output_rect.h {
5031        let source_y = output_y.saturating_mul(denom).min(height - 1) as usize;
5032        let dst_row = (output_y - output_rect.y) as usize;
5033        for output_x in output_rect.x..output_rect.x + output_rect.w {
5034            let source_x = output_x.saturating_mul(denom).min(width - 1) as usize;
5035            let c0 = planes[0].pixels[source_y * planes[0].stride + source_x];
5036            let (c1, c2, c3) = match sampling {
5037                Extended12ColorSampling::S444 => (
5038                    sample_extended12_plane_at(&planes[1], source_x, source_y),
5039                    sample_extended12_plane_at(&planes[2], source_x, source_y),
5040                    sample_extended12_plane_at(&planes[3], source_x, source_y),
5041                ),
5042                Extended12ColorSampling::S422 => (
5043                    upsample_extended12_plane_h2v1_at(&planes[1], source_x, source_y),
5044                    upsample_extended12_plane_h2v1_at(&planes[2], source_x, source_y),
5045                    upsample_extended12_plane_h2v1_at(&planes[3], source_x, source_y),
5046                ),
5047                Extended12ColorSampling::S420 => (
5048                    upsample_extended12_plane_h2v2_at(&planes[1], source_x, source_y),
5049                    upsample_extended12_plane_h2v2_at(&planes[2], source_x, source_y),
5050                    upsample_extended12_plane_h2v2_at(&planes[3], source_x, source_y),
5051                ),
5052            };
5053            let (r, g, b) = match color_space {
5054                ColorSpace::Cmyk => crate::color::cmyk::inverted_cmyk12_to_rgb16(c0, c1, c2, c3),
5055                ColorSpace::Ycck => crate::color::cmyk::ycck12_to_rgb16(c0, c1, c2, c3),
5056                _ => unreachable!("12-bit four-component plane path only accepts CMYK/YCCK"),
5057            };
5058            let dst_col = (output_x - output_rect.x) as usize;
5059            let dst_start = dst_row * stride + dst_col * 6;
5060            let dst = &mut out[dst_start..dst_start + 6];
5061            dst[0..2].copy_from_slice(&r.to_le_bytes());
5062            dst[2..4].copy_from_slice(&g.to_le_bytes());
5063            dst[4..6].copy_from_slice(&b.to_le_bytes());
5064        }
5065    }
5066}
5067
5068fn sample_extended12_plane_at(plane: &Extended12Plane, source_x: usize, source_y: usize) -> u16 {
5069    let height = plane.pixels.len() / plane.stride;
5070    let y = source_y.min(height - 1);
5071    let x = source_x.min(plane.width - 1);
5072    plane.pixels[y * plane.stride + x]
5073}
5074
5075fn upsample_extended12_plane_h2v1_at(
5076    plane: &Extended12Plane,
5077    source_x: usize,
5078    source_y: usize,
5079) -> u16 {
5080    let height = plane.pixels.len() / plane.stride;
5081    let y = source_y.min(height - 1);
5082    upsample_h2v1_12bit_at(extended12_plane_row(plane, y), source_x)
5083}
5084
5085fn upsample_extended12_plane_h2v2_at(
5086    plane: &Extended12Plane,
5087    source_x: usize,
5088    source_y: usize,
5089) -> u16 {
5090    let height = plane.pixels.len() / plane.stride;
5091    let chroma_y = (source_y / 2).min(height - 1);
5092    let prev_y = chroma_y.saturating_sub(1);
5093    let next_y = (chroma_y + 1).min(height - 1);
5094    upsample_h2v2_12bit_at(
5095        extended12_plane_row(plane, prev_y),
5096        extended12_plane_row(plane, chroma_y),
5097        extended12_plane_row(plane, next_y),
5098        source_x,
5099        !source_y.is_multiple_of(2),
5100    )
5101}
5102
5103fn extended12_plane_row(plane: &Extended12Plane, y: usize) -> &[u16] {
5104    let row_start = y * plane.stride;
5105    &plane.pixels[row_start..row_start + plane.width]
5106}
5107
5108fn upsample_h2v1_12bit_at(row: &[u16], output_x: usize) -> u16 {
5109    debug_assert!(!row.is_empty());
5110    if row.len() == 1 {
5111        return row[0];
5112    }
5113    let sample = output_x / 2;
5114    if output_x == 0 {
5115        row[0]
5116    } else if output_x == row.len() * 2 - 1 {
5117        row[row.len() - 1]
5118    } else if output_x.is_multiple_of(2) {
5119        ((3 * u32::from(row[sample]) + u32::from(row[sample - 1]) + 2) / 4) as u16
5120    } else {
5121        ((3 * u32::from(row[sample]) + u32::from(row[sample + 1]) + 2) / 4) as u16
5122    }
5123}
5124
5125fn upsample_h2v2_12bit_at(
5126    prev: &[u16],
5127    curr: &[u16],
5128    next: &[u16],
5129    output_x: usize,
5130    output_is_bottom: bool,
5131) -> u16 {
5132    debug_assert!(!curr.is_empty());
5133    debug_assert_eq!(prev.len(), curr.len());
5134    debug_assert_eq!(next.len(), curr.len());
5135    let near = if output_is_bottom { next } else { prev };
5136    let colsum = |index: usize| 3 * u32::from(curr[index]) + u32::from(near[index]);
5137    if curr.len() == 1 {
5138        return ((4 * colsum(0) + 8) >> 4) as u16;
5139    }
5140
5141    let sample = output_x / 2;
5142    let this = colsum(sample);
5143    match output_x {
5144        0 => ((this * 4 + 8) >> 4) as u16,
5145        _ if output_x == curr.len() * 2 - 1 => ((this * 4 + 7) >> 4) as u16,
5146        _ if output_x.is_multiple_of(2) => {
5147            let last = colsum(sample - 1);
5148            ((this * 3 + last + 8) >> 4) as u16
5149        }
5150        _ => {
5151            let next = colsum(sample + 1);
5152            ((this * 3 + next + 7) >> 4) as u16
5153        }
5154    }
5155}
5156
5157fn write_extended12_block_region(
5158    out: &mut [u8],
5159    stride: usize,
5160    region: Extended12WriteRegion,
5161    block_origin: (u32, u32),
5162    pixels: &[u16; 64],
5163) {
5164    let (width, height) = region.dimensions;
5165    let (x0, y0) = block_origin;
5166    let block_x1 = (x0 + 8).min(width);
5167    let block_y1 = (y0 + 8).min(height);
5168    let denom = region.downscale.denominator();
5169    let bytes_per_pixel = match region.output {
5170        Extended12Output::Gray16 => 2,
5171        Extended12Output::Rgb16 => 6,
5172    };
5173    let output_rect = region.output_rect;
5174    for output_y in output_rect.y..output_rect.y + output_rect.h {
5175        let source_y = output_y.saturating_mul(denom).min(height - 1);
5176        if source_y < y0 || source_y >= block_y1 {
5177            continue;
5178        }
5179        let src_row = (source_y - y0) as usize;
5180        let dst_row = (output_y - output_rect.y) as usize;
5181        for output_x in output_rect.x..output_rect.x + output_rect.w {
5182            let source_x = output_x.saturating_mul(denom).min(width - 1);
5183            if source_x < x0 || source_x >= block_x1 {
5184                continue;
5185            }
5186            let src_col = (source_x - x0) as usize;
5187            let sample = pixels[src_row * 8 + src_col].to_le_bytes();
5188            let dst_col = (output_x - output_rect.x) as usize;
5189            let dst_start = dst_row * stride + dst_col * bytes_per_pixel;
5190            let dst = &mut out[dst_start..dst_start + bytes_per_pixel];
5191            match region.output {
5192                Extended12Output::Gray16 => {
5193                    dst.copy_from_slice(&sample);
5194                }
5195                Extended12Output::Rgb16 => {
5196                    dst[0..2].copy_from_slice(&sample);
5197                    dst[2..4].copy_from_slice(&sample);
5198                    dst[4..6].copy_from_slice(&sample);
5199                }
5200            }
5201        }
5202    }
5203}
5204
5205fn restart_index_for_stream(
5206    bytes: &[u8],
5207    scan_data_offset: Option<usize>,
5208    info: &Info,
5209    restart_interval: Option<u16>,
5210) -> Result<Option<RestartIndex>, JpegError> {
5211    let Some(interval_mcus) = restart_interval
5212        .filter(|&interval| interval > 0)
5213        .map(u32::from)
5214    else {
5215        return Ok(None);
5216    };
5217    let scan_data_offset = scan_data_offset.ok_or(JpegError::MissingMarker {
5218        marker: MarkerKind::Sos,
5219    })?;
5220    if !matches!(info.sof_kind, SofKind::Baseline8 | SofKind::Extended8) || info.scan_count != 1 {
5221        return Err(JpegError::NotImplemented { sof: info.sof_kind });
5222    }
5223    let total_mcus = info.mcu_geometry.count;
5224    let expected_restarts = total_mcus.saturating_sub(1) / interval_mcus;
5225    let mut segments = Vec::new();
5226    segments.push(RestartSegment {
5227        start_mcu: 0,
5228        entropy_offset: scan_data_offset,
5229        marker_offset: None,
5230        marker: None,
5231    });
5232
5233    let mut found_restarts = 0u32;
5234    let mut expected_rst = 0xd0u8;
5235    let mut pos = scan_data_offset;
5236    while pos < bytes.len() {
5237        if bytes[pos] != 0xff {
5238            pos += 1;
5239            continue;
5240        }
5241
5242        let mut marker_code_pos = pos + 1;
5243        while marker_code_pos < bytes.len() && bytes[marker_code_pos] == 0xff {
5244            marker_code_pos += 1;
5245        }
5246        if marker_code_pos >= bytes.len() {
5247            return Err(JpegError::Truncated {
5248                offset: pos,
5249                expected: 1,
5250            });
5251        }
5252
5253        let marker = bytes[marker_code_pos];
5254        let marker_offset = marker_code_pos - 1;
5255        match marker {
5256            0x00 => pos = marker_code_pos + 1,
5257            0xd0..=0xd7 => {
5258                if found_restarts >= expected_restarts {
5259                    return Err(JpegError::UnexpectedMarker {
5260                        offset: marker_offset,
5261                        expected: MarkerKind::Eoi,
5262                        found: marker,
5263                    });
5264                }
5265                if marker != expected_rst {
5266                    return Err(JpegError::RestartMismatch {
5267                        offset: marker_offset,
5268                        expected: expected_rst & 0x07,
5269                        found: marker,
5270                    });
5271                }
5272                found_restarts += 1;
5273                segments.push(RestartSegment {
5274                    start_mcu: found_restarts.saturating_mul(interval_mcus),
5275                    entropy_offset: marker_code_pos + 1,
5276                    marker_offset: Some(marker_offset),
5277                    marker: Some(marker),
5278                });
5279                expected_rst = if expected_rst == 0xd7 {
5280                    0xd0
5281                } else {
5282                    expected_rst + 1
5283                };
5284                pos = marker_code_pos + 1;
5285            }
5286            0xd9 => {
5287                if found_restarts != expected_restarts {
5288                    return Err(JpegError::UnexpectedEoi {
5289                        mcu_at: found_restarts
5290                            .saturating_add(1)
5291                            .saturating_mul(interval_mcus),
5292                        mcu_total: total_mcus,
5293                    });
5294                }
5295                return Ok(Some(RestartIndex {
5296                    scan_data_offset,
5297                    interval_mcus,
5298                    segments,
5299                }));
5300            }
5301            found => {
5302                return Err(JpegError::UnexpectedMarker {
5303                    offset: marker_offset,
5304                    expected: MarkerKind::Eoi,
5305                    found,
5306                });
5307            }
5308        }
5309    }
5310
5311    Err(JpegError::MissingMarker {
5312        marker: MarkerKind::Eoi,
5313    })
5314}
5315
5316fn output_format_profile_name(fmt: OutputFormat) -> &'static str {
5317    match fmt {
5318        OutputFormat::Rgb8 | OutputFormat::Rgb8Scaled { .. } => "Rgb8",
5319        OutputFormat::Rgba8 { .. } | OutputFormat::Rgba8Scaled { .. } => "Rgba8",
5320        OutputFormat::Gray8 | OutputFormat::Gray8Scaled { .. } => "Gray8",
5321        OutputFormat::Gray16 | OutputFormat::Gray16Scaled { .. } => "Gray16",
5322        OutputFormat::Rgb16 | OutputFormat::Rgb16Scaled { .. } => "Rgb16",
5323        OutputFormat::Rgba16 { .. } | OutputFormat::Rgba16Scaled { .. } => "Rgba16",
5324    }
5325}
5326
5327fn downscale_profile_name(downscale: DownscaleFactor) -> &'static str {
5328    match downscale {
5329        DownscaleFactor::Full => "full",
5330        DownscaleFactor::Half => "half",
5331        DownscaleFactor::Quarter => "quarter",
5332        DownscaleFactor::Eighth => "eighth",
5333    }
5334}
5335
5336fn emit_decode_scan_profile(
5337    scan_path: &str,
5338    dimensions: (u32, u32),
5339    decoded: Rect,
5340    downscale: DownscaleFactor,
5341    elapsed: Duration,
5342) {
5343    let source_width_s = dimensions.0.to_string();
5344    let source_height_s = dimensions.1.to_string();
5345    let decoded_x_s = decoded.x.to_string();
5346    let decoded_y_s = decoded.y.to_string();
5347    let decoded_w_s = decoded.w.to_string();
5348    let decoded_h_s = decoded.h.to_string();
5349    let scan_us = duration_us_string(elapsed);
5350    emit_jpeg_profile_row(
5351        "decode_scan",
5352        "cpu",
5353        &[
5354            ("scan_path", scan_path),
5355            ("downscale", downscale_profile_name(downscale)),
5356            ("source_width", source_width_s.as_str()),
5357            ("source_height", source_height_s.as_str()),
5358            ("decoded_x", decoded_x_s.as_str()),
5359            ("decoded_y", decoded_y_s.as_str()),
5360            ("decoded_w", decoded_w_s.as_str()),
5361            ("decoded_h", decoded_h_s.as_str()),
5362            ("scan_us", scan_us.as_str()),
5363        ],
5364    );
5365}
5366
5367fn consume_lossless_restart(
5368    br: &mut BitReader<'_>,
5369    sample_index: u32,
5370    total_samples: u32,
5371    expected_rst: &mut u8,
5372) -> Result<(), JpegError> {
5373    br.reset_at_restart();
5374    let _ = br.ensure_bits(1);
5375    let marker = br.take_marker().ok_or(JpegError::UnexpectedEoi {
5376        mcu_at: sample_index,
5377        mcu_total: total_samples,
5378    })?;
5379    let expected = 0xD0 | *expected_rst;
5380    if marker != expected {
5381        return Err(JpegError::RestartMismatch {
5382            offset: br.position(),
5383            expected: *expected_rst,
5384            found: marker,
5385        });
5386    }
5387    *expected_rst = (*expected_rst + 1) & 0x07;
5388    br.reset_at_restart();
5389    Ok(())
5390}
5391
5392fn consume_extended12_restart(
5393    br: &mut BitReader<'_>,
5394    mcu_index: u32,
5395    total_mcus: u32,
5396    expected_rst: &mut u8,
5397) -> Result<(), JpegError> {
5398    let _ = br.ensure_bits(1);
5399    let marker = br.take_marker().ok_or(JpegError::UnexpectedEoi {
5400        mcu_at: mcu_index,
5401        mcu_total: total_mcus,
5402    })?;
5403    let expected = 0xD0 | *expected_rst;
5404    if marker != expected {
5405        return Err(JpegError::RestartMismatch {
5406            offset: br.position(),
5407            expected: *expected_rst,
5408            found: marker,
5409        });
5410    }
5411    *expected_rst = (*expected_rst + 1) & 0x07;
5412    br.reset_at_restart();
5413    Ok(())
5414}
5415
5416fn lossless_predictor_value(predictor: u8, out: &[u8], stride: usize, x: usize, y: usize) -> i32 {
5417    lossless_predict(predictor, 128, x, y, |sx, sy| {
5418        i32::from(out[sy * stride + sx])
5419    })
5420}
5421
5422fn lossless_predictor_color_into<P: LosslessSample>(
5423    predictor: u8,
5424    out: &[u8],
5425    stride: usize,
5426    x: usize,
5427    y: usize,
5428    component: usize,
5429) -> i32 {
5430    lossless_predict(predictor, P::RESTART_PREDICTOR, x, y, |sx, sy| {
5431        P::read_le(&out[sy * stride + (sx * 3 + component) * P::BYTES..])
5432    })
5433}
5434
5435fn lossless_predictor_gray_rows<P: LosslessSample>(
5436    predictor: u8,
5437    curr_row: &[u8],
5438    prev_row: &[u8],
5439    x: usize,
5440    y: usize,
5441) -> i32 {
5442    lossless_predict(predictor, P::RESTART_PREDICTOR, x, y, |sx, sy| {
5443        let row = if sy == y { curr_row } else { prev_row };
5444        P::read_le(&row[sx * P::BYTES..])
5445    })
5446}
5447
5448fn lossless_predictor_color_rows<P: LosslessSample>(
5449    predictor: u8,
5450    curr_row: &[u8],
5451    prev_row: &[u8],
5452    x: usize,
5453    y: usize,
5454    component: usize,
5455) -> i32 {
5456    lossless_predict(predictor, P::RESTART_PREDICTOR, x, y, |sx, sy| {
5457        let row = if sy == y { curr_row } else { prev_row };
5458        P::read_le(&row[(sx * 3 + component) * P::BYTES..])
5459    })
5460}
5461
5462#[derive(Clone, Copy)]
5463struct LosslessPlaneSample {
5464    x: usize,
5465    y: usize,
5466    restart_first_sample: bool,
5467}
5468
5469fn decode_lossless_plane_sample<P: LosslessSample>(
5470    br: &mut BitReader<'_>,
5471    table: &HuffmanTable,
5472    predictor: u8,
5473    plane: &mut [P],
5474    width: usize,
5475    sample: LosslessPlaneSample,
5476) -> Result<(), JpegError> {
5477    let predicted = if sample.restart_first_sample {
5478        P::RESTART_PREDICTOR
5479    } else {
5480        lossless_predictor_plane(predictor, plane, width, sample.x, sample.y)
5481    };
5482    let diff = table.decode_fast_dc(br)?;
5483    plane[sample.y * width + sample.x] = P::from_i32(predicted + diff)?;
5484    Ok(())
5485}
5486
5487fn lossless_predictor_plane<P: LosslessSample>(
5488    predictor: u8,
5489    plane: &[P],
5490    width: usize,
5491    x: usize,
5492    y: usize,
5493) -> i32 {
5494    lossless_predict(predictor, P::RESTART_PREDICTOR, x, y, |sx, sy| {
5495        plane[sy * width + sx].into()
5496    })
5497}
5498
5499struct LosslessColorPlanes<'a, P> {
5500    c0: &'a [P],
5501    c1: &'a [P],
5502    c2: &'a [P],
5503}
5504
5505fn write_lossless_color8_sampled_output(
5506    out: &mut [u8],
5507    stride: usize,
5508    color_space: ColorSpace,
5509    sampling: LosslessColorSampling,
5510    dimensions: (usize, usize),
5511    planes: LosslessColorPlanes<'_, u8>,
5512) {
5513    let (width, height) = dimensions;
5514    let chroma_width = width.div_ceil(2);
5515    let chroma_height = match sampling {
5516        LosslessColorSampling::S422 => height,
5517        LosslessColorSampling::S420 => height.div_ceil(2),
5518        LosslessColorSampling::S444 => unreachable!("sampled writer is not used for 4:4:4"),
5519    };
5520    for y in 0..height {
5521        for x in 0..width {
5522            let c0_sample = planes.c0[y * width + x];
5523            let (c1_sample, c2_sample) = match sampling {
5524                LosslessColorSampling::S422 => {
5525                    let c1_row = &planes.c1[y * chroma_width..(y + 1) * chroma_width];
5526                    let c2_row = &planes.c2[y * chroma_width..(y + 1) * chroma_width];
5527                    (
5528                        upsample_h2v1_u8_at(c1_row, x),
5529                        upsample_h2v1_u8_at(c2_row, x),
5530                    )
5531                }
5532                LosslessColorSampling::S420 => (
5533                    upsample_h2v2_u8_at(planes.c1, chroma_width, chroma_height, width, x, y),
5534                    upsample_h2v2_u8_at(planes.c2, chroma_width, chroma_height, width, x, y),
5535                ),
5536                LosslessColorSampling::S444 => unreachable!("sampled writer is not used for 4:4:4"),
5537            };
5538            let (r, g, b) = match color_space {
5539                ColorSpace::Rgb => (c0_sample, c1_sample, c2_sample),
5540                ColorSpace::YCbCr => {
5541                    crate::color::ycbcr::ycbcr_to_rgb(c0_sample, c1_sample, c2_sample)
5542                }
5543                _ => unreachable!("lossless sampled color path only accepts RGB/YCbCr"),
5544            };
5545            let dst = y * stride + x * 3;
5546            out[dst..dst + 3].copy_from_slice(&[r, g, b]);
5547        }
5548    }
5549}
5550
5551fn write_lossless_color16_sampled_output(
5552    out: &mut [u8],
5553    stride: usize,
5554    color_space: ColorSpace,
5555    sampling: LosslessColorSampling,
5556    dimensions: (usize, usize),
5557    planes: LosslessColorPlanes<'_, u16>,
5558) {
5559    let (width, height) = dimensions;
5560    let chroma_width = width.div_ceil(2);
5561    let chroma_height = match sampling {
5562        LosslessColorSampling::S422 => height,
5563        LosslessColorSampling::S420 => height.div_ceil(2),
5564        LosslessColorSampling::S444 => unreachable!("sampled writer is not used for 4:4:4"),
5565    };
5566    for y in 0..height {
5567        for x in 0..width {
5568            let c0_sample = planes.c0[y * width + x];
5569            let (c1_sample, c2_sample) = match sampling {
5570                LosslessColorSampling::S422 => {
5571                    let c1_row = &planes.c1[y * chroma_width..(y + 1) * chroma_width];
5572                    let c2_row = &planes.c2[y * chroma_width..(y + 1) * chroma_width];
5573                    (
5574                        upsample_h2v1_u16_at(c1_row, x),
5575                        upsample_h2v1_u16_at(c2_row, x),
5576                    )
5577                }
5578                LosslessColorSampling::S420 => (
5579                    upsample_h2v2_u16_at(planes.c1, chroma_width, chroma_height, width, x, y),
5580                    upsample_h2v2_u16_at(planes.c2, chroma_width, chroma_height, width, x, y),
5581                ),
5582                LosslessColorSampling::S444 => unreachable!("sampled writer is not used for 4:4:4"),
5583            };
5584            let (r, g, b) = match color_space {
5585                ColorSpace::Rgb => (c0_sample, c1_sample, c2_sample),
5586                ColorSpace::YCbCr => {
5587                    crate::color::ycbcr::ycbcr16_to_rgb16(c0_sample, c1_sample, c2_sample)
5588                }
5589                _ => unreachable!("lossless 4:2:2 color path only accepts RGB/YCbCr"),
5590            };
5591            let dst = y * stride + x * 6;
5592            out[dst..dst + 2].copy_from_slice(&r.to_le_bytes());
5593            out[dst + 2..dst + 4].copy_from_slice(&g.to_le_bytes());
5594            out[dst + 4..dst + 6].copy_from_slice(&b.to_le_bytes());
5595        }
5596    }
5597}
5598
5599fn upsample_h2v2_u8_at(
5600    plane: &[u8],
5601    chroma_width: usize,
5602    chroma_height: usize,
5603    output_width: usize,
5604    output_x: usize,
5605    output_y: usize,
5606) -> u8 {
5607    debug_assert!(!plane.is_empty());
5608    debug_assert!(chroma_width > 0);
5609    debug_assert!(chroma_height > 0);
5610    let chroma_y = output_y / 2;
5611    let current = &plane[chroma_y * chroma_width..(chroma_y + 1) * chroma_width];
5612    let near_y = if output_y.is_multiple_of(2) {
5613        chroma_y.saturating_sub(1)
5614    } else {
5615        (chroma_y + 1).min(chroma_height - 1)
5616    };
5617    let near = &plane[near_y * chroma_width..(near_y + 1) * chroma_width];
5618    let colsum = |index: usize| 3 * u32::from(current[index]) + u32::from(near[index]);
5619    if chroma_width == 1 {
5620        return ((4 * colsum(0) + 8) >> 4) as u8;
5621    }
5622
5623    let sample = output_x / 2;
5624    let this = colsum(sample);
5625    match output_x {
5626        0 => ((this * 4 + 8) >> 4) as u8,
5627        _ if output_x == output_width - 1 => ((this * 4 + 7) >> 4) as u8,
5628        _ if output_x.is_multiple_of(2) => {
5629            let last = colsum(sample - 1);
5630            ((this * 3 + last + 8) >> 4) as u8
5631        }
5632        _ => {
5633            let next = colsum(sample + 1);
5634            ((this * 3 + next + 7) >> 4) as u8
5635        }
5636    }
5637}
5638
5639fn upsample_h2v1_u8_at(row: &[u8], output_x: usize) -> u8 {
5640    debug_assert!(!row.is_empty());
5641    if row.len() == 1 {
5642        return row[0];
5643    }
5644    let sample = output_x / 2;
5645    if output_x == 0 {
5646        row[0]
5647    } else if output_x == row.len() * 2 - 1 {
5648        row[row.len() - 1]
5649    } else if output_x.is_multiple_of(2) {
5650        ((3 * u32::from(row[sample]) + u32::from(row[sample - 1]) + 2) / 4) as u8
5651    } else {
5652        ((3 * u32::from(row[sample]) + u32::from(row[sample + 1]) + 2) / 4) as u8
5653    }
5654}
5655
5656fn upsample_h2v2_u16_at(
5657    plane: &[u16],
5658    chroma_width: usize,
5659    chroma_height: usize,
5660    output_width: usize,
5661    output_x: usize,
5662    output_y: usize,
5663) -> u16 {
5664    debug_assert!(!plane.is_empty());
5665    debug_assert!(chroma_width > 0);
5666    debug_assert!(chroma_height > 0);
5667    let chroma_y = output_y / 2;
5668    let current = &plane[chroma_y * chroma_width..(chroma_y + 1) * chroma_width];
5669    let near_y = if output_y.is_multiple_of(2) {
5670        chroma_y.saturating_sub(1)
5671    } else {
5672        (chroma_y + 1).min(chroma_height - 1)
5673    };
5674    let near = &plane[near_y * chroma_width..(near_y + 1) * chroma_width];
5675    let colsum = |index: usize| 3 * u32::from(current[index]) + u32::from(near[index]);
5676    if chroma_width == 1 {
5677        return ((4 * colsum(0) + 8) >> 4) as u16;
5678    }
5679
5680    let sample = output_x / 2;
5681    let this = colsum(sample);
5682    match output_x {
5683        0 => ((this * 4 + 8) >> 4) as u16,
5684        _ if output_x == output_width - 1 => ((this * 4 + 7) >> 4) as u16,
5685        _ if output_x.is_multiple_of(2) => {
5686            let last = colsum(sample - 1);
5687            ((this * 3 + last + 8) >> 4) as u16
5688        }
5689        _ => {
5690            let next = colsum(sample + 1);
5691            ((this * 3 + next + 7) >> 4) as u16
5692        }
5693    }
5694}
5695
5696fn upsample_h2v1_u16_at(row: &[u16], output_x: usize) -> u16 {
5697    debug_assert!(!row.is_empty());
5698    if row.len() == 1 {
5699        return row[0];
5700    }
5701    let sample = output_x / 2;
5702    if output_x == 0 {
5703        row[0]
5704    } else if output_x == row.len() * 2 - 1 {
5705        row[row.len() - 1]
5706    } else if output_x.is_multiple_of(2) {
5707        ((3 * u32::from(row[sample]) + u32::from(row[sample - 1]) + 2) / 4) as u16
5708    } else {
5709        ((3 * u32::from(row[sample]) + u32::from(row[sample + 1]) + 2) / 4) as u16
5710    }
5711}
5712
5713fn lossless_predictor_value_u16(
5714    predictor: u8,
5715    out: &[u8],
5716    stride: usize,
5717    x: usize,
5718    y: usize,
5719) -> i32 {
5720    lossless_predict(predictor, 32768, x, y, |sx, sy| {
5721        i32::from(read_gray16_sample(out, sy * stride + sx * 2))
5722    })
5723}
5724
5725fn read_gray16_sample(out: &[u8], offset: usize) -> u16 {
5726    u16::from_le_bytes([out[offset], out[offset + 1]])
5727}
5728
5729fn merged_warnings(header_warnings: &[Warning], scan_warnings: Vec<Warning>) -> Vec<Warning> {
5730    if header_warnings.is_empty() {
5731        return scan_warnings;
5732    }
5733    if scan_warnings.is_empty() {
5734        return header_warnings.to_vec();
5735    }
5736    let mut warnings = Vec::with_capacity(header_warnings.len() + scan_warnings.len());
5737    warnings.extend_from_slice(header_warnings);
5738    warnings.extend(scan_warnings);
5739    warnings
5740}
5741
5742fn copy_gray8_scaled_rect(
5743    full: &[u8],
5744    dimensions: (u32, u32),
5745    output_rect: Rect,
5746    denom: u32,
5747    out: &mut [u8],
5748    stride: usize,
5749) {
5750    let (width, height) = dimensions;
5751    for output_y in output_rect.y..output_rect.y + output_rect.h {
5752        let source_y = output_y.saturating_mul(denom).min(height - 1);
5753        let dst_row = (output_y - output_rect.y) as usize;
5754        let dst_start = dst_row * stride;
5755        let dst = &mut out[dst_start..dst_start + output_rect.w as usize];
5756        for (dst_px, output_x) in (output_rect.x..output_rect.x + output_rect.w).enumerate() {
5757            let source_x = output_x.saturating_mul(denom).min(width - 1);
5758            let src = source_y as usize * width as usize + source_x as usize;
5759            dst[dst_px] = full[src];
5760        }
5761    }
5762}
5763
5764fn copy_rgb8_scaled_rect(
5765    full: &[u8],
5766    dimensions: (u32, u32),
5767    output_rect: Rect,
5768    denom: u32,
5769    out: &mut [u8],
5770    stride: usize,
5771) {
5772    let (width, height) = dimensions;
5773    for output_y in output_rect.y..output_rect.y + output_rect.h {
5774        let source_y = output_y.saturating_mul(denom).min(height - 1);
5775        let dst_row = (output_y - output_rect.y) as usize;
5776        for output_x in output_rect.x..output_rect.x + output_rect.w {
5777            let source_x = output_x.saturating_mul(denom).min(width - 1);
5778            let src = (source_y as usize * width as usize + source_x as usize) * 3;
5779            let dst = dst_row * stride + (output_x - output_rect.x) as usize * 3;
5780            out[dst..dst + 3].copy_from_slice(&full[src..src + 3]);
5781        }
5782    }
5783}
5784
5785fn convert_ycbcr8_to_rgb8_in_place(out: &mut [u8], stride: usize, dimensions: (u32, u32)) {
5786    let (width, height) = dimensions;
5787    let row_bytes = width as usize * 3;
5788    for y in 0..height as usize {
5789        let row = &mut out[y * stride..y * stride + row_bytes];
5790        for pixel in row.chunks_exact_mut(3) {
5791            let (r, g, b) = crate::color::ycbcr::ycbcr_to_rgb(pixel[0], pixel[1], pixel[2]);
5792            pixel.copy_from_slice(&[r, g, b]);
5793        }
5794    }
5795}
5796
5797fn copy_ycbcr8_row_to_rgb8(src: &[u8], dst: &mut [u8]) {
5798    debug_assert_eq!(src.len(), dst.len());
5799    for (source, target) in src.chunks_exact(3).zip(dst.chunks_exact_mut(3)) {
5800        let (r, g, b) = crate::color::ycbcr::ycbcr_to_rgb(source[0], source[1], source[2]);
5801        target.copy_from_slice(&[r, g, b]);
5802    }
5803}
5804
5805fn copy_rgb8_to_rgba8(
5806    src: &[u8],
5807    src_stride: usize,
5808    width: u32,
5809    height: u32,
5810    dst: &mut [u8],
5811    dst_stride: usize,
5812    alpha: u8,
5813) {
5814    let src_row_bytes = width as usize * 3;
5815    let dst_row_bytes = width as usize * 4;
5816    for y in 0..height as usize {
5817        let src_row = &src[y * src_stride..y * src_stride + src_row_bytes];
5818        let dst_row = &mut dst[y * dst_stride..y * dst_stride + dst_row_bytes];
5819        for (source, target) in src_row.chunks_exact(3).zip(dst_row.chunks_exact_mut(4)) {
5820            target.copy_from_slice(&[source[0], source[1], source[2], alpha]);
5821        }
5822    }
5823}
5824
5825fn copy_rgb16_to_rgba16(
5826    src: &[u8],
5827    src_stride: usize,
5828    width: u32,
5829    height: u32,
5830    dst: &mut [u8],
5831    dst_stride: usize,
5832    alpha: u16,
5833) {
5834    let src_row_bytes = width as usize * 6;
5835    let dst_row_bytes = width as usize * 8;
5836    let alpha = alpha.to_le_bytes();
5837    for y in 0..height as usize {
5838        let src_row = &src[y * src_stride..y * src_stride + src_row_bytes];
5839        let dst_row = &mut dst[y * dst_stride..y * dst_stride + dst_row_bytes];
5840        for (source, target) in src_row.chunks_exact(6).zip(dst_row.chunks_exact_mut(8)) {
5841            target[..6].copy_from_slice(source);
5842            target[6..8].copy_from_slice(&alpha);
5843        }
5844    }
5845}
5846
5847fn convert_ycbcr16_to_rgb16_in_place(out: &mut [u8], stride: usize, dimensions: (u32, u32)) {
5848    let (width, height) = dimensions;
5849    let row_bytes = width as usize * 6;
5850    for y in 0..height as usize {
5851        let row = &mut out[y * stride..y * stride + row_bytes];
5852        for pixel in row.chunks_exact_mut(6) {
5853            let y = u16::from_le_bytes([pixel[0], pixel[1]]);
5854            let cb = u16::from_le_bytes([pixel[2], pixel[3]]);
5855            let cr = u16::from_le_bytes([pixel[4], pixel[5]]);
5856            let (r, g, b) = crate::color::ycbcr::ycbcr16_to_rgb16(y, cb, cr);
5857            pixel[0..2].copy_from_slice(&r.to_le_bytes());
5858            pixel[2..4].copy_from_slice(&g.to_le_bytes());
5859            pixel[4..6].copy_from_slice(&b.to_le_bytes());
5860        }
5861    }
5862}
5863
5864fn copy_ycbcr16_row_to_rgb16(src: &[u8], dst: &mut [u8]) {
5865    debug_assert_eq!(src.len(), dst.len());
5866    for (source, target) in src.chunks_exact(6).zip(dst.chunks_exact_mut(6)) {
5867        let y = u16::from_le_bytes([source[0], source[1]]);
5868        let cb = u16::from_le_bytes([source[2], source[3]]);
5869        let cr = u16::from_le_bytes([source[4], source[5]]);
5870        let (r, g, b) = crate::color::ycbcr::ycbcr16_to_rgb16(y, cb, cr);
5871        target[0..2].copy_from_slice(&r.to_le_bytes());
5872        target[2..4].copy_from_slice(&g.to_le_bytes());
5873        target[4..6].copy_from_slice(&b.to_le_bytes());
5874    }
5875}
5876
5877fn copy_rgb16_scaled_rect(
5878    full: &[u8],
5879    dimensions: (u32, u32),
5880    output_rect: Rect,
5881    denom: u32,
5882    out: &mut [u8],
5883    stride: usize,
5884) {
5885    let (width, height) = dimensions;
5886    let full_stride = width as usize * 6;
5887    for output_y in output_rect.y..output_rect.y + output_rect.h {
5888        let source_y = output_y.saturating_mul(denom).min(height - 1);
5889        let dst_row = (output_y - output_rect.y) as usize;
5890        for output_x in output_rect.x..output_rect.x + output_rect.w {
5891            let source_x = output_x.saturating_mul(denom).min(width - 1);
5892            let src = source_y as usize * full_stride + source_x as usize * 6;
5893            let dst = dst_row * stride + (output_x - output_rect.x) as usize * 6;
5894            out[dst..dst + 6].copy_from_slice(&full[src..src + 6]);
5895        }
5896    }
5897}
5898
5899fn copy_gray16_scaled_rect(
5900    full: &[u8],
5901    dimensions: (u32, u32),
5902    output_rect: Rect,
5903    denom: u32,
5904    out: &mut [u8],
5905    stride: usize,
5906) {
5907    let (width, height) = dimensions;
5908    let full_stride = width as usize * 2;
5909    for output_y in output_rect.y..output_rect.y + output_rect.h {
5910        let source_y = output_y.saturating_mul(denom).min(height - 1);
5911        let dst_row = (output_y - output_rect.y) as usize;
5912        for output_x in output_rect.x..output_rect.x + output_rect.w {
5913            let source_x = output_x.saturating_mul(denom).min(width - 1);
5914            let src = source_y as usize * full_stride + source_x as usize * 2;
5915            let dst = dst_row * stride + (output_x - output_rect.x) as usize * 2;
5916            out[dst..dst + 2].copy_from_slice(&full[src..src + 2]);
5917        }
5918    }
5919}
5920
5921fn jpeg_passthrough_syntax(info: &Info) -> Option<CompressedTransferSyntax> {
5922    match info.sof_kind {
5923        SofKind::Baseline8 if info.bit_depth == 8 => Some(CompressedTransferSyntax::JpegBaseline8),
5924        SofKind::Extended8 | SofKind::Extended12 => {
5925            Some(CompressedTransferSyntax::JpegExtendedSequential)
5926        }
5927        SofKind::Baseline8 | SofKind::Progressive8 | SofKind::Progressive12 | SofKind::Lossless => {
5928            None
5929        }
5930    }
5931}
5932
5933fn core_outcome(outcome: DecodeOutcome) -> CoreDecodeOutcome<Warning> {
5934    outcome.into()
5935}
5936
5937fn jpeg_downscale(scale: Downscale) -> DownscaleFactor {
5938    match scale {
5939        Downscale::None => DownscaleFactor::Full,
5940        Downscale::Half => DownscaleFactor::Half,
5941        Downscale::Quarter => DownscaleFactor::Quarter,
5942        Downscale::Eighth => DownscaleFactor::Eighth,
5943        _ => unreachable!("unsupported Downscale variant"),
5944    }
5945}
5946
5947fn output_format_from_parts(
5948    sof_kind: SofKind,
5949    fmt: PixelFormat,
5950    scale: Downscale,
5951) -> Result<OutputFormat, JpegError> {
5952    if matches!(sof_kind, SofKind::Extended12 | SofKind::Progressive12) {
5953        return match (sof_kind, fmt, scale) {
5954            (SofKind::Extended12, PixelFormat::Gray16, Downscale::None) => Ok(OutputFormat::Gray16),
5955            (SofKind::Extended12, PixelFormat::Gray16, scale) => Ok(OutputFormat::Gray16Scaled {
5956                factor: jpeg_downscale(scale),
5957            }),
5958            (SofKind::Extended12, PixelFormat::Rgb16, Downscale::None) => Ok(OutputFormat::Rgb16),
5959            (SofKind::Extended12, PixelFormat::Rgb16, scale) => Ok(OutputFormat::Rgb16Scaled {
5960                factor: jpeg_downscale(scale),
5961            }),
5962            (SofKind::Extended12, PixelFormat::Rgba16, Downscale::None) => {
5963                Ok(OutputFormat::Rgba16 { alpha: u16::MAX })
5964            }
5965            (SofKind::Extended12, PixelFormat::Rgba16, scale) => Ok(OutputFormat::Rgba16Scaled {
5966                alpha: u16::MAX,
5967                factor: jpeg_downscale(scale),
5968            }),
5969            (SofKind::Progressive12, PixelFormat::Gray16, Downscale::None) => {
5970                Ok(OutputFormat::Gray16)
5971            }
5972            (SofKind::Progressive12, PixelFormat::Gray16, scale) => {
5973                Ok(OutputFormat::Gray16Scaled {
5974                    factor: jpeg_downscale(scale),
5975                })
5976            }
5977            (SofKind::Progressive12, PixelFormat::Rgb16, Downscale::None) => {
5978                Ok(OutputFormat::Rgb16)
5979            }
5980            (SofKind::Progressive12, PixelFormat::Rgb16, scale) => Ok(OutputFormat::Rgb16Scaled {
5981                factor: jpeg_downscale(scale),
5982            }),
5983            (SofKind::Progressive12, PixelFormat::Rgba16, Downscale::None) => {
5984                Ok(OutputFormat::Rgba16 { alpha: u16::MAX })
5985            }
5986            (SofKind::Progressive12, PixelFormat::Rgba16, scale) => {
5987                Ok(OutputFormat::Rgba16Scaled {
5988                    alpha: u16::MAX,
5989                    factor: jpeg_downscale(scale),
5990                })
5991            }
5992            (_, PixelFormat::Rgb16 | PixelFormat::Rgba16 | PixelFormat::Gray16, _) => {
5993                Err(JpegError::NotImplemented { sof: sof_kind })
5994            }
5995            _ => Err(JpegError::UnsupportedBitDepth { depth: 12 }),
5996        };
5997    }
5998    if sof_kind == SofKind::Lossless {
5999        return match (fmt, scale) {
6000            (PixelFormat::Gray8, Downscale::None) => Ok(OutputFormat::Gray8),
6001            (PixelFormat::Gray8, scale) => Ok(OutputFormat::Gray8Scaled {
6002                factor: jpeg_downscale(scale),
6003            }),
6004            (PixelFormat::Gray16, Downscale::None) => Ok(OutputFormat::Gray16),
6005            (PixelFormat::Gray16, scale) => Ok(OutputFormat::Gray16Scaled {
6006                factor: jpeg_downscale(scale),
6007            }),
6008            (PixelFormat::Rgb8, Downscale::None) => Ok(OutputFormat::Rgb8),
6009            (PixelFormat::Rgb8, scale) => Ok(OutputFormat::Rgb8Scaled {
6010                factor: jpeg_downscale(scale),
6011            }),
6012            (PixelFormat::Rgba8, Downscale::None) => Ok(OutputFormat::Rgba8 { alpha: 255 }),
6013            (PixelFormat::Rgba8, scale) => Ok(OutputFormat::Rgba8Scaled {
6014                alpha: 255,
6015                factor: jpeg_downscale(scale),
6016            }),
6017            (PixelFormat::Rgb16, Downscale::None) => Ok(OutputFormat::Rgb16),
6018            (PixelFormat::Rgb16, scale) => Ok(OutputFormat::Rgb16Scaled {
6019                factor: jpeg_downscale(scale),
6020            }),
6021            (PixelFormat::Rgba16, Downscale::None) => Ok(OutputFormat::Rgba16 { alpha: u16::MAX }),
6022            (PixelFormat::Rgba16, scale) => Ok(OutputFormat::Rgba16Scaled {
6023                alpha: u16::MAX,
6024                factor: jpeg_downscale(scale),
6025            }),
6026            _ => Err(JpegError::NotImplemented { sof: sof_kind }),
6027        };
6028    }
6029
6030    match (fmt, scale) {
6031        (PixelFormat::Rgb8, Downscale::None) => Ok(OutputFormat::Rgb8),
6032        (PixelFormat::Rgb8, scale) => Ok(OutputFormat::Rgb8Scaled {
6033            factor: jpeg_downscale(scale),
6034        }),
6035        (PixelFormat::Gray8, Downscale::None) => Ok(OutputFormat::Gray8),
6036        (PixelFormat::Gray8, scale) => Ok(OutputFormat::Gray8Scaled {
6037            factor: jpeg_downscale(scale),
6038        }),
6039        (PixelFormat::Rgba8, Downscale::None) => Ok(OutputFormat::Rgba8 { alpha: 255 }),
6040        (PixelFormat::Rgba8, scale) => Ok(OutputFormat::Rgba8Scaled {
6041            alpha: 255,
6042            factor: jpeg_downscale(scale),
6043        }),
6044        (PixelFormat::Rgb16 | PixelFormat::Rgba16 | PixelFormat::Gray16, _) => {
6045            Err(JpegError::UnsupportedBitDepth { depth: 16 })
6046        }
6047        _ => Err(JpegError::DownscaleUnsupported { sof: sof_kind }),
6048    }
6049}
6050
6051impl ImageCodec for JpegCodec {
6052    type Error = JpegError;
6053    type Warning = Warning;
6054    type Pool = ScratchPool;
6055}
6056
6057impl ImageCodec for Decoder<'_> {
6058    type Error = JpegError;
6059    type Warning = Warning;
6060    type Pool = ScratchPool;
6061}
6062
6063impl<'a> ImageDecode<'a> for Decoder<'a> {
6064    type View = JpegView<'a>;
6065
6066    fn inspect(input: &'a [u8]) -> Result<j2k_core::Info, Self::Error> {
6067        Ok(Decoder::inspect(input)?.to_core_info())
6068    }
6069
6070    fn parse(input: &'a [u8]) -> Result<Self::View, Self::Error> {
6071        JpegView::parse(input)
6072    }
6073
6074    fn from_view(view: Self::View) -> Result<Self, Self::Error> {
6075        Decoder::from_view(view)
6076    }
6077
6078    fn decode_into(
6079        &mut self,
6080        out: &mut [u8],
6081        stride: usize,
6082        fmt: PixelFormat,
6083    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6084        Decoder::decode_into(self, out, stride, fmt).map(core_outcome)
6085    }
6086
6087    fn decode_into_with_scratch(
6088        &mut self,
6089        pool: &mut Self::Pool,
6090        out: &mut [u8],
6091        stride: usize,
6092        fmt: PixelFormat,
6093    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6094        Decoder::decode_into_with_scratch(self, pool, out, stride, fmt).map(core_outcome)
6095    }
6096
6097    fn decode_region_into(
6098        &mut self,
6099        pool: &mut Self::Pool,
6100        out: &mut [u8],
6101        stride: usize,
6102        fmt: PixelFormat,
6103        roi: j2k_core::Rect,
6104    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6105        Decoder::decode_region_into_with_scratch(self, pool, out, stride, fmt, roi.into())
6106            .map(core_outcome)
6107    }
6108
6109    fn decode_scaled_into(
6110        &mut self,
6111        pool: &mut Self::Pool,
6112        out: &mut [u8],
6113        stride: usize,
6114        fmt: PixelFormat,
6115        scale: Downscale,
6116    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6117        Decoder::decode_scaled_into_with_scratch(self, pool, out, stride, fmt, scale)
6118            .map(core_outcome)
6119    }
6120
6121    fn decode_region_scaled_into(
6122        &mut self,
6123        pool: &mut Self::Pool,
6124        out: &mut [u8],
6125        stride: usize,
6126        fmt: PixelFormat,
6127        roi: j2k_core::Rect,
6128        scale: Downscale,
6129    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6130        Decoder::decode_region_scaled_into_with_scratch(
6131            self,
6132            pool,
6133            out,
6134            stride,
6135            fmt,
6136            roi.into(),
6137            scale,
6138        )
6139        .map(core_outcome)
6140    }
6141}
6142
6143struct CoreRowSinkAdapter<'a, R: RowSink<u8>> {
6144    sink: &'a mut R,
6145    sink_error: Option<R::Error>,
6146}
6147
6148impl<R: RowSink<u8>> RowSink<u8> for CoreRowSinkAdapter<'_, R> {
6149    type Error = JpegError;
6150
6151    fn write_row(&mut self, y: u32, row: &[u8]) -> Result<(), JpegError> {
6152        match self.sink.write_row(y, row) {
6153            Ok(()) => Ok(()),
6154            Err(err) => {
6155                self.sink_error = Some(err);
6156                Err(JpegError::RowSinkAborted)
6157            }
6158        }
6159    }
6160}
6161
6162impl<'a> ImageDecodeRows<'a, u8> for Decoder<'a> {
6163    fn decode_rows<R: RowSink<u8>>(
6164        &mut self,
6165        sink: &mut R,
6166    ) -> Result<CoreDecodeOutcome<Self::Warning>, DecodeRowsError<Self::Error, R::Error>> {
6167        let mut adapter = CoreRowSinkAdapter {
6168            sink,
6169            sink_error: None,
6170        };
6171        match Decoder::decode_rows(self, &mut adapter) {
6172            Ok(outcome) => Ok(core_outcome(outcome)),
6173            Err(JpegError::RowSinkAborted) => Err(DecodeRowsError::Sink(
6174                adapter
6175                    .sink_error
6176                    .expect("row sink abort stores the original sink error"),
6177            )),
6178            Err(err) => Err(DecodeRowsError::Decode(err)),
6179        }
6180    }
6181}
6182
6183impl TileBatchDecode for JpegCodec {
6184    type Context = DecoderContext;
6185
6186    fn decode_tile(
6187        ctx: &mut CoreDecoderContext<Self::Context>,
6188        pool: &mut Self::Pool,
6189        input: &[u8],
6190        out: &mut [u8],
6191        stride: usize,
6192        fmt: PixelFormat,
6193    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6194        let dec = Decoder::from_view_in_context(JpegView::parse(input)?, ctx.codec_mut())?;
6195        dec.decode_into_with_scratch(pool, out, stride, fmt)
6196            .map(core_outcome)
6197    }
6198
6199    fn decode_tile_region(
6200        ctx: &mut CoreDecoderContext<Self::Context>,
6201        pool: &mut Self::Pool,
6202        input: &[u8],
6203        out: &mut [u8],
6204        stride: usize,
6205        fmt: PixelFormat,
6206        roi: j2k_core::Rect,
6207    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6208        let dec = Decoder::from_view_in_context(JpegView::parse(input)?, ctx.codec_mut())?;
6209        dec.decode_region_into_with_scratch(pool, out, stride, fmt, roi.into())
6210            .map(core_outcome)
6211    }
6212
6213    fn decode_tile_scaled(
6214        ctx: &mut CoreDecoderContext<Self::Context>,
6215        pool: &mut Self::Pool,
6216        input: &[u8],
6217        out: &mut [u8],
6218        stride: usize,
6219        fmt: PixelFormat,
6220        scale: Downscale,
6221    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6222        let dec = Decoder::from_view_in_context(JpegView::parse(input)?, ctx.codec_mut())?;
6223        dec.decode_scaled_into_with_scratch(pool, out, stride, fmt, scale)
6224            .map(core_outcome)
6225    }
6226
6227    fn decode_tile_region_scaled(
6228        ctx: &mut CoreDecoderContext<Self::Context>,
6229        pool: &mut Self::Pool,
6230        input: &[u8],
6231        out: &mut [u8],
6232        stride: usize,
6233        fmt: PixelFormat,
6234        roi: j2k_core::Rect,
6235        scale: Downscale,
6236    ) -> Result<CoreDecodeOutcome<Self::Warning>, Self::Error> {
6237        let dec = Decoder::from_view_in_context(JpegView::parse(input)?, ctx.codec_mut())?;
6238        dec.decode_region_scaled_into_with_scratch(pool, out, stride, fmt, roi.into(), scale)
6239            .map(core_outcome)
6240    }
6241}
6242
6243#[allow(clippy::uninit_vec)]
6244fn allocate_output_buffer(len: usize) -> Vec<u8> {
6245    let mut out = Vec::with_capacity(len);
6246    // Safety: all owned-output entrypoints use tight row strides, and the
6247    // decode writers fully initialize every byte in the destination on success.
6248    // If decode returns an error, dropping a Vec<u8> with uninitialized bytes is
6249    // still sound because `u8` has no drop glue.
6250    unsafe {
6251        out.set_len(len);
6252    }
6253    out
6254}
6255
6256fn scaled_dimensions(dims: (u32, u32), factor: DownscaleFactor) -> (u32, u32) {
6257    let denom = factor.denominator();
6258    (dims.0.div_ceil(denom), dims.1.div_ceil(denom))
6259}
6260
6261fn scaled_rect_covering(rect: Rect, factor: DownscaleFactor) -> Result<Rect, JpegError> {
6262    let denom = factor.denominator();
6263    let x_end = rect
6264        .x
6265        .checked_add(rect.w)
6266        .ok_or(JpegError::RectOutOfBounds {
6267            rect,
6268            width: u32::MAX,
6269            height: u32::MAX,
6270        })?;
6271    let y_end = rect
6272        .y
6273        .checked_add(rect.h)
6274        .ok_or(JpegError::RectOutOfBounds {
6275            rect,
6276            width: u32::MAX,
6277            height: u32::MAX,
6278        })?;
6279    let x0 = rect.x / denom;
6280    let y0 = rect.y / denom;
6281    let x1 = x_end.div_ceil(denom);
6282    let y1 = y_end.div_ceil(denom);
6283    Ok(Rect {
6284        x: x0,
6285        y: y0,
6286        w: x1.saturating_sub(x0),
6287        h: y1.saturating_sub(y0),
6288    })
6289}
6290
6291struct CroppedWriter<W> {
6292    inner: W,
6293    rect: Rect,
6294    source_x0: u32,
6295    source_width: u32,
6296    top_row: Vec<u8>,
6297    bottom_row: Vec<u8>,
6298}
6299
6300struct ProgressiveDownscaleWriter<'a, W> {
6301    inner: &'a mut W,
6302    denom: u32,
6303    scaled_width: usize,
6304    r: Vec<u8>,
6305    g: Vec<u8>,
6306    b: Vec<u8>,
6307}
6308
6309impl<'a, W> ProgressiveDownscaleWriter<'a, W> {
6310    fn new(inner: &'a mut W, downscale: DownscaleFactor, dimensions: (u32, u32)) -> Self {
6311        let denom = downscale.denominator();
6312        let scaled_width = dimensions.0.div_ceil(denom) as usize;
6313        Self {
6314            inner,
6315            denom,
6316            scaled_width,
6317            r: Vec::new(),
6318            g: Vec::new(),
6319            b: Vec::new(),
6320        }
6321    }
6322
6323    fn should_emit(&self, y: u32) -> bool {
6324        y.is_multiple_of(self.denom)
6325    }
6326
6327    fn sample_row(src: &[u8], denom: u32, width: usize, dst: &mut Vec<u8>) {
6328        dst.resize(width, 0);
6329        for (x, out) in dst.iter_mut().enumerate() {
6330            let src_x = (x as u32)
6331                .saturating_mul(denom)
6332                .min(src.len().saturating_sub(1) as u32);
6333            *out = src[src_x as usize];
6334        }
6335    }
6336}
6337
6338impl<W: OutputWriter> OutputWriter for ProgressiveDownscaleWriter<'_, W> {
6339    fn write_rgb_row(
6340        &mut self,
6341        y: u32,
6342        r_row: &[u8],
6343        g_row: &[u8],
6344        b_row: &[u8],
6345    ) -> Result<(), JpegError> {
6346        if !self.should_emit(y) {
6347            return Ok(());
6348        }
6349        Self::sample_row(r_row, self.denom, self.scaled_width, &mut self.r);
6350        Self::sample_row(g_row, self.denom, self.scaled_width, &mut self.g);
6351        Self::sample_row(b_row, self.denom, self.scaled_width, &mut self.b);
6352        self.inner
6353            .write_rgb_row(y / self.denom, &self.r, &self.g, &self.b)
6354    }
6355
6356    fn write_ycbcr_row(
6357        &mut self,
6358        y: u32,
6359        y_row: &[u8],
6360        cb_row: &[u8],
6361        cr_row: &[u8],
6362    ) -> Result<(), JpegError> {
6363        if !self.should_emit(y) {
6364            return Ok(());
6365        }
6366        Self::sample_row(y_row, self.denom, self.scaled_width, &mut self.r);
6367        Self::sample_row(cb_row, self.denom, self.scaled_width, &mut self.g);
6368        Self::sample_row(cr_row, self.denom, self.scaled_width, &mut self.b);
6369        self.inner
6370            .write_ycbcr_row(y / self.denom, &self.r, &self.g, &self.b)
6371    }
6372
6373    fn write_gray_row(&mut self, y: u32, gray_row: &[u8]) -> Result<(), JpegError> {
6374        if !self.should_emit(y) {
6375            return Ok(());
6376        }
6377        Self::sample_row(gray_row, self.denom, self.scaled_width, &mut self.r);
6378        self.inner.write_gray_row(y / self.denom, &self.r)
6379    }
6380}
6381
6382struct ComponentWriterAdapter<'a, W> {
6383    inner: &'a mut W,
6384}
6385
6386impl<W: ComponentRowWriter> OutputWriter for ComponentWriterAdapter<'_, W> {
6387    fn write_rgb_row(
6388        &mut self,
6389        y: u32,
6390        r_row: &[u8],
6391        g_row: &[u8],
6392        b_row: &[u8],
6393    ) -> Result<(), JpegError> {
6394        self.inner.write_rgb_row(y, r_row, g_row, b_row)
6395    }
6396
6397    fn write_ycbcr_row(
6398        &mut self,
6399        y: u32,
6400        y_row: &[u8],
6401        cb_row: &[u8],
6402        cr_row: &[u8],
6403    ) -> Result<(), JpegError> {
6404        self.inner.write_ycbcr_row(y, y_row, cb_row, cr_row)
6405    }
6406
6407    fn write_gray_row(&mut self, y: u32, gray_row: &[u8]) -> Result<(), JpegError> {
6408        self.inner.write_gray_row(y, gray_row)
6409    }
6410}
6411
6412impl<W> CroppedWriter<W> {
6413    fn new(inner: W, rect: Rect, source_x0: u32, source_width: u32) -> Self {
6414        let row_len = source_width as usize * 3;
6415        Self {
6416            inner,
6417            rect,
6418            source_x0,
6419            source_width,
6420            top_row: vec![0; row_len],
6421            bottom_row: vec![0; row_len],
6422        }
6423    }
6424}
6425
6426impl<W: OutputWriter> OutputWriter for CroppedWriter<W> {
6427    fn write_rgb_row(
6428        &mut self,
6429        y: u32,
6430        r_row: &[u8],
6431        g_row: &[u8],
6432        b_row: &[u8],
6433    ) -> Result<(), JpegError> {
6434        if y < self.rect.y || y >= self.rect.y + self.rect.h {
6435            return Ok(());
6436        }
6437        let x0 = self
6438            .rect
6439            .x
6440            .checked_sub(self.source_x0)
6441            .expect("crop window must cover requested rect") as usize;
6442        let x1 = x0 + self.rect.w as usize;
6443        self.inner.write_rgb_row(
6444            y - self.rect.y,
6445            &r_row[x0..x1],
6446            &g_row[x0..x1],
6447            &b_row[x0..x1],
6448        )
6449    }
6450
6451    fn write_ycbcr_row(
6452        &mut self,
6453        y: u32,
6454        y_row: &[u8],
6455        cb_row: &[u8],
6456        cr_row: &[u8],
6457    ) -> Result<(), JpegError> {
6458        if y < self.rect.y || y >= self.rect.y + self.rect.h {
6459            return Ok(());
6460        }
6461        let x0 = self
6462            .rect
6463            .x
6464            .checked_sub(self.source_x0)
6465            .expect("crop window must cover requested rect") as usize;
6466        let x1 = x0 + self.rect.w as usize;
6467        self.inner.write_ycbcr_row(
6468            y - self.rect.y,
6469            &y_row[x0..x1],
6470            &cb_row[x0..x1],
6471            &cr_row[x0..x1],
6472        )
6473    }
6474
6475    fn write_gray_row(&mut self, y: u32, gray_row: &[u8]) -> Result<(), JpegError> {
6476        if y < self.rect.y || y >= self.rect.y + self.rect.h {
6477            return Ok(());
6478        }
6479        let x0 = self
6480            .rect
6481            .x
6482            .checked_sub(self.source_x0)
6483            .expect("crop window must cover requested rect") as usize;
6484        let x1 = x0 + self.rect.w as usize;
6485        self.inner
6486            .write_gray_row(y - self.rect.y, &gray_row[x0..x1])
6487    }
6488}
6489
6490impl<W: InterleavedRgbWriter> InterleavedRgbWriter for CroppedWriter<W> {
6491    fn with_rgb_rows<R, F>(&mut self, y: u32, row_count: usize, fill: F) -> Result<R, JpegError>
6492    where
6493        F: FnOnce(&mut [u8], Option<&mut [u8]>) -> Result<R, JpegError>,
6494    {
6495        let row_len = self.source_width as usize * 3;
6496        if self.top_row.len() != row_len {
6497            self.top_row.resize(row_len, 0);
6498            self.bottom_row.resize(row_len, 0);
6499        }
6500
6501        let result = match row_count {
6502            1 => fill(&mut self.top_row, None)?,
6503            2 => fill(&mut self.top_row, Some(&mut self.bottom_row))?,
6504            _ => unreachable!("CroppedWriter only supports one or two rows"),
6505        };
6506
6507        let top_in = y >= self.rect.y && y < self.rect.y + self.rect.h;
6508        let bottom_y = y + 1;
6509        let bottom_in =
6510            row_count == 2 && bottom_y >= self.rect.y && bottom_y < self.rect.y + self.rect.h;
6511        let x0 = self
6512            .rect
6513            .x
6514            .checked_sub(self.source_x0)
6515            .expect("crop window must cover requested rect") as usize
6516            * 3;
6517        let x1 = x0 + self.rect.w as usize * 3;
6518
6519        match (top_in, bottom_in) {
6520            (false, false) => {}
6521            (true, false) => {
6522                self.inner.with_rgb_rows(y - self.rect.y, 1, |dst, _| {
6523                    dst.copy_from_slice(&self.top_row[x0..x1]);
6524                    Ok(())
6525                })?;
6526            }
6527            (false, true) => {
6528                self.inner
6529                    .with_rgb_rows(bottom_y - self.rect.y, 1, |dst, _| {
6530                        dst.copy_from_slice(&self.bottom_row[x0..x1]);
6531                        Ok(())
6532                    })?;
6533            }
6534            (true, true) => {
6535                self.inner
6536                    .with_rgb_rows(y - self.rect.y, 2, |dst_top, dst_bottom| {
6537                        dst_top.copy_from_slice(&self.top_row[x0..x1]);
6538                        dst_bottom
6539                            .expect("row_count=2 supplies bottom row")
6540                            .copy_from_slice(&self.bottom_row[x0..x1]);
6541                        Ok(())
6542                    })?;
6543            }
6544        }
6545
6546        Ok(result)
6547    }
6548}
6549
6550fn build_decode_plan(
6551    header: &ParsedHeader,
6552    info: &Info,
6553    dc_tables: &[Option<Arc<HuffmanTable>>; 4],
6554    ac_tables: &[Option<Arc<HuffmanTable>>; 4],
6555    ctx: &mut DecoderContext,
6556) -> Result<PreparedDecodePlan, JpegError> {
6557    let scan = header.scan.as_ref().ok_or(JpegError::MissingMarker {
6558        marker: MarkerKind::Sos,
6559    })?;
6560    let scan_offset = header.sos_offset.ok_or(JpegError::MissingMarker {
6561        marker: MarkerKind::Sos,
6562    })?;
6563
6564    let mut components = Vec::with_capacity(scan.components.len());
6565    for scan_comp in scan.components.iter().copied() {
6566        let output_index = find_component_index(&header.component_ids, scan_comp.id).ok_or(
6567            JpegError::UnknownScanComponent {
6568                offset: scan_offset,
6569                component: scan_comp.id,
6570            },
6571        )?;
6572        let (h, v) = header
6573            .sampling
6574            .component(output_index)
6575            .ok_or(JpegError::MissingMarker {
6576                marker: MarkerKind::Sof,
6577            })?;
6578        let quant_id =
6579            *header
6580                .quant_table_ids
6581                .get(output_index)
6582                .ok_or(JpegError::MissingMarker {
6583                    marker: MarkerKind::Sof,
6584                })? as usize;
6585        let quant = *header
6586            .quant_tables
6587            .entries
6588            .get(quant_id)
6589            .and_then(|q| q.as_ref())
6590            .ok_or(JpegError::MissingQuantTable {
6591                component: scan_comp.id,
6592                table_id: quant_id as u8,
6593            })?;
6594        let dc_table = dc_tables[scan_comp.dc_table as usize].as_ref().ok_or(
6595            JpegError::MissingHuffmanTable {
6596                component: scan_comp.id,
6597                class: 0,
6598                id: scan_comp.dc_table,
6599            },
6600        )?;
6601        let ac_table = ac_tables[scan_comp.ac_table as usize].as_ref().ok_or(
6602            JpegError::MissingHuffmanTable {
6603                component: scan_comp.id,
6604                class: 1,
6605                id: scan_comp.ac_table,
6606            },
6607        )?;
6608        components.push(PreparedComponentPlan {
6609            h,
6610            v,
6611            output_index,
6612            quant: ctx.resolve_quant_table(quant),
6613            dc_table: Arc::clone(dc_table),
6614            ac_table: Arc::clone(ac_table),
6615        });
6616    }
6617
6618    let mut scratch_bytes =
6619        compute_decode_scratch_bytes(info.dimensions, info.sampling, DEFAULT_MAX_DECODE_BYTES)?;
6620    if info.sof_kind == SofKind::Extended12 {
6621        // The sequential 12-bit paths render through full-frame u16 component
6622        // planes, which dwarf the stripe-based estimate above.
6623        scratch_bytes = scratch_bytes.max(compute_extended12_planes_scratch_bytes(
6624            &components,
6625            info.dimensions,
6626            info.sampling,
6627            DEFAULT_MAX_DECODE_BYTES,
6628        )?);
6629    }
6630
6631    Ok(PreparedDecodePlan {
6632        components,
6633        sampling: info.sampling,
6634        color_space: info.color_space,
6635        restart_interval: header.restart_interval,
6636        dimensions: info.dimensions,
6637        scan_offset,
6638        scratch_bytes,
6639    })
6640}
6641
6642fn validate_sampling_factors(header: &ParsedHeader, info: &Info) -> Result<(), JpegError> {
6643    validate_leading_component_sampling(header, info)?;
6644    for (h, v) in header.sampling.iter() {
6645        if h == 0 || v == 0 || h > 4 || v > 4 {
6646            return Err(JpegError::NotImplemented { sof: info.sof_kind });
6647        }
6648        if !header.sampling.max_h.is_multiple_of(h) || !header.sampling.max_v.is_multiple_of(v) {
6649            return Err(JpegError::NotImplemented { sof: info.sof_kind });
6650        }
6651    }
6652    Ok(())
6653}
6654
6655fn validate_leading_component_sampling(
6656    header: &ParsedHeader,
6657    info: &Info,
6658) -> Result<(), JpegError> {
6659    if !matches!(info.color_space, ColorSpace::YCbCr) {
6660        return Ok(());
6661    }
6662    if let Some((h, v)) = header.sampling.component(0) {
6663        if h != header.sampling.max_h || v != header.sampling.max_v {
6664            return Err(JpegError::NotImplemented { sof: info.sof_kind });
6665        }
6666    }
6667    Ok(())
6668}
6669
6670fn resolve_progressive_huffman(
6671    ctx: &mut DecoderContext,
6672    tables: &[Option<RawHuffmanTable>; 4],
6673    component: u8,
6674    class: u8,
6675    id: u8,
6676) -> Result<Arc<HuffmanTable>, JpegError> {
6677    let raw = tables
6678        .get(id as usize)
6679        .and_then(|table| table.as_ref())
6680        .ok_or(JpegError::MissingHuffmanTable {
6681            component,
6682            class,
6683            id,
6684        })?;
6685    ctx.resolve_huffman_table(raw)
6686}
6687
6688fn compute_progressive_scratch_bytes(
6689    components: &[PreparedProgressiveComponentPlan],
6690    output_width: usize,
6691) -> Result<usize, JpegError> {
6692    let cap = DEFAULT_MAX_DECODE_BYTES;
6693    let mut total = 0usize;
6694    for component in components {
6695        let blocks = checked_usize_product(
6696            &[component.block_cols as usize, component.block_rows as usize],
6697            cap,
6698        )?;
6699        let coeffs = checked_usize_product(&[blocks, 64, core::mem::size_of::<i32>()], cap)?;
6700        total = total
6701            .checked_add(coeffs)
6702            .ok_or(JpegError::MemoryCapExceeded {
6703                requested: usize::MAX,
6704                cap,
6705            })?;
6706
6707        let plane = checked_usize_product(
6708            &[
6709                component.block_cols as usize,
6710                component.block_rows as usize,
6711                64,
6712            ],
6713            cap,
6714        )?;
6715        total = total
6716            .checked_add(plane)
6717            .ok_or(JpegError::MemoryCapExceeded {
6718                requested: usize::MAX,
6719                cap,
6720            })?;
6721        if total > cap {
6722            return Err(JpegError::MemoryCapExceeded {
6723                requested: total,
6724                cap,
6725            });
6726        }
6727    }
6728    total =
6729        total
6730            .checked_add(output_width.saturating_mul(3))
6731            .ok_or(JpegError::MemoryCapExceeded {
6732                requested: usize::MAX,
6733                cap,
6734            })?;
6735    if total > cap {
6736        return Err(JpegError::MemoryCapExceeded {
6737            requested: total,
6738            cap,
6739        });
6740    }
6741    Ok(total)
6742}
6743
6744struct SinkWriter<'a, S> {
6745    sink: &'a mut S,
6746    rows: SinkRows,
6747    backend: Backend,
6748}
6749
6750impl<'a, S> SinkWriter<'a, S> {
6751    fn new(sink: &'a mut S, rows: SinkRows, backend: Backend) -> Self {
6752        debug_assert_eq!(rows.top_row.len(), rows.bottom_row.len());
6753        Self {
6754            sink,
6755            rows,
6756            backend,
6757        }
6758    }
6759
6760    fn into_rows(self) -> SinkRows {
6761        self.rows
6762    }
6763}
6764
6765impl<S> InterleavedRgbWriter for SinkWriter<'_, S>
6766where
6767    S: RowSink<u8, Error = JpegError>,
6768{
6769    fn with_rgb_rows<R, F>(&mut self, y: u32, row_count: usize, fill: F) -> Result<R, JpegError>
6770    where
6771        F: FnOnce(&mut [u8], Option<&mut [u8]>) -> Result<R, JpegError>,
6772    {
6773        let result = match row_count {
6774            1 => fill(&mut self.rows.top_row, None),
6775            2 => fill(&mut self.rows.top_row, Some(&mut self.rows.bottom_row)),
6776            _ => unreachable!("SinkWriter only supports one or two rows"),
6777        }?;
6778        self.sink.write_row(y, &self.rows.top_row)?;
6779        if row_count == 2 {
6780            self.sink.write_row(y + 1, &self.rows.bottom_row)?;
6781        }
6782        Ok(result)
6783    }
6784}
6785
6786impl<S> OutputWriter for SinkWriter<'_, S>
6787where
6788    S: RowSink<u8, Error = JpegError>,
6789{
6790    fn write_rgb_row(
6791        &mut self,
6792        y: u32,
6793        r_row: &[u8],
6794        g_row: &[u8],
6795        b_row: &[u8],
6796    ) -> Result<(), JpegError> {
6797        self.backend
6798            .fill_rgb_row_from_rgb(r_row, g_row, b_row, &mut self.rows.top_row);
6799        self.sink.write_row(y, &self.rows.top_row)
6800    }
6801
6802    fn write_ycbcr_row(
6803        &mut self,
6804        y: u32,
6805        y_row: &[u8],
6806        cb_row: &[u8],
6807        cr_row: &[u8],
6808    ) -> Result<(), JpegError> {
6809        self.backend
6810            .fill_rgb_row_from_ycbcr(y_row, cb_row, cr_row, &mut self.rows.top_row);
6811        self.sink.write_row(y, &self.rows.top_row)
6812    }
6813
6814    fn write_gray_row(&mut self, y: u32, gray_row: &[u8]) -> Result<(), JpegError> {
6815        self.backend
6816            .fill_rgb_row_from_gray(gray_row, &mut self.rows.top_row);
6817        self.sink.write_row(y, &self.rows.top_row)
6818    }
6819}
6820
6821fn find_component_index(component_ids: &[u8], id: u8) -> Option<usize> {
6822    component_ids
6823        .iter()
6824        .position(|&component_id| component_id == id)
6825}
6826
6827fn compute_decode_scratch_bytes(
6828    (width, height): (u32, u32),
6829    sampling: crate::info::SamplingFactors,
6830    cap: usize,
6831) -> Result<usize, JpegError> {
6832    let max_h = u32::from(sampling.max_h);
6833    let max_v = u32::from(sampling.max_v);
6834    let mcu_width = 8u32
6835        .checked_mul(max_h)
6836        .ok_or(JpegError::MemoryCapExceeded {
6837            requested: usize::MAX,
6838            cap,
6839        })?;
6840    let mcu_height = 8u32
6841        .checked_mul(max_v)
6842        .ok_or(JpegError::MemoryCapExceeded {
6843            requested: usize::MAX,
6844            cap,
6845        })?;
6846    let mcus_per_row = width.div_ceil(mcu_width);
6847    let _mcu_rows = height.div_ceil(mcu_height);
6848
6849    let mut stripe_total = 0usize;
6850    for (h, v) in sampling.iter() {
6851        let cols = checked_usize_product(&[mcus_per_row as usize, usize::from(h), 8usize], cap)?;
6852        let rows = checked_usize_product(&[usize::from(v), 8usize], cap)?;
6853        let plane = cols.checked_mul(rows).ok_or(JpegError::MemoryCapExceeded {
6854            requested: usize::MAX,
6855            cap,
6856        })?;
6857        stripe_total = stripe_total
6858            .checked_add(plane)
6859            .ok_or(JpegError::MemoryCapExceeded {
6860                requested: usize::MAX,
6861                cap,
6862            })?;
6863        if stripe_total > cap {
6864            return Err(JpegError::MemoryCapExceeded {
6865                requested: stripe_total,
6866                cap,
6867            });
6868        }
6869    }
6870
6871    let stripe_buffers = checked_usize_product(&[stripe_total, 3], cap)?;
6872    let row_scratch = checked_usize_product(&[width as usize, 7], cap)?;
6873    let total = stripe_buffers
6874        .checked_add(row_scratch)
6875        .ok_or(JpegError::MemoryCapExceeded {
6876            requested: usize::MAX,
6877            cap,
6878        })?;
6879    if total > cap {
6880        return Err(JpegError::MemoryCapExceeded {
6881            requested: total,
6882            cap,
6883        });
6884    }
6885
6886    Ok(total)
6887}
6888
6889fn checked_usize_product(factors: &[usize], cap: usize) -> Result<usize, JpegError> {
6890    let mut value = 1usize;
6891    for factor in factors {
6892        value = value
6893            .checked_mul(*factor)
6894            .ok_or(JpegError::MemoryCapExceeded {
6895                requested: usize::MAX,
6896                cap,
6897            })?;
6898    }
6899    Ok(value)
6900}
6901
6902/// Checked size for a transient full-frame intermediate buffer, enforcing the
6903/// decode memory cap at the allocation site.
6904fn checked_scratch_len(factors: &[usize]) -> Result<usize, JpegError> {
6905    let cap = DEFAULT_MAX_DECODE_BYTES;
6906    let len = checked_usize_product(factors, cap)?;
6907    if len > cap {
6908        return Err(JpegError::MemoryCapExceeded {
6909            requested: len,
6910            cap,
6911        });
6912    }
6913    Ok(len)
6914}
6915
6916fn output_cap_error(requested: usize) -> JpegError {
6917    JpegError::MemoryCapExceeded {
6918        requested,
6919        cap: DEFAULT_MAX_DECODE_BYTES,
6920    }
6921}
6922
6923#[inline]
6924fn checked_output_geometry(
6925    width: u32,
6926    height: u32,
6927    bytes_per_pixel: usize,
6928) -> Result<(usize, usize), JpegError> {
6929    #[cfg(target_pointer_width = "64")]
6930    {
6931        // SOF parsing caps JPEG dimensions at 65_500, so these products cannot
6932        // overflow usize on 64-bit targets. Keep the hot path to one cap check.
6933        let stride = width as usize * bytes_per_pixel;
6934        let len = stride * height as usize;
6935        if len > DEFAULT_MAX_DECODE_BYTES {
6936            return Err(output_cap_error(len));
6937        }
6938        Ok((stride, len))
6939    }
6940
6941    #[cfg(not(target_pointer_width = "64"))]
6942    {
6943        let stride = checked_output_product(width as usize, bytes_per_pixel)?;
6944        let len = checked_output_product(stride, height as usize)?;
6945        Ok((stride, len))
6946    }
6947}
6948
6949#[cfg(not(target_pointer_width = "64"))]
6950#[inline]
6951fn checked_output_product(left: usize, right: usize) -> Result<usize, JpegError> {
6952    let len = left
6953        .checked_mul(right)
6954        .ok_or_else(|| output_cap_error(usize::MAX))?;
6955    if len > DEFAULT_MAX_DECODE_BYTES {
6956        return Err(output_cap_error(len));
6957    }
6958    Ok(len)
6959}
6960
6961fn compute_lossless_scratch_bytes(info: &Info, cap: usize) -> Result<usize, JpegError> {
6962    // Only the sampled-color lossless paths materialize full-frame component
6963    // planes; grayscale and 4:4:4 decode stream straight into caller buffers.
6964    // Region/scaled lossless intermediates are capped at their allocation
6965    // sites via checked_scratch_len.
6966    if !matches!(
6967        lossless_color_sampling(info),
6968        Some(LosslessColorSampling::S422 | LosslessColorSampling::S420)
6969    ) {
6970        return Ok(0);
6971    }
6972    let width = info.dimensions.0 as usize;
6973    let height = info.dimensions.1 as usize;
6974    let bytes_per_sample: usize = if info.bit_depth > 8 { 2 } else { 1 };
6975    let chroma_width = width.div_ceil(usize::from(info.sampling.max_h));
6976    let chroma_height = height.div_ceil(usize::from(info.sampling.max_v));
6977    let luma = checked_usize_product(&[width, height, bytes_per_sample], cap)?;
6978    let chroma = checked_usize_product(&[chroma_width, chroma_height, bytes_per_sample, 2], cap)?;
6979    let total = luma
6980        .checked_add(chroma)
6981        .ok_or(JpegError::MemoryCapExceeded {
6982            requested: usize::MAX,
6983            cap,
6984        })?;
6985    if total > cap {
6986        return Err(JpegError::MemoryCapExceeded {
6987            requested: total,
6988            cap,
6989        });
6990    }
6991    Ok(total)
6992}
6993
6994fn compute_extended12_planes_scratch_bytes(
6995    components: &[PreparedComponentPlan],
6996    (width, height): (u32, u32),
6997    sampling: crate::info::SamplingFactors,
6998    cap: usize,
6999) -> Result<usize, JpegError> {
7000    let mcu_cols = width.div_ceil(u32::from(sampling.max_h) * 8) as usize;
7001    let mcu_rows = height.div_ceil(u32::from(sampling.max_v) * 8) as usize;
7002    let mut total = 0usize;
7003    for component in components {
7004        let stride = checked_usize_product(&[mcu_cols, usize::from(component.h), 8], cap)?;
7005        let rows = checked_usize_product(&[mcu_rows, usize::from(component.v), 8], cap)?;
7006        let plane = checked_usize_product(&[stride, rows, core::mem::size_of::<u16>()], cap)?;
7007        total = total
7008            .checked_add(plane)
7009            .ok_or(JpegError::MemoryCapExceeded {
7010                requested: usize::MAX,
7011                cap,
7012            })?;
7013    }
7014    if total > cap {
7015        return Err(JpegError::MemoryCapExceeded {
7016            requested: total,
7017            cap,
7018        });
7019    }
7020    Ok(total)
7021}
7022
7023#[cfg(test)]
7024mod tests {
7025    use super::*;
7026    use crate::error::Warning;
7027    use crate::output::OutputWriter;
7028    use alloc::vec;
7029    use alloc::vec::Vec;
7030
7031    fn minimal_baseline_jpeg() -> Vec<u8> {
7032        let mut v = Vec::new();
7033        v.extend_from_slice(&[0xFF, 0xD8]);
7034        v.extend_from_slice(&[0xFF, 0xDB, 0x00, 67, 0x00]);
7035        v.extend(core::iter::repeat_n(1u8, 64));
7036        v.extend_from_slice(&[
7037            0xFF,
7038            0xC0,
7039            0x00,
7040            17,
7041            8,
7042            0,
7043            16,
7044            0,
7045            16,
7046            3,
7047            1,
7048            (2 << 4) | 2,
7049            0,
7050            2,
7051            (1 << 4) | 1,
7052            0,
7053            3,
7054            (1 << 4) | 1,
7055            0,
7056        ]);
7057        v.extend_from_slice(&[
7058            0xFF, 0xC4, 0x00, 20, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xAA,
7059        ]);
7060        v.extend_from_slice(&[
7061            0xFF, 0xC4, 0x00, 20, 0x10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xBB,
7062        ]);
7063        v.extend_from_slice(&[0xFF, 0xDA, 0x00, 12, 3, 1, 0x00, 2, 0x00, 3, 0x00, 0, 63, 0]);
7064        v.extend_from_slice(&[0x00, 0xFF, 0xD9]);
7065        v
7066    }
7067
7068    fn baseline_jpeg_with_dimensions(width: u16, height: u16) -> Vec<u8> {
7069        let mut bytes = minimal_baseline_jpeg();
7070        let sof = bytes
7071            .windows(2)
7072            .position(|w| w == [0xFF, 0xC0])
7073            .expect("SOF0 marker");
7074        bytes[sof + 5..sof + 7].copy_from_slice(&height.to_be_bytes());
7075        bytes[sof + 7..sof + 9].copy_from_slice(&width.to_be_bytes());
7076        bytes
7077    }
7078
7079    fn dc_only_420_jpeg(width: u16, height: u16) -> Vec<u8> {
7080        let mut v = Vec::new();
7081        v.extend_from_slice(&[0xFF, 0xD8]);
7082        v.extend_from_slice(&[0xFF, 0xDB, 0x00, 67, 0x00]);
7083        v.extend(core::iter::repeat_n(1u8, 64));
7084        v.extend_from_slice(&[
7085            0xFF,
7086            0xC0,
7087            0x00,
7088            17,
7089            8,
7090            (height >> 8) as u8,
7091            height as u8,
7092            (width >> 8) as u8,
7093            width as u8,
7094            3,
7095            1,
7096            (2 << 4) | 2,
7097            0,
7098            2,
7099            (1 << 4) | 1,
7100            0,
7101            3,
7102            (1 << 4) | 1,
7103            0,
7104        ]);
7105        v.extend_from_slice(&[
7106            0xFF, 0xC4, 0x00, 20, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7107        ]);
7108        v.extend_from_slice(&[
7109            0xFF, 0xC4, 0x00, 20, 0x10, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
7110        ]);
7111        v.extend_from_slice(&[0xFF, 0xDA, 0x00, 12, 3, 1, 0x00, 2, 0x00, 3, 0x00, 0, 63, 0]);
7112
7113        let mcus_per_row = u32::from(width).div_ceil(16);
7114        let mcu_rows = u32::from(height).div_ceil(16);
7115        let entropy_bits = mcus_per_row * mcu_rows * 12;
7116        let entropy_bytes = (entropy_bits as usize).div_ceil(8) + 8;
7117        v.extend(core::iter::repeat_n(0u8, entropy_bytes));
7118        v.extend_from_slice(&[0xFF, 0xD9]);
7119        v
7120    }
7121
7122    #[test]
7123    fn decoder_new_succeeds_on_baseline_stream() {
7124        let bytes = minimal_baseline_jpeg();
7125        let dec = Decoder::new(&bytes).unwrap();
7126        assert_eq!(dec.info().dimensions, (16, 16));
7127    }
7128
7129    #[test]
7130    fn owned_baseline_decode_with_huge_dimensions_errors_before_allocating() {
7131        let bytes = baseline_jpeg_with_dimensions(65_500, 65_500);
7132        let dec = Decoder::new(&bytes).expect("huge baseline header should parse");
7133
7134        let err = dec.decode(PixelFormat::Rgb8).unwrap_err();
7135
7136        assert!(
7137            matches!(err, JpegError::MemoryCapExceeded { .. }),
7138            "expected MemoryCapExceeded before owned output allocation, got {err:?}"
7139        );
7140    }
7141
7142    #[test]
7143    fn owned_baseline_region_decode_with_huge_dimensions_errors_before_allocating() {
7144        let bytes = baseline_jpeg_with_dimensions(65_500, 65_500);
7145        let dec = Decoder::new(&bytes).expect("huge baseline header should parse");
7146
7147        let err = dec
7148            .decode_region(PixelFormat::Rgb8, Rect::full(dec.info().dimensions))
7149            .unwrap_err();
7150
7151        assert!(
7152            matches!(err, JpegError::MemoryCapExceeded { .. }),
7153            "expected MemoryCapExceeded before region output allocation, got {err:?}"
7154        );
7155    }
7156
7157    fn minimal_lossless_jpeg(
7158        width: u16,
7159        height: u16,
7160        precision: u8,
7161        sampling_420: bool,
7162    ) -> Vec<u8> {
7163        let mut v = Vec::new();
7164        v.extend_from_slice(&[0xFF, 0xD8]);
7165        let first_sampling = if sampling_420 {
7166            (2 << 4) | 2
7167        } else {
7168            (1 << 4) | 1
7169        };
7170        v.extend_from_slice(&[
7171            0xFF,
7172            0xC3,
7173            0x00,
7174            17,
7175            precision,
7176            (height >> 8) as u8,
7177            height as u8,
7178            (width >> 8) as u8,
7179            width as u8,
7180            3,
7181            1,
7182            first_sampling,
7183            0,
7184            2,
7185            (1 << 4) | 1,
7186            0,
7187            3,
7188            (1 << 4) | 1,
7189            0,
7190        ]);
7191        v.extend_from_slice(&[
7192            0xFF, 0xC4, 0x00, 20, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x00,
7193        ]);
7194        // SOS: predictor 1 (ss=1), point transform 0.
7195        v.extend_from_slice(&[0xFF, 0xDA, 0x00, 12, 3, 1, 0x00, 2, 0x00, 3, 0x00, 1, 0, 0]);
7196        v.extend_from_slice(&[0x00, 0x00, 0xFF, 0xD9]);
7197        v
7198    }
7199
7200    #[test]
7201    fn lossless_sampled_plan_with_huge_dimensions_is_rejected_by_memory_cap() {
7202        // 65500x65500 lossless 16-bit 4:2:0: the sampled-color decode path
7203        // would allocate ~12.9 GB of full-frame planes. Plan build must refuse.
7204        let bytes = minimal_lossless_jpeg(65_500, 65_500, 16, true);
7205        match Decoder::new(&bytes) {
7206            Err(JpegError::MemoryCapExceeded { .. }) => {}
7207            other => panic!("expected MemoryCapExceeded at plan build, got {other:?}"),
7208        }
7209    }
7210
7211    #[test]
7212    fn lossless_sampled_plan_with_small_dimensions_still_parses() {
7213        let bytes = minimal_lossless_jpeg(16, 16, 16, true);
7214        let dec = Decoder::new(&bytes).expect("small lossless stream should parse");
7215        assert_eq!(dec.info().dimensions, (16, 16));
7216        assert!(
7217            dec.plan.scratch_bytes > 0,
7218            "sampled lossless plan must report scratch"
7219        );
7220    }
7221
7222    #[test]
7223    fn extended12_plan_with_huge_dimensions_is_rejected_by_memory_cap() {
7224        // 65500x65500 Extended12 4:2:0: the sequential 12-bit path allocates
7225        // full-frame u16 planes (~13 GB). Plan build must refuse.
7226        let mut bytes = minimal_baseline_jpeg();
7227        let p = bytes.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap();
7228        bytes[p + 1] = 0xC1;
7229        bytes[p + 4] = 12;
7230        bytes[p + 5] = (65_500u16 >> 8) as u8;
7231        bytes[p + 6] = 65_500u16 as u8;
7232        bytes[p + 7] = (65_500u16 >> 8) as u8;
7233        bytes[p + 8] = 65_500u16 as u8;
7234        match Decoder::new(&bytes) {
7235            Err(JpegError::MemoryCapExceeded { .. }) => {}
7236            other => panic!("expected MemoryCapExceeded at plan build, got {other:?}"),
7237        }
7238    }
7239
7240    #[test]
7241    fn lossless_gray16_region_scaled_with_huge_dimensions_errors_before_allocating() {
7242        // Grayscale direct decode streams to the caller's buffer, so plan
7243        // scratch stays 0 — but the region/scaled path materializes the full
7244        // frame (~8.6 GB here) and must be stopped at the allocation site.
7245        let mut bytes = minimal_lossless_jpeg(65_500, 65_500, 16, false);
7246        let p = bytes.windows(2).position(|w| w == [0xFF, 0xC3]).unwrap();
7247        // Rewrite SOF to a single grayscale component.
7248        bytes[p + 3] = 11;
7249        bytes[p + 9] = 1;
7250        bytes[p + 10] = 1;
7251        bytes[p + 11] = (1 << 4) | 1;
7252        bytes[p + 12] = 0;
7253        bytes.splice(p + 13..p + 19, core::iter::empty());
7254        let p = bytes.windows(2).position(|w| w == [0xFF, 0xDA]).unwrap();
7255        bytes.splice(
7256            p..p + 14,
7257            [0xFF, 0xDA, 0x00, 8, 1, 1, 0x00, 1, 0, 0].iter().copied(),
7258        );
7259        let dec = Decoder::new(&bytes).expect("huge lossless gray stream should parse");
7260        let roi = Rect {
7261            x: 0,
7262            y: 0,
7263            w: 16,
7264            h: 16,
7265        };
7266        let mut out = vec![0u8; 16 * 16 * 2];
7267        let err = dec
7268            .decode_region_scaled_into(&mut out, 16 * 2, PixelFormat::Gray16, roi, Downscale::Half)
7269            .unwrap_err();
7270        assert!(
7271            matches!(err, JpegError::MemoryCapExceeded { .. }),
7272            "expected MemoryCapExceeded, got {err:?}"
7273        );
7274    }
7275
7276    #[test]
7277    fn decode_into_rejects_unsupported_extended12_ycbcr_sampling_with_not_implemented() {
7278        let mut bytes = minimal_baseline_jpeg();
7279        let p = bytes.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap();
7280        bytes[p + 1] = 0xC1;
7281        bytes[p + 4] = 12;
7282        bytes[p + 11] = (1 << 4) | 2;
7283        let dec = Decoder::new(&bytes).expect("unsupported Extended12 YCbCr sampling should parse");
7284        let stride = dec.info().dimensions.0 as usize * PixelFormat::Rgb16.bytes_per_pixel();
7285        let mut out = vec![0u8; stride * dec.info().dimensions.1 as usize];
7286
7287        let err = dec
7288            .decode_into(&mut out, stride, PixelFormat::Rgb16)
7289            .unwrap_err();
7290
7291        assert!(err.is_not_implemented());
7292    }
7293
7294    #[test]
7295    fn decoder_new_rejects_arithmetic_as_unsupported() {
7296        let mut bytes = minimal_baseline_jpeg();
7297        let p = bytes.windows(2).position(|w| w == [0xFF, 0xC0]).unwrap();
7298        bytes[p + 1] = 0xC9;
7299        let err = Decoder::new(&bytes).unwrap_err();
7300        assert!(err.is_unsupported());
7301    }
7302
7303    #[test]
7304    fn decode_outcome_carries_rect_and_warnings() {
7305        let outcome = DecodeOutcome {
7306            decoded: Rect {
7307                x: 0,
7308                y: 0,
7309                w: 16,
7310                h: 16,
7311            },
7312            warnings: vec![Warning::MissingEoi],
7313        };
7314        assert_eq!(outcome.decoded.w, 16);
7315        assert_eq!(outcome.warnings.len(), 1);
7316    }
7317
7318    #[test]
7319    fn decode_into_rejects_undersized_buffer() {
7320        let bytes = minimal_baseline_jpeg();
7321        let dec = Decoder::new(&bytes).unwrap();
7322        let mut buf = vec![0u8; 4];
7323        let err = dec
7324            .decode_into(&mut buf, 48, PixelFormat::Rgb8)
7325            .unwrap_err();
7326        assert!(matches!(err, JpegError::OutputBufferTooSmall { .. }));
7327    }
7328
7329    #[test]
7330    fn decode_into_rejects_invalid_stride() {
7331        let bytes = minimal_baseline_jpeg();
7332        let dec = Decoder::new(&bytes).unwrap();
7333        let mut buf = vec![0u8; 16 * 16 * 3];
7334        let err = dec
7335            .decode_into(&mut buf, 10, PixelFormat::Rgb8)
7336            .unwrap_err();
7337        assert!(matches!(err, JpegError::InvalidStride { .. }));
7338    }
7339
7340    #[test]
7341    fn large_fast_420_region_decode_populates_cpu_entropy_checkpoints() {
7342        let bytes = dc_only_420_jpeg(1024, 2048);
7343        let dec = Decoder::new(&bytes).expect("decoder");
7344        assert!(dec.plan.matches_fast_tile_shape());
7345
7346        let roi = Rect {
7347            x: 64,
7348            y: 1536,
7349            w: 64,
7350            h: 64,
7351        };
7352        let mut out = vec![0u8; roi.w as usize * roi.h as usize * 3];
7353        let mut pool = ScratchPool::new();
7354        dec.decode_region_into_with_scratch(
7355            &mut pool,
7356            &mut out,
7357            roi.w as usize * 3,
7358            PixelFormat::Rgb8,
7359            roi,
7360        )
7361        .expect("deep ROI decode");
7362
7363        let cache = dec
7364            .cpu_entropy_checkpoints
7365            .lock()
7366            .expect("checkpoint cache mutex");
7367        assert!(cache
7368            .checkpoints
7369            .iter()
7370            .any(|checkpoint| checkpoint.mcu_index >= CPU_ROI_CHECKPOINT_MIN_TARGET_MCUS));
7371    }
7372
7373    #[derive(Default)]
7374    struct GrayRows {
7375        rows: Vec<(u32, Vec<u8>)>,
7376    }
7377
7378    impl OutputWriter for GrayRows {
7379        fn write_rgb_row(
7380            &mut self,
7381            _y: u32,
7382            _r_row: &[u8],
7383            _g_row: &[u8],
7384            _b_row: &[u8],
7385        ) -> Result<(), JpegError> {
7386            unreachable!("gray test writer should not receive rgb rows");
7387        }
7388
7389        fn write_ycbcr_row(
7390            &mut self,
7391            _y: u32,
7392            _y_row: &[u8],
7393            _cb_row: &[u8],
7394            _cr_row: &[u8],
7395        ) -> Result<(), JpegError> {
7396            unreachable!("gray test writer should not receive ycbcr rows");
7397        }
7398
7399        fn write_gray_row(&mut self, y: u32, gray_row: &[u8]) -> Result<(), JpegError> {
7400            self.rows.push((y, gray_row.to_vec()));
7401            Ok(())
7402        }
7403    }
7404
7405    #[test]
7406    fn cropped_writer_honors_source_window_origin() {
7407        let inner = GrayRows::default();
7408        let rect = Rect {
7409            x: 6,
7410            y: 1,
7411            w: 2,
7412            h: 1,
7413        };
7414        let mut writer = CroppedWriter::new(inner, rect, 4, 4);
7415
7416        writer
7417            .write_gray_row(1, &[10, 20, 30, 40])
7418            .expect("crop write must succeed");
7419
7420        assert_eq!(writer.inner.rows, vec![(0, vec![30, 40])]);
7421    }
7422}