Skip to main content

j2k_jpeg/
decoder.rs

1// SPDX-License-Identifier: MIT OR 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, PreparedHuffmanTableId, PreparedHuffmanTables};
9use crate::entropy::progressive::{
10    decode_progressive, decode_progressive_dct_blocks, PreparedProgressiveComponentPlan,
11    PreparedProgressivePlan, PreparedProgressiveScan, PreparedProgressiveScanComponent,
12    COMPONENT_IMAGE_METADATA_BYTES,
13};
14use crate::entropy::sequential::{
15    decode_scan_baseline, decode_scan_baseline_rgb, decode_scan_fast_rgb_444,
16    decode_scan_fast_tile_rgb, decode_scan_fast_tile_rgb_region,
17    decode_scan_fast_tile_rgb_region_scaled, fast_tile_region_first_decode_mcu, finish_scan,
18    stripe_region_layout, FastTileRegionScaledRequest, PreparedComponentPlan, PreparedDecodePlan,
19    ResolvedPreparedComponentPlan,
20};
21use crate::entropy::ZIGZAG;
22use crate::error::{JpegError, MarkerKind, Warning};
23use crate::info::{
24    ColorSpace, DecodeOptions, DownscaleFactor, Info, OutputFormat, Rect, RestartIndex,
25    RestartSegment, SofKind,
26};
27use crate::internal::bit_reader::BitReader;
28use crate::internal::checkpoint::{checkpoint_before_mcu, CpuCheckpointCache, DeviceCheckpoint};
29use crate::internal::scratch::ScratchPool;
30use crate::lossless::{lossless_predict, LosslessSample};
31use crate::output::{
32    validate_buffer, Gray8Writer, InterleavedRgbWriter, OutputWriter, Rgb8Writer, Rgba8Writer,
33};
34use crate::parse::header::{parse_info, ParsedHeader};
35use crate::profile::{emit_jpeg_profile_fields, jpeg_profile_stages_enabled, ProfileField};
36use crate::segment::PreparedJpeg;
37use crate::JpegCodec;
38use alloc::vec::Vec;
39use core::cell::RefCell;
40pub use j2k_core::TileBatchOptions;
41use j2k_core::{
42    CompressedTransferSyntax, DecodeOutcome as CoreDecodeOutcome, DecodeRowsError,
43    DecoderContext as CoreDecoderContext, Downscale, ImageCodec, ImageDecode, ImageDecodeRows,
44    PixelFormat, RowSink, TileBatchDecode,
45};
46use std::sync::Mutex;
47use std::time::{Duration, Instant};
48
49pub(crate) const DEFAULT_MAX_DECODE_BYTES: usize = 512 * 1024 * 1024;
50pub(super) const MAX_DECODE_SCAN_WARNINGS: usize = 1;
51const CPU_ROI_CHECKPOINT_CADENCE_MCUS: u32 = 1024;
52const CPU_ROI_CHECKPOINT_MIN_TARGET_MCUS: u32 = 4096;
53
54std::thread_local! {
55    static DEFAULT_SCRATCH: RefCell<ScratchPool> = RefCell::new(ScratchPool::new());
56    static DEFAULT_CONTEXT: RefCell<DecoderContext> = RefCell::new(DecoderContext::new());
57}
58
59mod view;
60pub use self::view::JpegView;
61mod allocation;
62mod output_format;
63use self::output_format::{
64    allocate_output_buffer_with_live_budget, checked_output_geometry, downscale_profile_name,
65    jpeg_downscale, output_format_from_parts, output_format_profile_name, scaled_dimensions,
66    scaled_rect_covering,
67};
68mod extended12;
69use self::extended12::{lossless_color_sampling, upsample_h2v1_sample_at, upsample_h2v2_rows_at};
70mod lossless_helpers;
71pub(crate) use self::lossless_helpers::restart_index_allocation_bytes;
72use self::lossless_helpers::{
73    decode_lossless_color_sample, decode_lossless_sampled_color_mcu, emit_decode_scan_profile,
74    lossless_predictor_gray_rows, lossless_predictor_value, lossless_predictor_value_u16,
75    restart_index_for_stream, validate_lossless_color_plan, write_lossless_color16_sampled_output,
76    write_lossless_color8_sampled_output, LosslessColorIntoSample, LosslessColorPlanes,
77    LosslessColorRowSample, LosslessRestartTracker, LosslessSampledColorPlanesMut,
78    LosslessSampledMcu,
79};
80mod color_convert;
81use self::color_convert::{
82    convert_ycbcr16_to_rgb16_in_place, convert_ycbcr8_to_rgb8_in_place, copy_gray16_scaled_rect,
83    copy_gray8_scaled_rect, copy_rgb16_to_rgba16, copy_ycbcr16_row_to_rgb16,
84    copy_ycbcr8_row_to_rgb8,
85};
86mod warning_ownership;
87use self::warning_ownership::{merged_warnings, try_clone_warnings};
88mod core_traits;
89use self::core_traits::{CroppedWriter, ProgressiveDownscaleWriter};
90mod lossless_region;
91use self::lossless_region::{LosslessRegionRequest, LosslessRgbRegionFallback, LosslessRgbaAlpha};
92mod scratch;
93use self::scratch::{
94    additional_decode_scratch_bytes, checked_scratch_len, checked_usize_product,
95    compute_decode_scratch_bytes, compute_extended12_planes_scratch_bytes,
96    compute_lossless_scratch_bytes, lossless_sampled_plane_layout, LosslessSampledPlaneLayout,
97};
98mod sink_writer;
99pub(crate) use self::sink_writer::SinkWriter;
100mod plan;
101use self::plan::find_component_index;
102mod routing;
103mod rows;
104mod sequential;
105mod tile;
106pub(crate) use self::tile::{
107    decode_prepared_jpeg_tile_rgb8_in_context, planned_jpeg_tile_decode_live_bytes,
108    PlannedJpegTileDecode,
109};
110pub use self::tile::{
111    decode_prepared_jpeg_tiles_rgb8, decode_tile_into, decode_tile_into_in_context,
112    decode_tile_into_in_context_with_options, decode_tile_region_into_in_context,
113    decode_tile_region_into_in_context_with_options, decode_tile_region_scaled_into_in_context,
114    decode_tile_region_scaled_into_in_context_with_options, decode_tile_scaled_into_in_context,
115    decode_tile_scaled_into_in_context_with_options, decode_tiles_into,
116    decode_tiles_into_with_options, decode_tiles_region_scaled_into,
117    decode_tiles_region_scaled_into_with_options, decode_tiles_scaled_into,
118    decode_tiles_scaled_into_with_options,
119};
120mod lossless_render;
121
122/// Non-fatal outcome of a successful decode. See spec Section 2.
123///
124/// `DecodeOutcome` lives on `decoder.rs` rather than `info.rs` because it
125/// carries `Warning` values from `error.rs`, and moving it into `info` would
126/// create a `info → error` cycle (see `info.rs` header note).
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct DecodeOutcome {
129    /// The source-coordinate rectangle represented by the output buffer.
130    /// Full-image decodes return `Rect::full(info.dimensions)` even when the
131    /// requested output is downscaled; region decodes return their source ROI.
132    pub decoded: Rect,
133    /// Warnings emitted during parse or decode. Empty when the stream is
134    /// syntactically clean and every capability was exercised without fallback.
135    pub warnings: Vec<Warning>,
136}
137
138impl From<DecodeOutcome> for CoreDecodeOutcome<Warning> {
139    fn from(outcome: DecodeOutcome) -> Self {
140        Self {
141            decoded: outcome.decoded.into(),
142            warnings: outcome.warnings,
143        }
144    }
145}
146
147/// Owned-output JPEG decode request.
148///
149/// This consolidates the full-image, region, and downscale axes for callers
150/// that want a freshly allocated tightly packed output buffer.
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub struct DecodeRequest {
153    /// Requested output pixel format.
154    pub fmt: PixelFormat,
155    /// Optional source-image region to decode.
156    pub region: Option<Rect>,
157    /// Requested decoder downscale.
158    pub scale: Downscale,
159}
160
161impl DecodeRequest {
162    /// Full-image decode at native scale.
163    #[must_use]
164    pub const fn full(fmt: PixelFormat) -> Self {
165        Self {
166            fmt,
167            region: None,
168            scale: Downscale::None,
169        }
170    }
171
172    /// Full-image decode with downscale.
173    #[must_use]
174    pub const fn scaled(fmt: PixelFormat, scale: Downscale) -> Self {
175        Self {
176            fmt,
177            region: None,
178            scale,
179        }
180    }
181
182    /// Region decode at native scale.
183    #[must_use]
184    pub const fn region(fmt: PixelFormat, region: Rect) -> Self {
185        Self {
186            fmt,
187            region: Some(region),
188            scale: Downscale::None,
189        }
190    }
191
192    /// Region decode with downscale.
193    #[must_use]
194    pub const fn region_scaled(fmt: PixelFormat, region: Rect, scale: Downscale) -> Self {
195        Self {
196            fmt,
197            region: Some(region),
198            scale,
199        }
200    }
201}
202
203/// One tile decode request for [`decode_tiles_into`].
204pub type TileDecodeJob<'i, 'o> = j2k_core::TileDecodeJob<'i, 'o>;
205
206/// Caller-owned output target for one context-reused tile decode helper.
207pub struct TileDecodeOutput<'o> {
208    /// Caller-owned output buffer.
209    pub out: &'o mut [u8],
210    /// Distance in bytes between output rows.
211    pub stride: usize,
212    /// Requested output pixel format.
213    pub fmt: PixelFormat,
214}
215
216/// One decode request for a JPEG tile already normalized by
217/// [`prepare_tiff_jpeg_tile`](crate::prepare_tiff_jpeg_tile).
218pub struct PreparedJpegTileJob<'i, 'o> {
219    /// Decode-ready prepared JPEG bytes.
220    pub input: PreparedJpeg<'i>,
221    /// Caller-owned RGB8 output buffer for this tile.
222    pub out: &'o mut [u8],
223    /// Distance in bytes between output rows.
224    pub stride: usize,
225    /// Per-job JPEG decode options.
226    pub options: DecodeOptions,
227}
228
229/// Result for one successful prepared JPEG tile decode.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct DecodedTile {
232    /// Tile dimensions reported by the prepared JPEG header.
233    pub dimensions: (u32, u32),
234    /// Rectangle written into the output buffer.
235    pub decoded: Rect,
236    /// Non-fatal warnings emitted during parse or decode.
237    pub warnings: Vec<Warning>,
238}
239
240/// One scaled tile decode request for [`decode_tiles_scaled_into`].
241pub type TileScaledDecodeJob<'i, 'o> = j2k_core::TileScaledDecodeJob<'i, 'o>;
242
243/// One ROI+scaled tile decode request for
244/// [`decode_tiles_region_scaled_into`].
245pub type TileRegionScaledDecodeJob<'i, 'o> = j2k_core::TileRegionScaledDecodeJob<'i, 'o>;
246
247/// Error returned by [`decode_tiles_into`], annotated with the failing tile
248/// index from the caller's input order when a codec error occurs, or carrying
249/// a typed infrastructure error when no tile index applies.
250pub type TileBatchError = j2k_core::BatchDecodeError<JpegError>;
251
252/// Allocation, scheduling, or collection failure for a prepared JPEG batch.
253///
254/// Prepared batches retain codec failures independently in their returned
255/// per-tile result vector, so only infrastructure failures use this outer type.
256pub type PreparedTileBatchError = j2k_core::BatchInfrastructureError;
257
258/// Receives decoded component rows before they are packed into the final
259/// interleaved pixel format.
260pub trait ComponentRowWriter {
261    /// Receive one grayscale row.
262    ///
263    /// # Errors
264    ///
265    /// Returns an error when the destination cannot accept the row.
266    fn write_gray_row(&mut self, y: u32, gray_row: &[u8]) -> Result<(), JpegError>;
267
268    /// Receive one full-width Y/Cb/Cr row.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error when the destination cannot accept the component row.
273    fn write_ycbcr_row(
274        &mut self,
275        y: u32,
276        y_row: &[u8],
277        cb_row: &[u8],
278        cr_row: &[u8],
279    ) -> Result<(), JpegError>;
280
281    /// Receive one full-width planar RGB row.
282    ///
283    /// # Errors
284    ///
285    /// Returns an error when the destination cannot accept the component row.
286    fn write_rgb_row(
287        &mut self,
288        y: u32,
289        r_row: &[u8],
290        g_row: &[u8],
291        b_row: &[u8],
292    ) -> Result<(), JpegError>;
293}
294
295/// A borrowed view of a JPEG stream ready to decode. Constructed via
296/// [`Decoder::new`] or [`Decoder::from_view`]. `Decoder<'a>: Send + Sync`.
297#[derive(Debug)]
298pub struct Decoder<'a> {
299    pub(crate) bytes: &'a [u8],
300    pub(crate) info: Info,
301    pub(crate) warnings: Vec<Warning>,
302    pub(crate) backend: Backend,
303    pub(crate) plan: PreparedDecodePlan,
304    pub(crate) progressive_plan: Option<PreparedProgressivePlan>,
305    lossless_plan: Option<PreparedLosslessPlan>,
306    pub(crate) cpu_entropy_checkpoints: Mutex<CpuCheckpointCache>,
307}
308
309struct PreparedDecoderMetadata {
310    info: Info,
311    warnings: Vec<Warning>,
312    plan: PreparedDecodePlan,
313    progressive_plan: Option<PreparedProgressivePlan>,
314    lossless_plan: Option<PreparedLosslessPlan>,
315}
316
317#[derive(Debug, Clone)]
318struct PreparedLosslessPlan {
319    predictor: u8,
320    bit_depth: u8,
321    dc_table: PreparedHuffmanTableId,
322    dimensions: (u32, u32),
323    scan_offset: usize,
324}
325
326#[derive(Clone, Copy, Debug, Eq, PartialEq)]
327enum LosslessColorSampling {
328    S444,
329    S422,
330    S420,
331}
332
333impl<'a> Decoder<'a> {
334    /// Parse the headers without decoding pixels. The parser walks headers up
335    /// to the first SOS and then performs a lightweight marker scan so
336    /// `Info::scan_count` reflects all scans in the file.
337    ///
338    /// # Errors
339    /// Returns any structural, unsupported-SOF, or sanity-check error
340    /// encountered before the Start-of-Scan marker. See [`JpegError`].
341    pub fn inspect(input: &'a [u8]) -> Result<Info, JpegError> {
342        let info = parse_info(input)?;
343        Ok(info)
344    }
345
346    fn from_bytes_with_options(input: &'a [u8], options: DecodeOptions) -> Result<Self, JpegError> {
347        let view = JpegView::parse_with_options(input, options)?;
348        DEFAULT_CONTEXT.with(|ctx| Self::from_view_in_context(view, &mut ctx.borrow_mut()))
349    }
350
351    /// Build a decoder ready for `decode_into`. Parses the full header, pre-
352    /// builds every referenced Huffman table, and validates that the stream is
353    /// one of the SOFs this release implements.
354    ///
355    /// # Errors
356    /// - Any parse error encountered before SOS (see [`Self::inspect`]).
357    /// - [`JpegError::NotImplemented`] for SOFs that parse but are not yet
358    ///   decodable for the requested shape (for example Progressive12 or
359    ///   unsupported Lossless predictors).
360    /// - [`JpegError::MissingHuffmanTable`] if the scan references a DC/AC
361    ///   table slot that was never defined by a DHT segment.
362    pub fn new(input: &'a [u8]) -> Result<Self, JpegError> {
363        Self::from_bytes_with_options(input, DecodeOptions::default())
364    }
365
366    /// Build a decoder from a previously parsed [`JpegView`].
367    ///
368    /// # Errors
369    ///
370    /// Returns an error when the view describes an unsupported JPEG shape or
371    /// references missing or invalid coding tables.
372    pub fn from_view(view: JpegView<'a>) -> Result<Self, JpegError> {
373        DEFAULT_CONTEXT.with(|ctx| Self::from_view_in_context(view, &mut ctx.borrow_mut()))
374    }
375
376    /// Build from a parsed view while charging an already-live owner baseline.
377    ///
378    /// # Errors
379    ///
380    /// Returns an error when the aggregate host budget is exceeded or the view
381    /// describes unsupported or invalid coding state.
382    pub(crate) fn from_view_with_external_live(
383        view: JpegView<'a>,
384        external_live_bytes: usize,
385    ) -> Result<Self, JpegError> {
386        DEFAULT_CONTEXT.with(|ctx| {
387            Self::from_view_in_context_with_external_live(
388                view,
389                &mut ctx.borrow_mut(),
390                external_live_bytes,
391            )
392        })
393    }
394
395    /// Build a decoder from a previously parsed [`JpegView`], reusing shared
396    /// compiled DHT/DQT state from `ctx` when table contents repeat.
397    ///
398    /// # Errors
399    ///
400    /// Returns an error when the view describes an unsupported JPEG shape or
401    /// references missing or invalid coding tables.
402    pub fn from_view_in_context(
403        view: JpegView<'a>,
404        ctx: &mut DecoderContext,
405    ) -> Result<Self, JpegError> {
406        Self::from_view_in_context_with_external_live(view, ctx, 0)
407    }
408
409    fn from_view_in_context_with_external_live(
410        view: JpegView<'a>,
411        ctx: &mut DecoderContext,
412        external_live_bytes: usize,
413    ) -> Result<Self, JpegError> {
414        let JpegView {
415            bytes,
416            header,
417            info,
418            options,
419        } = view;
420        let backend = Backend::detect();
421        let PreparedDecoderMetadata {
422            info,
423            warnings,
424            plan,
425            progressive_plan,
426            lossless_plan,
427        } = Self::prepare_header_with_external_live(
428            header,
429            info,
430            options,
431            bytes,
432            ctx,
433            external_live_bytes,
434        )?;
435        Ok(Self {
436            bytes,
437            info,
438            warnings,
439            backend,
440            plan,
441            progressive_plan,
442            lossless_plan,
443            cpu_entropy_checkpoints: Mutex::new(CpuCheckpointCache::default()),
444        })
445    }
446
447    /// The parsed header as a public [`Info`].
448    pub fn info(&self) -> &Info {
449        &self.info
450    }
451
452    /// Build a restart-marker byte-offset index for the first scan.
453    ///
454    /// Offsets are absolute byte positions in the original JPEG byte slice.
455    /// Returns `Ok(None)` when the stream has no non-zero DRI marker.
456    pub(crate) fn restart_index(&self) -> Result<Option<RestartIndex>, JpegError> {
457        restart_index_for_stream(
458            self.bytes,
459            Some(self.plan.scan_offset),
460            &self.info,
461            self.plan.restart_interval,
462        )
463    }
464}
465
466#[cfg(test)]
467mod tests;