Skip to main content

j2k_jpeg/decoder/
routing.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Public output-buffer routing for full, scaled, and region decodes.
4
5use super::{
6    additional_decode_scratch_bytes, jpeg_profile_stages_enabled, output_format_from_parts,
7    scaled_dimensions, scaled_rect_covering, validate_buffer, DecodeOutcome, Decoder, Downscale,
8    DownscaleFactor, Instant, JpegError, LosslessRegionRequest, LosslessRgbRegionFallback,
9    LosslessRgbaAlpha, OutputFormat, PixelFormat, Rect, ScratchPool, DEFAULT_SCRATCH,
10};
11
12mod dispatch;
13mod owned_output;
14mod profile;
15
16#[cfg(test)]
17mod tests;
18
19use self::dispatch::OutputRoute;
20use self::profile::DecodeProfileRecord;
21
22impl Decoder<'_> {
23    /// Decode the full image into the caller's buffer.
24    ///
25    /// # Errors
26    /// - [`JpegError::OutputBufferTooSmall`] or [`JpegError::InvalidStride`]
27    ///   if the provided buffer/stride cannot hold the image at `fmt`.
28    /// - [`JpegError::NotImplemented`] if `fmt` requests a raw output the
29    ///   current release does not emit (e.g. `RawYCbCr8`).
30    /// - Any entropy- or structural-decode error from the scan walker.
31    pub fn decode_into(
32        &self,
33        out: &mut [u8],
34        stride: usize,
35        fmt: PixelFormat,
36    ) -> Result<DecodeOutcome, JpegError> {
37        DEFAULT_SCRATCH
38            .with(|pool| self.decode_into_with_scratch(&mut pool.borrow_mut(), out, stride, fmt))
39    }
40
41    /// Decode the full image into the caller's buffer using the core
42    /// `PixelFormat` + `Downscale` contract.
43    ///
44    /// # Errors
45    ///
46    /// Returns an output-buffer, unsupported-format, or scan decode error.
47    pub fn decode_scaled_into(
48        &self,
49        out: &mut [u8],
50        stride: usize,
51        fmt: PixelFormat,
52        scale: Downscale,
53    ) -> Result<DecodeOutcome, JpegError> {
54        DEFAULT_SCRATCH.with(|pool| {
55            self.decode_scaled_into_with_scratch(&mut pool.borrow_mut(), out, stride, fmt, scale)
56        })
57    }
58
59    /// Decode the full image into the caller's buffer, reusing the supplied
60    /// [`ScratchPool`]. On a long-running tile batch this eliminates the
61    /// per-tile allocation of stripe buffers, the DC predictor, and the
62    /// chroma upsample rows — the realistic WSI reader shape. The first
63    /// call against a fresh pool does the allocation; subsequent calls at
64    /// the same-or-smaller shape reuse the underlying `Vec`s.
65    ///
66    /// # Errors
67    /// Identical to [`Self::decode_into`].
68    pub fn decode_into_with_scratch(
69        &self,
70        pool: &mut ScratchPool,
71        out: &mut [u8],
72        stride: usize,
73        fmt: PixelFormat,
74    ) -> Result<DecodeOutcome, JpegError> {
75        self.decode_scaled_into_with_scratch(pool, out, stride, fmt, Downscale::None)
76    }
77
78    pub(super) fn decode_lossless_output_format_region_scaled(
79        &self,
80        out: &mut [u8],
81        stride: usize,
82        fmt: OutputFormat,
83        roi: Rect,
84        downscale: DownscaleFactor,
85        external_live_bytes: usize,
86    ) -> Option<Result<DecodeOutcome, JpegError>> {
87        self.lossless_plan.as_ref()?;
88        let result = match fmt {
89            OutputFormat::Rgb8 | OutputFormat::Rgb8Scaled { .. } => {
90                LosslessRgbRegionFallback::for_color_space_8(self.info.color_space)
91                    .decode_rgb_region_scaled_into(
92                        self,
93                        LosslessRegionRequest {
94                            out,
95                            stride,
96                            roi,
97                            downscale,
98                            external_live_bytes,
99                        },
100                    )
101            }
102            OutputFormat::Rgba8 { alpha } | OutputFormat::Rgba8Scaled { alpha, .. } => {
103                LosslessRgbRegionFallback::for_color_space_8(self.info.color_space)
104                    .decode_rgba_region_scaled_into(
105                        self,
106                        LosslessRegionRequest {
107                            out,
108                            stride,
109                            roi,
110                            downscale,
111                            external_live_bytes,
112                        },
113                        LosslessRgbaAlpha::U8(alpha),
114                    )
115            }
116            OutputFormat::Gray8 | OutputFormat::Gray8Scaled { .. } => self
117                .decode_lossless_gray8_region_scaled_into(
118                    out,
119                    stride,
120                    roi,
121                    downscale,
122                    external_live_bytes,
123                ),
124            OutputFormat::Gray16 | OutputFormat::Gray16Scaled { .. } => self
125                .decode_lossless_gray16_region_scaled_into(
126                    out,
127                    stride,
128                    roi,
129                    downscale,
130                    external_live_bytes,
131                ),
132            OutputFormat::Rgb16 | OutputFormat::Rgb16Scaled { .. } => {
133                LosslessRgbRegionFallback::for_color_space_16(self.info.color_space)
134                    .decode_rgb_region_scaled_into(
135                        self,
136                        LosslessRegionRequest {
137                            out,
138                            stride,
139                            roi,
140                            downscale,
141                            external_live_bytes,
142                        },
143                    )
144            }
145            OutputFormat::Rgba16 { alpha } | OutputFormat::Rgba16Scaled { alpha, .. } => {
146                LosslessRgbRegionFallback::for_color_space_16(self.info.color_space)
147                    .decode_rgba_region_scaled_into(
148                        self,
149                        LosslessRegionRequest {
150                            out,
151                            stride,
152                            roi,
153                            downscale,
154                            external_live_bytes,
155                        },
156                        LosslessRgbaAlpha::U16(alpha),
157                    )
158            }
159        };
160        Some(result)
161    }
162
163    pub(super) fn decode_into_output_format_with_scratch(
164        &self,
165        pool: &mut ScratchPool,
166        out: &mut [u8],
167        stride: usize,
168        fmt: OutputFormat,
169    ) -> Result<DecodeOutcome, JpegError> {
170        self.decode_into_output_format_with_scratch_and_external(pool, out, stride, fmt, 0)
171    }
172
173    fn decode_into_output_format_with_scratch_and_external(
174        &self,
175        pool: &mut ScratchPool,
176        out: &mut [u8],
177        stride: usize,
178        fmt: OutputFormat,
179        external_live_bytes: usize,
180    ) -> Result<DecodeOutcome, JpegError> {
181        let profile_enabled = jpeg_profile_stages_enabled();
182        let total_start = profile_enabled.then(Instant::now);
183        let downscale = fmt.downscale();
184        let (w, h) = scaled_dimensions(self.info.dimensions, downscale);
185        let full_roi = Rect::full(self.info.dimensions);
186        let additional_scratch = additional_decode_scratch_bytes(
187            self.info.sof_kind,
188            self.info.dimensions,
189            fmt,
190            full_roi,
191            Rect::full((w, h)),
192            downscale,
193        )?;
194        let scratch_bytes = self.prepare_decode_workspace_with_additional(
195            pool,
196            external_live_bytes,
197            additional_scratch,
198        )?;
199        let bpp = fmt.bytes_per_pixel();
200        validate_buffer(out, stride, w, h, bpp)?;
201        if let Some(result) = self.decode_lossless_output_format_region_scaled(
202            out,
203            stride,
204            fmt,
205            full_roi,
206            downscale,
207            external_live_bytes,
208        ) {
209            return result;
210        }
211        let decode_start = profile_enabled.then(Instant::now);
212        let output_rect = Rect::full((w, h));
213        let result = self.dispatch_full_output(OutputRoute {
214            pool,
215            out,
216            stride,
217            fmt,
218            source_roi: full_roi,
219            output_rect,
220            downscale,
221            external_live_bytes,
222        });
223        if let (Some(total_start), Some(decode_start), Ok(outcome)) =
224            (total_start, decode_start, &result)
225        {
226            DecodeProfileRecord {
227                total_start,
228                decode_start,
229                source_dimensions: self.info.dimensions,
230                output_rect,
231                stride,
232                bytes_per_pixel: bpp,
233                scratch_bytes,
234                fmt,
235                downscale,
236                source_roi: None,
237            }
238            .emit(outcome);
239        }
240        result
241    }
242
243    /// [`Self::decode_scaled_into`] with caller-owned scratch.
244    ///
245    /// # Errors
246    ///
247    /// Returns an output-buffer, unsupported-format, or scan decode error.
248    pub fn decode_scaled_into_with_scratch(
249        &self,
250        pool: &mut ScratchPool,
251        out: &mut [u8],
252        stride: usize,
253        fmt: PixelFormat,
254        scale: Downscale,
255    ) -> Result<DecodeOutcome, JpegError> {
256        self.decode_into_output_format_with_scratch(
257            pool,
258            out,
259            stride,
260            output_format_from_parts(self.info.sof_kind, fmt, scale)?,
261        )
262    }
263    /// Decode a rectangular region of the image into the caller's buffer.
264    ///
265    /// `roi` is expressed in source-image coordinates. If `fmt` requests a
266    /// downscaled output, the written pixels cover the corresponding bounding
267    /// box in the scaled image grid.
268    ///
269    /// # Errors
270    ///
271    /// Returns an invalid-region, output-buffer, unsupported-format, or scan
272    /// decode error.
273    pub fn decode_region_into(
274        &self,
275        out: &mut [u8],
276        stride: usize,
277        fmt: PixelFormat,
278        roi: Rect,
279    ) -> Result<DecodeOutcome, JpegError> {
280        DEFAULT_SCRATCH.with(|pool| {
281            self.decode_region_into_with_scratch(&mut pool.borrow_mut(), out, stride, fmt, roi)
282        })
283    }
284
285    /// [`Self::decode_region_into`] with caller-owned scratch.
286    pub(crate) fn decode_region_into_with_scratch(
287        &self,
288        pool: &mut ScratchPool,
289        out: &mut [u8],
290        stride: usize,
291        fmt: PixelFormat,
292        roi: Rect,
293    ) -> Result<DecodeOutcome, JpegError> {
294        self.decode_region_scaled_into_with_scratch(pool, out, stride, fmt, roi, Downscale::None)
295    }
296
297    pub(super) fn decode_region_into_output_format_with_scratch(
298        &self,
299        pool: &mut ScratchPool,
300        out: &mut [u8],
301        stride: usize,
302        fmt: OutputFormat,
303        roi: Rect,
304    ) -> Result<DecodeOutcome, JpegError> {
305        self.decode_region_into_output_format_with_scratch_and_external(
306            pool, out, stride, fmt, roi, 0,
307        )
308    }
309
310    fn decode_region_into_output_format_with_scratch_and_external(
311        &self,
312        pool: &mut ScratchPool,
313        out: &mut [u8],
314        stride: usize,
315        fmt: OutputFormat,
316        roi: Rect,
317        external_live_bytes: usize,
318    ) -> Result<DecodeOutcome, JpegError> {
319        let profile_enabled = jpeg_profile_stages_enabled();
320        let total_start = profile_enabled.then(Instant::now);
321        if !roi.is_within(self.info.dimensions) {
322            return Err(JpegError::RectOutOfBounds {
323                rect: roi,
324                width: self.info.dimensions.0,
325                height: self.info.dimensions.1,
326            });
327        }
328
329        if roi == Rect::full(self.info.dimensions) {
330            return self.decode_into_output_format_with_scratch_and_external(
331                pool,
332                out,
333                stride,
334                fmt,
335                external_live_bytes,
336            );
337        }
338
339        let downscale = fmt.downscale();
340        let scaled_roi = scaled_rect_covering(roi, downscale)?;
341        let additional_scratch = additional_decode_scratch_bytes(
342            self.info.sof_kind,
343            self.info.dimensions,
344            fmt,
345            roi,
346            scaled_roi,
347            downscale,
348        )?;
349        let scratch_bytes = self.prepare_decode_workspace_with_additional(
350            pool,
351            external_live_bytes,
352            additional_scratch,
353        )?;
354        validate_buffer(
355            out,
356            stride,
357            scaled_roi.w,
358            scaled_roi.h,
359            fmt.bytes_per_pixel(),
360        )?;
361        if let Some(result) = self.decode_lossless_output_format_region_scaled(
362            out,
363            stride,
364            fmt,
365            roi,
366            downscale,
367            external_live_bytes,
368        ) {
369            return result;
370        }
371
372        let decode_start = profile_enabled.then(Instant::now);
373        let result = self.dispatch_region_output(OutputRoute {
374            pool,
375            out,
376            stride,
377            fmt,
378            source_roi: roi,
379            output_rect: scaled_roi,
380            downscale,
381            external_live_bytes,
382        });
383        if let (Some(total_start), Some(decode_start), Ok(outcome)) =
384            (total_start, decode_start, &result)
385        {
386            DecodeProfileRecord {
387                total_start,
388                decode_start,
389                source_dimensions: self.info.dimensions,
390                output_rect: scaled_roi,
391                stride,
392                bytes_per_pixel: fmt.bytes_per_pixel(),
393                scratch_bytes,
394                fmt,
395                downscale,
396                source_roi: Some(roi),
397            }
398            .emit(outcome);
399        }
400        result
401    }
402
403    /// Decode `roi` into the caller's buffer using the core `PixelFormat` +
404    /// `Downscale` contract.
405    ///
406    /// # Errors
407    ///
408    /// Returns an invalid-region, output-buffer, unsupported-format, or scan
409    /// decode error.
410    pub fn decode_region_scaled_into(
411        &self,
412        out: &mut [u8],
413        stride: usize,
414        fmt: PixelFormat,
415        roi: Rect,
416        scale: Downscale,
417    ) -> Result<DecodeOutcome, JpegError> {
418        DEFAULT_SCRATCH.with(|pool| {
419            self.decode_region_scaled_into_with_scratch(
420                &mut pool.borrow_mut(),
421                out,
422                stride,
423                fmt,
424                roi,
425                scale,
426            )
427        })
428    }
429
430    /// [`Self::decode_region_scaled_into`] with caller-owned scratch.
431    ///
432    /// # Errors
433    ///
434    /// Returns an invalid-region, output-buffer, unsupported-format, or scan
435    /// decode error.
436    pub fn decode_region_scaled_into_with_scratch(
437        &self,
438        pool: &mut ScratchPool,
439        out: &mut [u8],
440        stride: usize,
441        fmt: PixelFormat,
442        roi: Rect,
443        scale: Downscale,
444    ) -> Result<DecodeOutcome, JpegError> {
445        self.decode_region_into_output_format_with_scratch(
446            pool,
447            out,
448            stride,
449            output_format_from_parts(self.info.sof_kind, fmt, scale)?,
450            roi,
451        )
452    }
453}