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