Skip to main content

hpvcd/
lib.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 6/2026. All rights reserved.
3 * //
4 * // Redistribution and use in source and binary forms, with or without modification,
5 * // are permitted provided that the following conditions are met:
6 * //
7 * // 1.  Redistributions of source code must retain the above copyright notice, this
8 * // list of conditions and the following disclaimer.
9 * //
10 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * // this list of conditions and the following disclaimer in the documentation
12 * // and/or other materials provided with the distribution.
13 * //
14 * // 3.  Neither the name of the copyright holder nor the names of its
15 * // contributors may be used to endorse or promote products derived from
16 * // this software without specific prior written permission.
17 * //
18 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30#![allow(unreachable_pub)]
31
32/// Allocate and fill a vector without aborting the process on allocation
33/// failure. Use this only for image-sized or otherwise potentially large
34/// buffers; small fixed/scratch allocations should keep ordinary `vec!`.
35macro_rules! try_vec {
36    ($value:expr; $len:expr, $what:expr) => {{
37        let len = $len;
38        let value = $value;
39        let element_size = core::mem::size_of_val(&value);
40        let mut out = Vec::new();
41        out.try_reserve_exact(len)
42            .map_err(|_| $crate::error::DecodeError::AllocationFailed {
43                what: $what,
44                bytes: len.saturating_mul(element_size),
45            })?;
46        out.resize(len, value);
47        out
48    }};
49}
50mod act;
51#[cfg(all(feature = "avx", target_arch = "x86_64"))]
52mod avx;
53mod bitreader;
54mod cabac;
55mod color;
56mod config;
57mod deblock;
58mod decode;
59mod decoder;
60mod demux;
61mod dpb;
62mod error;
63mod exec;
64mod fast_divide;
65mod fmt;
66mod heif;
67mod ibc;
68mod info;
69mod inter;
70mod intra;
71mod limits;
72mod mc;
73mod metadata;
74mod motion;
75#[cfg(all(feature = "neon", target_arch = "aarch64"))]
76mod neon;
77mod palette;
78mod plane;
79mod reconstruct;
80mod rps;
81mod sao;
82mod settings;
83#[cfg(all(feature = "sse", any(target_arch = "x86", target_arch = "x86_64")))]
84mod sse;
85mod threadpool;
86mod tiles;
87mod transform;
88mod video;
89mod wpp;
90mod yuv;
91
92pub use color::{Cicp, ColorMetadata, MatrixCoefficients, Primaries, TransferFunction};
93pub use decoder::Decoder;
94pub use error::DecodeError;
95pub use fmt::{
96    BitDepth, ChromaFormat, ImageBuffer, PlanarImage, PlaneBuffer, PlaneLayout, SampleBuf,
97    SamplePlane, YuvBuffer,
98};
99pub use info::{ImageInfo, read_heic_info, read_heic_info_with_limits};
100pub use limits::ParseLimits;
101pub use metadata::{CleanAperture, ContentLightLevel, Metadata, Orientation, PixelAspectRatio};
102pub use settings::{DecodeThreads, HeicSettings};
103pub use video::{FrameYuv, VideoDecoder, VideoFrame, decode_hevc, decode_hevc_frame_at};
104
105#[derive(Clone, Copy, Default)]
106struct HevcVisibleCrop {
107    left: usize,
108    top: usize,
109}
110
111struct DecodedGainMapImage {
112    width: u32,
113    height: u32,
114    chroma_width: u32,
115    chroma_height: u32,
116    y: SampleBuf,
117    cb: Option<SampleBuf>,
118    cr: Option<SampleBuf>,
119    chroma: ChromaFormat,
120    bit_depth: BitDepth,
121    color: ColorMetadata,
122    orientation: Orientation,
123}
124
125struct OrientedImage {
126    width: u32,
127    height: u32,
128    pixels: ImageBuffer,
129    alpha: Option<SampleBuf>,
130}
131
132#[inline]
133fn visible_crop_from_hvcc(hvcc: &[u8]) -> HevcVisibleCrop {
134    config::parse_hvcc_full(hvcc)
135        .ok()
136        .map(|(sps, _)| HevcVisibleCrop {
137            left: sps.crop_left as usize,
138            top: sps.crop_top as usize,
139        })
140        .unwrap_or_default()
141}
142
143fn plane_window<T: Copy + Default>(
144    data: Vec<T>,
145    src_w: usize,
146    src_h: usize,
147    crop_left: usize,
148    crop_top: usize,
149    dst_w: usize,
150    dst_h: usize,
151) -> Result<PlaneBuffer<T>, DecodeError> {
152    if dst_w == 0 || dst_h == 0 {
153        return Ok(PlaneBuffer::tight(Vec::new(), dst_w, dst_h));
154    }
155
156    #[allow(clippy::manual_checked_ops)]
157    let rows = if src_w == 0 {
158        0
159    } else {
160        (data.len() / src_w).min(src_h)
161    };
162    let direct = rows != 0
163        && crop_left.checked_add(dst_w).is_some_and(|end| end <= src_w)
164        && crop_top.checked_add(dst_h).is_some_and(|end| end <= rows)
165        && crop_top
166            .checked_mul(src_w)
167            .and_then(|v| v.checked_add(crop_left))
168            .and_then(|offset| {
169                (dst_h - 1)
170                    .checked_mul(src_w)
171                    .and_then(|rows| offset.checked_add(rows))
172                    .and_then(|v| v.checked_add(dst_w))
173                    .map(|end| (offset, end))
174            })
175            .is_some_and(|(_, end)| end <= data.len());
176
177    if direct {
178        let offset = crop_top * src_w + crop_left;
179        return Ok(PlaneBuffer::from_parts(
180            data,
181            PlaneLayout {
182                width: dst_w,
183                height: dst_h,
184                stride: src_w,
185                offset,
186            },
187        ));
188    }
189
190    let len = dst_w
191        .checked_mul(dst_h)
192        .ok_or_else(|| DecodeError::Bitstream("visible plane dimensions overflow usize".into()))?;
193    let mut out = try_vec![T::default(); len, "visible image plane"];
194    if rows == 0 || src_w == 0 || data.is_empty() {
195        return Ok(PlaneBuffer::tight(out, dst_w, dst_h));
196    }
197    let src_y0 = crop_top.min(rows - 1);
198    let src_x0 = crop_left.min(src_w - 1);
199    for (row, dst) in out.chunks_exact_mut(dst_w).enumerate() {
200        let src_y = (src_y0 + row).min(rows - 1);
201        let src_base = src_y * src_w;
202        let available = (data.len() - src_base).min(src_w);
203        if available == 0 {
204            continue;
205        }
206        let src_x = src_x0.min(available - 1);
207        let copy = dst_w.min(available - src_x);
208        let (exact, pad) = dst.split_at_mut(copy);
209        exact.copy_from_slice(&data[src_base + src_x..src_base + src_x + copy]);
210        if !pad.is_empty() {
211            pad.fill(data[src_base + src_x + copy - 1]);
212        }
213    }
214    Ok(PlaneBuffer::tight(out, dst_w, dst_h))
215}
216
217fn visible_native_planes<T: Copy + Default>(
218    planes: yuv::NativePlanes<T>,
219    dw: usize,
220    dh: usize,
221    crop: HevcVisibleCrop,
222) -> Result<PlanarImage<T>, DecodeError> {
223    let y = plane_window(
224        planes.y,
225        planes.width,
226        planes.height,
227        crop.left,
228        crop.top,
229        dw,
230        dh,
231    )?;
232    if planes.chroma.is_monochrome() {
233        return Ok(PlanarImage {
234            y,
235            cb: None,
236            cr: None,
237        });
238    }
239
240    let sub_w = planes.chroma.sub_w();
241    let sub_h = planes.chroma.sub_h();
242    let coded_cw = planes.width.div_ceil(sub_w);
243    let coded_ch = planes.height.div_ceil(sub_h);
244    let cw = dw.div_ceil(sub_w);
245    let ch = dh.div_ceil(sub_h);
246    let crop_cx = crop.left / sub_w;
247    let crop_cy = crop.top / sub_h;
248    Ok(PlanarImage {
249        y,
250        cb: Some(plane_window(
251            planes.cb, coded_cw, coded_ch, crop_cx, crop_cy, cw, ch,
252        )?),
253        cr: Some(plane_window(
254            planes.cr, coded_cw, coded_ch, crop_cx, crop_cy, cw, ch,
255        )?),
256    })
257}
258
259fn native_to_visible_buffer(
260    planes: yuv::NativeYuvPlanes,
261    dw: usize,
262    dh: usize,
263    crop: HevcVisibleCrop,
264) -> Result<YuvBuffer, DecodeError> {
265    match planes {
266        yuv::NativeYuvPlanes::U8(planes) => {
267            Ok(YuvBuffer::U8(visible_native_planes(planes, dw, dh, crop)?))
268        }
269        yuv::NativeYuvPlanes::U16(planes) => {
270            Ok(YuvBuffer::U16(visible_native_planes(planes, dw, dh, crop)?))
271        }
272    }
273}
274
275/// Decoded HDR gain map carried as a HEIF auxiliary image.
276#[derive(Clone, Debug)]
277pub struct GainMap {
278    /// Luma-plane dimensions.
279    pub width: u32,
280    pub height: u32,
281    /// Dimensions of each chroma plane. Both are zero for monochrome maps.
282    pub chroma_width: u32,
283    pub chroma_height: u32,
284    /// Gain-map luma samples.
285    pub y: SampleBuf,
286    /// Gain-map blue-difference chroma samples. `None` for monochrome maps.
287    pub cb: Option<SampleBuf>,
288    /// Gain-map red-difference chroma samples. `None` for monochrome maps.
289    pub cr: Option<SampleBuf>,
290    /// Chroma subsampling signaled by the gain-map HEVC SPS. The explicit
291    /// chroma dimensions remain authoritative after display orientation; this
292    /// matters for a 90°/270° rotation of a coded 4:2:2 map.
293    pub chroma: ChromaFormat,
294    pub bit_depth: BitDepth,
295    /// Color signalling attached to the gain-map item. This matters for
296    /// converting a three-channel gain map from YCbCr to RGB gains.
297    pub color: ColorMetadata,
298    /// Source orientation from the gain-map item. Display-ready decode applies
299    /// it to all present gain-map planes; raw-YUV decode leaves the samples in
300    /// coded orientation, matching the parent image planes.
301    pub orientation: Orientation,
302    /// Opaque metadata item directly associated with the gain-map auxiliary
303    /// image. Apple files normally store an XMP packet containing fields such
304    /// as `HDRGainMapVersion` and `HDRGainMapHeadroom`.
305    pub metadata: Option<Vec<u8>>,
306}
307
308impl GainMap {
309    /// Number of gain channels represented by the decoded auxiliary image.
310    #[inline]
311    pub fn channels(&self) -> usize {
312        if self.chroma.is_monochrome() { 1 } else { 3 }
313    }
314}
315
316/// Strided, typed gain-map planes returned by [`Decoder::decode_yuv`].
317#[derive(Clone, Debug)]
318pub struct GainMapFrame {
319    pub planes: YuvBuffer,
320    pub bit_depth: BitDepth,
321    pub chroma: ChromaFormat,
322    pub color: ColorMetadata,
323    pub orientation: Orientation,
324    pub metadata: Option<Vec<u8>>,
325}
326
327impl GainMapFrame {
328    #[inline]
329    pub fn width(&self) -> usize {
330        self.planes.width()
331    }
332
333    #[inline]
334    pub fn height(&self) -> usize {
335        self.planes.height()
336    }
337
338    #[inline]
339    pub fn channels(&self) -> usize {
340        if self.chroma.is_monochrome() { 1 } else { 3 }
341    }
342
343    pub fn into_packed(self) -> Result<GainMap, DecodeError> {
344        let width = self.planes.width() as u32;
345        let height = self.planes.height() as u32;
346        let (chroma_width, chroma_height) = match &self.planes {
347            YuvBuffer::U8(planes) => planes
348                .cb
349                .as_ref()
350                .map(|plane| (plane.width() as u32, plane.height() as u32))
351                .unwrap_or((0, 0)),
352            YuvBuffer::U16(planes) => planes
353                .cb
354                .as_ref()
355                .map(|plane| (plane.width() as u32, plane.height() as u32))
356                .unwrap_or((0, 0)),
357        };
358        let (y, cb, cr) = match self.planes {
359            YuvBuffer::U8(planes) => (
360                SampleBuf::U8(planes.y.into_tight()?),
361                planes
362                    .cb
363                    .map(|plane| plane.into_tight().map(SampleBuf::U8))
364                    .transpose()?,
365                planes
366                    .cr
367                    .map(|plane| plane.into_tight().map(SampleBuf::U8))
368                    .transpose()?,
369            ),
370            YuvBuffer::U16(planes) => (
371                SampleBuf::U16(planes.y.into_tight()?),
372                planes
373                    .cb
374                    .map(|plane| plane.into_tight().map(SampleBuf::U16))
375                    .transpose()?,
376                planes
377                    .cr
378                    .map(|plane| plane.into_tight().map(SampleBuf::U16))
379                    .transpose()?,
380            ),
381        };
382        Ok(GainMap {
383            width,
384            height,
385            chroma_width,
386            chroma_height,
387            y,
388            cb,
389            cr,
390            chroma: self.chroma,
391            bit_depth: self.bit_depth,
392            color: self.color,
393            orientation: self.orientation,
394            metadata: self.metadata,
395        })
396    }
397}
398
399/// Zero-copy-or-minimal-copy raw YCbCr result. Single-item HEIC images retain
400/// their decoder allocation and expose the visible crop through plane offsets
401/// and strides. Grid images are naturally tightly packed.
402#[derive(Clone, Debug)]
403pub struct DecodedYuv {
404    pub planes: YuvBuffer,
405    pub alpha: Option<SamplePlane>,
406    pub gain_map: Option<GainMapFrame>,
407    pub bit_depth: BitDepth,
408    pub chroma: ChromaFormat,
409    pub color: ColorMetadata,
410    pub orientation: Orientation,
411    pub clean_aperture: Option<CleanAperture>,
412    pub pixel_aspect_ratio: Option<PixelAspectRatio>,
413    pub exif: Option<Vec<u8>>,
414}
415
416impl DecodedYuv {
417    #[inline]
418    pub fn width(&self) -> usize {
419        self.planes.width()
420    }
421
422    #[inline]
423    pub fn height(&self) -> usize {
424        self.planes.height()
425    }
426}
427
428/// Packed display-ready 8-bit RGB output.
429#[derive(Clone, Debug)]
430pub struct Rgb8Image {
431    /// Interleaved RGB samples, exactly three bytes per pixel.
432    pub pixels: Vec<u8>,
433    pub width: u32,
434    pub height: u32,
435}
436
437/// A fully decoded HEIF/HEIC image.
438pub struct DecodedImage {
439    pub width: u32,
440    pub height: u32,
441    /// Interleaved RGB pixels, typed to the source bit depth.
442    pub pixels: ImageBuffer,
443    pub alpha: Option<SampleBuf>,
444    /// Optional Apple HDR gain-map auxiliary image.
445    pub gain_map: Option<GainMap>,
446    pub bit_depth: BitDepth,
447    pub color: ColorMetadata,
448    pub orientation: Orientation,
449    pub content_light_level: Option<ContentLightLevel>,
450    /// Clean aperture (`clap` property) read from the item properties, if present.
451    pub clean_aperture: Option<CleanAperture>,
452    /// Pixel aspect ratio (`pasp` property). `None` means assume 1:1 (square pixels).
453    pub pixel_aspect_ratio: Option<PixelAspectRatio>,
454    pub exif: Option<Vec<u8>>,
455}
456
457/// Keep malformed optional auxiliary images non-fatal, while still surfacing
458/// allocation failure instead of aborting the process.
459fn optional_auxiliary<T>(result: Result<T, DecodeError>) -> Result<Option<T>, DecodeError> {
460    match result {
461        Ok(value) => Ok(Some(value)),
462        Err(err @ DecodeError::AllocationFailed { .. }) => Err(err),
463        Err(_) => Ok(None),
464    }
465}
466
467/// Decode the optional alpha auxiliary item (`auxl`) into a luma-only plane,
468/// applying the alpha HEVC conformance-window offset. `clap` is intentionally
469/// not applied here.
470fn decode_alpha_plane_native(
471    decoder: &Decoder,
472    file: &[u8],
473    heif: &heif::HeifFile,
474    dw: usize,
475    dh: usize,
476) -> Result<Option<SamplePlane>, DecodeError> {
477    let Some(alpha) = heif.alpha.as_ref() else {
478        return Ok(None);
479    };
480    if alpha.hvcc.is_empty() {
481        return Ok(None);
482    }
483    let (Ok(start), Ok(length)) = (
484        usize::try_from(alpha.data_offset),
485        usize::try_from(alpha.data_length),
486    ) else {
487        return Ok(None);
488    };
489    let Some(end) = start.checked_add(length) else {
490        return Ok(None);
491    };
492    let Some(sample) = file.get(start..end) else {
493        return Ok(None);
494    };
495    let Some((planes, _)) = optional_auxiliary(decode_hevc_item_native(
496        sample,
497        &alpha.hvcc,
498        decoder.exec(),
499        Some(decoder.pool()),
500    ))?
501    else {
502        return Ok(None);
503    };
504    let crop = visible_crop_from_hvcc(&alpha.hvcc);
505    let plane = match planes {
506        yuv::NativeYuvPlanes::U8(planes) => SamplePlane::U8(plane_window(
507            planes.y,
508            planes.width,
509            planes.height,
510            crop.left,
511            crop.top,
512            dw,
513            dh,
514        )?),
515        yuv::NativeYuvPlanes::U16(planes) => SamplePlane::U16(plane_window(
516            planes.y,
517            planes.width,
518            planes.height,
519            crop.left,
520            crop.top,
521            dw,
522            dh,
523        )?),
524    };
525    Ok(Some(plane))
526}
527
528/// Decode alpha for display-ready RGB output and tightly pack the visible rows.
529fn decode_alpha_plane(
530    decoder: &Decoder,
531    file: &[u8],
532    heif: &heif::HeifFile,
533    dw: usize,
534    dh: usize,
535) -> Result<Option<SampleBuf>, DecodeError> {
536    decode_alpha_plane_native(decoder, file, heif, dw, dh)?
537        .map(|plane| match plane {
538            SamplePlane::U8(plane) => plane.into_tight().map(SampleBuf::U8),
539            SamplePlane::U16(plane) => plane.into_tight().map(SampleBuf::U16),
540        })
541        .transpose()
542}
543
544impl DecodedGainMapImage {
545    fn apply_orientation(self) -> Result<Self, DecodeError> {
546        let Self {
547            width,
548            height,
549            chroma_width,
550            chroma_height,
551            y,
552            cb,
553            cr,
554            chroma,
555            bit_depth,
556            color,
557            orientation,
558        } = self;
559        let (oriented_width, oriented_height) = oriented_dimensions(width, height, orientation);
560        let (oriented_chroma_width, oriented_chroma_height) = if chroma.is_monochrome() {
561            (0, 0)
562        } else {
563            oriented_dimensions(chroma_width, chroma_height, orientation)
564        };
565        Ok(Self {
566            width: oriented_width,
567            height: oriented_height,
568            chroma_width: oriented_chroma_width,
569            chroma_height: oriented_chroma_height,
570            y: rotate_sample_buf(y, width, height, orientation)?,
571            cb: cb
572                .map(|plane| rotate_sample_buf(plane, chroma_width, chroma_height, orientation))
573                .transpose()?,
574            cr: cr
575                .map(|plane| rotate_sample_buf(plane, chroma_width, chroma_height, orientation))
576                .transpose()?,
577            chroma,
578            bit_depth,
579            color,
580            orientation,
581        })
582    }
583}
584
585fn rotate_sample_buf(
586    samples: SampleBuf,
587    width: u32,
588    height: u32,
589    orientation: Orientation,
590) -> Result<SampleBuf, DecodeError> {
591    match samples {
592        SampleBuf::U8(samples) => {
593            rotate_luma(width as usize, height as usize, samples, orientation).map(SampleBuf::U8)
594        }
595        SampleBuf::U16(samples) => {
596            rotate_luma(width as usize, height as usize, samples, orientation).map(SampleBuf::U16)
597        }
598    }
599}
600
601fn decode_gain_map_frame_item(
602    decoder: &Decoder,
603    file: &[u8],
604    item: &heif::HeifItem,
605) -> Result<GainMapFrame, DecodeError> {
606    let start = usize::try_from(item.data_offset)
607        .map_err(|_| DecodeError::Bitstream("gain-map data offset exceeds usize".into()))?;
608    let length = usize::try_from(item.data_length)
609        .map_err(|_| DecodeError::Bitstream("gain-map data length exceeds usize".into()))?;
610    let end = start
611        .checked_add(length)
612        .ok_or_else(|| DecodeError::Bitstream("gain-map data range overflows usize".into()))?;
613    let sample = file
614        .get(start..end)
615        .ok_or_else(|| DecodeError::Bitstream("gain-map data extends past file end".into()))?;
616    let (planes, vui_color) =
617        decode_hevc_item_native(sample, &item.hvcc, decoder.exec(), Some(decoder.pool()))?;
618    let crop = visible_crop_from_hvcc(&item.hvcc);
619    let (coded_w, coded_h) = planes.dims();
620    let (fallback_w, fallback_h) = config::parse_hvcc_full(&item.hvcc)
621        .ok()
622        .map(|(sps, _)| {
623            (
624                sps.width.saturating_sub(sps.crop_left + sps.crop_right),
625                sps.height.saturating_sub(sps.crop_top + sps.crop_bottom),
626            )
627        })
628        .unwrap_or((coded_w as u32, coded_h as u32));
629    let width = if item.display_w == 0 {
630        fallback_w
631    } else {
632        item.display_w
633    };
634    let height = if item.display_h == 0 {
635        fallback_h
636    } else {
637        item.display_h
638    };
639    decoder.check_dims(width as usize, height as usize)?;
640    let bit_depth = planes.bit_depth();
641    let chroma = planes.chroma();
642    Ok(GainMapFrame {
643        planes: native_to_visible_buffer(planes, width as usize, height as usize, crop)?,
644        bit_depth,
645        chroma,
646        color: gain_map_color_metadata(&item.color, vui_color),
647        orientation: item.orientation,
648        metadata: None,
649    })
650}
651
652fn decode_gain_map_frame_grid(
653    decoder: &Decoder,
654    file: &[u8],
655    grid: &heif::GridInfo,
656) -> Result<GainMapFrame, DecodeError> {
657    let decoded = decode_native_grid_image(decoder, file, grid)?;
658    let item_color = if grid.color.is_empty() {
659        grid.tiles
660            .iter()
661            .find(|tile| !tile.color.is_empty())
662            .map(|tile| &tile.color)
663            .unwrap_or(&grid.tiles[0].color)
664    } else {
665        &grid.color
666    };
667    Ok(GainMapFrame {
668        planes: decoded.planes,
669        bit_depth: decoded.bit_depth,
670        chroma: decoded.chroma,
671        color: gain_map_color_metadata(item_color, decoded.vui_color),
672        orientation: grid.orientation,
673        metadata: None,
674    })
675}
676
677fn decode_gain_map_frame(
678    decoder: &Decoder,
679    file: &[u8],
680    heif: &heif::HeifFile,
681) -> Result<Option<GainMapFrame>, DecodeError> {
682    let Some(gain) = heif.gain_map.as_ref() else {
683        return Ok(None);
684    };
685    let result = match &gain.image {
686        heif::HeifImageSource::Item(item) => decode_gain_map_frame_item(decoder, file, item),
687        heif::HeifImageSource::Grid(grid) => decode_gain_map_frame_grid(decoder, file, grid),
688    };
689    let Some(mut decoded) = optional_auxiliary(result)? else {
690        return Ok(None);
691    };
692    decoded.metadata = gain.metadata.clone();
693    Ok(Some(decoded))
694}
695
696fn decode_gain_map(
697    decoder: &Decoder,
698    file: &[u8],
699    heif: &heif::HeifFile,
700    apply_orientation: bool,
701) -> Result<Option<GainMap>, DecodeError> {
702    let Some(frame) = decode_gain_map_frame(decoder, file, heif)? else {
703        return Ok(None);
704    };
705    let packed = frame.into_packed()?;
706    if !apply_orientation {
707        return Ok(Some(packed));
708    }
709    let decoded = DecodedGainMapImage {
710        width: packed.width,
711        height: packed.height,
712        chroma_width: packed.chroma_width,
713        chroma_height: packed.chroma_height,
714        y: packed.y,
715        cb: packed.cb,
716        cr: packed.cr,
717        chroma: packed.chroma,
718        bit_depth: packed.bit_depth,
719        color: packed.color,
720        orientation: packed.orientation,
721    }
722    .apply_orientation()?;
723    Ok(Some(GainMap {
724        width: decoded.width,
725        height: decoded.height,
726        chroma_width: decoded.chroma_width,
727        chroma_height: decoded.chroma_height,
728        y: decoded.y,
729        cb: decoded.cb,
730        cr: decoded.cr,
731        chroma: decoded.chroma,
732        bit_depth: decoded.bit_depth,
733        color: decoded.color,
734        orientation: decoded.orientation,
735        metadata: packed.metadata,
736    }))
737}
738
739fn gain_map_color_metadata(item_color: &ColorMetadata, vui_color: Cicp) -> ColorMetadata {
740    let mut color = item_color.clone();
741    if vui_color.matrix != MatrixCoefficients::Unspecified {
742        color.cicp = Some(vui_color);
743    }
744    color
745}
746
747/// Decode a HEIF/HEIC file and return typed, owning, strided raw YCbCr
748/// planes using a default [`Decoder`]. Eight-bit items use `u8`; 10/12-bit
749/// items use `u16`. Single-item images retain coded stride and padding.
750pub fn decode_heic_yuv(file: &[u8]) -> Result<DecodedYuv, DecodeError> {
751    Decoder::default().decode_yuv(file)
752}
753
754/// Decode typed, strided raw YCbCr planes with one explicit settings value.
755pub fn decode_heic_yuv_with_settings(
756    file: &[u8],
757    settings: &HeicSettings,
758) -> Result<DecodedYuv, DecodeError> {
759    Decoder::from_settings(*settings).decode_yuv(file)
760}
761
762pub(crate) fn decode_heic_yuv_with(
763    decoder: &Decoder,
764    file: &[u8],
765) -> Result<DecodedYuv, DecodeError> {
766    let heif = heif::parse(
767        file,
768        decoder.limits(),
769        heif::HeifParseOptions {
770            load_alpha: decoder.decodes_alpha(),
771            load_gain_map: decoder.decodes_gain_map(),
772        },
773    )?;
774
775    if let Some(grid) = &heif.grid {
776        return decode_grid_yuv(decoder, file, grid, &heif);
777    }
778
779    let start = usize::try_from(heif.primary.data_offset)
780        .map_err(|_| DecodeError::Bitstream("image data offset exceeds usize".into()))?;
781    let length = usize::try_from(heif.primary.data_length)
782        .map_err(|_| DecodeError::Bitstream("image data length exceeds usize".into()))?;
783    let end = start
784        .checked_add(length)
785        .ok_or_else(|| DecodeError::Bitstream("image data range overflows usize".into()))?;
786    let sample = file
787        .get(start..end)
788        .ok_or_else(|| DecodeError::Bitstream("image data extends past file end".into()))?;
789    let (planes, _) = decode_hevc_item_native(
790        sample,
791        &heif.primary.hvcc,
792        decoder.exec(),
793        Some(decoder.pool()),
794    )?;
795    let width = heif.primary.display_w as usize;
796    let height = heif.primary.display_h as usize;
797    decoder.check_dims(width, height)?;
798    let bit_depth = planes.bit_depth();
799    let chroma = planes.chroma();
800    let crop = visible_crop_from_hvcc(&heif.primary.hvcc);
801    let visible = native_to_visible_buffer(planes, width, height, crop)?;
802    let alpha = if decoder.decodes_alpha() {
803        decode_alpha_plane_native(decoder, file, &heif, width, height)?
804    } else {
805        None
806    };
807    let gain_map = if decoder.decodes_gain_map() {
808        decode_gain_map_frame(decoder, file, &heif)?
809    } else {
810        None
811    };
812
813    Ok(DecodedYuv {
814        planes: visible,
815        alpha,
816        gain_map,
817        bit_depth,
818        chroma,
819        color: heif.primary.color,
820        orientation: heif.primary.orientation,
821        clean_aperture: heif.primary.clap,
822        pixel_aspect_ratio: heif.primary.pasp,
823        exif: heif.exif,
824    })
825}
826
827/// Immutable per-grid context for the YUV band workers.
828struct YuvGridCtx<'a> {
829    file: &'a [u8],
830    exec: &'a exec::ExecContext,
831    tiles: &'a [heif::HeifItem],
832    fallback_hvcc: &'a [u8],
833    cols: usize,
834    out_w: usize,
835    out_h: usize,
836    cw: usize,
837    ch: usize,
838    tile_w: usize,
839    tile_h: usize,
840    tile_cw: usize,
841    tile_ch: usize,
842    tile_crop_left: usize,
843    tile_crop_top: usize,
844    sub_w: usize,
845    sub_h: usize,
846    has_chroma: bool,
847}
848
849trait NativeGridSample: Copy + Default + Send {
850    fn select(planes: &yuv::NativeYuvPlanes) -> Option<&yuv::NativePlanes<Self>>;
851}
852
853impl NativeGridSample for u8 {
854    fn select(planes: &yuv::NativeYuvPlanes) -> Option<&yuv::NativePlanes<Self>> {
855        match planes {
856            yuv::NativeYuvPlanes::U8(planes) => Some(planes),
857            yuv::NativeYuvPlanes::U16(_) => None,
858        }
859    }
860}
861
862impl NativeGridSample for u16 {
863    fn select(planes: &yuv::NativeYuvPlanes) -> Option<&yuv::NativePlanes<Self>> {
864        match planes {
865            yuv::NativeYuvPlanes::U8(_) => None,
866            yuv::NativeYuvPlanes::U16(planes) => Some(planes),
867        }
868    }
869}
870
871fn stitch_native_yuv_band<T: NativeGridSample>(
872    ctx: &YuvGridCtx<'_>,
873    band_row: usize,
874    y_band: &mut [T],
875    cb_band: &mut [T],
876    cr_band: &mut [T],
877) -> Result<(), DecodeError> {
878    for col in 0..ctx.cols {
879        let tile_idx = band_row * ctx.cols + col;
880        let Some(tile) = ctx.tiles.get(tile_idx) else {
881            break;
882        };
883        let Ok(start) = usize::try_from(tile.data_offset) else {
884            continue;
885        };
886        let Ok(length) = usize::try_from(tile.data_length) else {
887            continue;
888        };
889        let Some(end) = start.checked_add(length) else {
890            continue;
891        };
892        let Some(sample) = ctx.file.get(start..end) else {
893            continue;
894        };
895        let hvcc = if tile.hvcc.is_empty() {
896            ctx.fallback_hvcc
897        } else {
898            &tile.hvcc
899        };
900        if hvcc.is_empty() {
901            continue;
902        }
903        let Some((native, _)) =
904            optional_auxiliary(decode_hevc_item_native(sample, hvcc, ctx.exec, None))?
905        else {
906            continue;
907        };
908        let Some(planes) = T::select(&native) else {
909            continue;
910        };
911
912        let dst_x = col * ctx.tile_w;
913        let dst_y_base = band_row * ctx.tile_h;
914        let copy_w = ctx.tile_w.min(ctx.out_w.saturating_sub(dst_x));
915        let copy_h = ctx.tile_h.min(ctx.out_h.saturating_sub(dst_y_base));
916        if copy_w == 0 || copy_h == 0 || planes.width == 0 || planes.height == 0 {
917            continue;
918        }
919        let crop_left = ctx.tile_crop_left.min(planes.width - 1);
920        let crop_top = ctx.tile_crop_top.min(planes.height - 1);
921        for y in 0..copy_h {
922            let dst = &mut y_band[y * ctx.out_w + dst_x..][..copy_w];
923            let src_y = (crop_top + y).min(planes.height - 1);
924            let src_base = src_y * planes.width;
925            if src_base >= planes.y.len() {
926                continue;
927            }
928            let available = (planes.y.len() - src_base).min(planes.width);
929            if available == 0 {
930                continue;
931            }
932            let src_x = crop_left.min(available - 1);
933            let exact = copy_w.min(available - src_x);
934            let (copied, pad) = dst.split_at_mut(exact);
935            copied.copy_from_slice(&planes.y[src_base + src_x..src_base + src_x + exact]);
936            if !pad.is_empty() {
937                pad.fill(planes.y[src_base + src_x + exact - 1]);
938            }
939        }
940
941        if !ctx.has_chroma || planes.cb.is_empty() || planes.cr.is_empty() {
942            continue;
943        }
944        let plane_cw = planes.width.div_ceil(ctx.sub_w);
945        let plane_ch = planes.height.div_ceil(ctx.sub_h);
946        if plane_cw == 0 || plane_ch == 0 {
947            continue;
948        }
949        let dst_x = col * ctx.tile_cw;
950        let copy_w = ctx.tile_cw.min(ctx.cw.saturating_sub(dst_x));
951        let copy_h = ctx
952            .tile_ch
953            .min(ctx.ch.saturating_sub(band_row * ctx.tile_ch));
954        if copy_w == 0 || copy_h == 0 {
955            continue;
956        }
957        let crop_left = (ctx.tile_crop_left / ctx.sub_w).min(plane_cw - 1);
958        let crop_top = (ctx.tile_crop_top / ctx.sub_h).min(plane_ch - 1);
959        for y in 0..copy_h {
960            let dst_start = y * ctx.cw + dst_x;
961            let src_y = (crop_top + y).min(plane_ch - 1);
962            let src_base = src_y * plane_cw;
963            if src_base >= planes.cb.len() || src_base >= planes.cr.len() {
964                continue;
965            }
966            let available = (planes.cb.len() - src_base)
967                .min(plane_cw)
968                .min((planes.cr.len() - src_base).min(plane_cw));
969            if available == 0 {
970                continue;
971            }
972            let src_x = crop_left.min(available - 1);
973            let exact = copy_w.min(available - src_x);
974
975            let cb_dst = &mut cb_band[dst_start..dst_start + copy_w];
976            let (copied, pad) = cb_dst.split_at_mut(exact);
977            copied.copy_from_slice(&planes.cb[src_base + src_x..src_base + src_x + exact]);
978            if !pad.is_empty() {
979                pad.fill(planes.cb[src_base + src_x + exact - 1]);
980            }
981
982            let cr_dst = &mut cr_band[dst_start..dst_start + copy_w];
983            let (copied, pad) = cr_dst.split_at_mut(exact);
984            copied.copy_from_slice(&planes.cr[src_base + src_x..src_base + src_x + exact]);
985            if !pad.is_empty() {
986                pad.fill(planes.cr[src_base + src_x + exact - 1]);
987            }
988        }
989    }
990    Ok(())
991}
992
993fn fill_native_grid_yuv<T: NativeGridSample>(
994    decoder: &Decoder,
995    rows: usize,
996    ctx: &YuvGridCtx<'_>,
997) -> Result<PlanarImage<T>, DecodeError> {
998    let y_total = ctx
999        .out_w
1000        .checked_mul(ctx.out_h)
1001        .ok_or_else(|| DecodeError::Bitstream("grid luma dimensions overflow usize".into()))?;
1002    let c_total = ctx
1003        .cw
1004        .checked_mul(ctx.ch)
1005        .ok_or_else(|| DecodeError::Bitstream("grid chroma dimensions overflow usize".into()))?;
1006    let y_stride = ctx.tile_h.saturating_mul(ctx.out_w);
1007    let c_stride = ctx.tile_ch.saturating_mul(ctx.cw);
1008    let pool = decoder.pool();
1009
1010    let (y, cb, cr) = if pool.threads() > 1 && rows > 1 {
1011        let y = threadpool::DisjointMut::new(try_vec![T::default(); y_total, "grid luma plane"]);
1012        let cb = threadpool::DisjointMut::new(try_vec![T::default(); c_total, "grid Cb plane"]);
1013        let cr = threadpool::DisjointMut::new(try_vec![T::default(); c_total, "grid Cr plane"]);
1014        let error = std::sync::Mutex::new(None);
1015        threadpool::parallel_for(pool, rows, |row| {
1016            if error.lock().unwrap().is_some() {
1017                return;
1018            }
1019            let y_lo = row * y_stride;
1020            let y_hi = ((row + 1) * y_stride).min(y_total);
1021            if y_lo >= y_hi {
1022                return;
1023            }
1024            let mut y_band = y.slice_mut(y_lo..y_hi);
1025            let c_lo = row * c_stride;
1026            let c_hi = ((row + 1) * c_stride).min(c_total);
1027            let result = if c_lo < c_hi {
1028                let mut cb_band = cb.slice_mut(c_lo..c_hi);
1029                let mut cr_band = cr.slice_mut(c_lo..c_hi);
1030                stitch_native_yuv_band(ctx, row, &mut y_band, &mut cb_band, &mut cr_band)
1031            } else {
1032                stitch_native_yuv_band(ctx, row, &mut y_band, &mut [], &mut [])
1033            };
1034            if let Err(err) = result {
1035                *error.lock().unwrap() = Some(err);
1036            }
1037        });
1038        if let Some(err) = error.into_inner().unwrap() {
1039            return Err(err);
1040        }
1041        (y.into_inner(), cb.into_inner(), cr.into_inner())
1042    } else {
1043        let mut y = try_vec![T::default(); y_total, "grid luma plane"];
1044        let mut cb = try_vec![T::default(); c_total, "grid Cb plane"];
1045        let mut cr = try_vec![T::default(); c_total, "grid Cr plane"];
1046        for row in 0..rows {
1047            let y_lo = row * y_stride;
1048            let y_hi = ((row + 1) * y_stride).min(y_total);
1049            if y_lo >= y_hi {
1050                continue;
1051            }
1052            let c_lo = row * c_stride;
1053            let c_hi = ((row + 1) * c_stride).min(c_total);
1054            if c_lo < c_hi {
1055                stitch_native_yuv_band(
1056                    ctx,
1057                    row,
1058                    &mut y[y_lo..y_hi],
1059                    &mut cb[c_lo..c_hi],
1060                    &mut cr[c_lo..c_hi],
1061                )?;
1062            } else {
1063                stitch_native_yuv_band(ctx, row, &mut y[y_lo..y_hi], &mut [], &mut [])?;
1064            }
1065        }
1066        (y, cb, cr)
1067    };
1068
1069    Ok(PlanarImage {
1070        y: PlaneBuffer::tight(y, ctx.out_w, ctx.out_h),
1071        cb: ctx
1072            .has_chroma
1073            .then(|| PlaneBuffer::tight(cb, ctx.cw, ctx.ch)),
1074        cr: ctx
1075            .has_chroma
1076            .then(|| PlaneBuffer::tight(cr, ctx.cw, ctx.ch)),
1077    })
1078}
1079
1080struct NativeGridImage {
1081    planes: YuvBuffer,
1082    bit_depth: BitDepth,
1083    chroma: ChromaFormat,
1084    vui_color: Cicp,
1085}
1086
1087fn decode_native_grid_image(
1088    decoder: &Decoder,
1089    file: &[u8],
1090    grid: &heif::GridInfo,
1091) -> Result<NativeGridImage, DecodeError> {
1092    let out_w = grid.output_width as usize;
1093    let out_h = grid.output_height as usize;
1094    decoder.check_dims(out_w, out_h)?;
1095    if grid.tiles.is_empty() {
1096        return Err(DecodeError::Bitstream("grid has no tiles".into()));
1097    }
1098    let cols = grid.cols as usize;
1099    let rows = grid.rows as usize;
1100    let fallback_hvcc = grid
1101        .tiles
1102        .iter()
1103        .find(|tile| !tile.hvcc.is_empty())
1104        .map(|tile| tile.hvcc.clone())
1105        .unwrap_or_default();
1106    let hvcc = if grid.tiles[0].hvcc.is_empty() {
1107        &fallback_hvcc
1108    } else {
1109        &grid.tiles[0].hvcc
1110    };
1111    if hvcc.is_empty() {
1112        return Err(DecodeError::MissingBox("grid hvcC property"));
1113    }
1114    let (sps, _) = config::parse_hvcc_full(hvcc)?;
1115    let chroma = sps.chroma;
1116    let bit_depth = match sps.bit_depth_luma {
1117        10 => BitDepth::Ten,
1118        12 => BitDepth::Twelve,
1119        _ => BitDepth::Eight,
1120    };
1121    let tile_w = sps.width.saturating_sub(sps.crop_left + sps.crop_right) as usize;
1122    let tile_h = sps.height.saturating_sub(sps.crop_top + sps.crop_bottom) as usize;
1123    let (tile_w, tile_h) = if tile_w != 0 && tile_h != 0 {
1124        (tile_w, tile_h)
1125    } else {
1126        (out_w.div_ceil(cols), out_h.div_ceil(rows))
1127    };
1128    let sub_w = chroma.sub_w();
1129    let sub_h = chroma.sub_h();
1130    let has_chroma = !chroma.is_monochrome();
1131    let (cw, ch) = if has_chroma {
1132        (out_w.div_ceil(sub_w), out_h.div_ceil(sub_h))
1133    } else {
1134        (0, 0)
1135    };
1136    let ctx = YuvGridCtx {
1137        file,
1138        exec: decoder.exec(),
1139        tiles: &grid.tiles,
1140        fallback_hvcc: &fallback_hvcc,
1141        cols,
1142        out_w,
1143        out_h,
1144        cw,
1145        ch,
1146        tile_w,
1147        tile_h,
1148        tile_cw: tile_w.div_ceil(sub_w),
1149        tile_ch: tile_h.div_ceil(sub_h),
1150        tile_crop_left: sps.crop_left as usize,
1151        tile_crop_top: sps.crop_top as usize,
1152        sub_w,
1153        sub_h,
1154        has_chroma,
1155    };
1156    let planes = if bit_depth == BitDepth::Eight {
1157        YuvBuffer::U8(fill_native_grid_yuv::<u8>(decoder, rows, &ctx)?)
1158    } else {
1159        YuvBuffer::U16(fill_native_grid_yuv::<u16>(decoder, rows, &ctx)?)
1160    };
1161    Ok(NativeGridImage {
1162        planes,
1163        bit_depth,
1164        chroma,
1165        vui_color: Cicp {
1166            primaries: Primaries::from_u8(sps.color_primaries),
1167            transfer: TransferFunction::from_u8(sps.transfer_characteristics),
1168            matrix: MatrixCoefficients::from_u8(sps.matrix_coefficients),
1169            full_range: sps.video_full_range,
1170        },
1171    })
1172}
1173
1174/// Composite a tiled grid into a strided typed YUV frame. Grid output is
1175/// tightly packed because stitching writes directly into final-sized planes.
1176fn decode_grid_yuv(
1177    decoder: &Decoder,
1178    file: &[u8],
1179    grid: &heif::GridInfo,
1180    heif_file: &heif::HeifFile,
1181) -> Result<DecodedYuv, DecodeError> {
1182    let decoded = decode_native_grid_image(decoder, file, grid)?;
1183    let width = grid.output_width as usize;
1184    let height = grid.output_height as usize;
1185    let alpha = if decoder.decodes_alpha() {
1186        decode_alpha_plane_native(decoder, file, heif_file, width, height)?
1187    } else {
1188        None
1189    };
1190    let gain_map = if decoder.decodes_gain_map() {
1191        decode_gain_map_frame(decoder, file, heif_file)?
1192    } else {
1193        None
1194    };
1195    Ok(DecodedYuv {
1196        planes: decoded.planes,
1197        alpha,
1198        gain_map,
1199        bit_depth: decoded.bit_depth,
1200        chroma: decoded.chroma,
1201        color: heif_file.primary.color.clone(),
1202        orientation: grid.orientation,
1203        clean_aperture: heif_file.primary.clap,
1204        pixel_aspect_ratio: heif_file.primary.pasp,
1205        exif: heif_file.exif.clone(),
1206    })
1207}
1208
1209#[inline]
1210fn hvcc_nal_length_size(hvcc: &[u8]) -> Result<usize, DecodeError> {
1211    // HEVCDecoderConfigurationRecord.lengthSizeMinusOne occupies the low two
1212    // bits of byte 21; samples may therefore use 1, 2, 3, or 4-byte lengths.
1213    let length_size_minus_one = *hvcc
1214        .get(21)
1215        .ok_or_else(|| DecodeError::ParamSet("hvcC too short for NAL length size".into()))?
1216        & 0x03;
1217    Ok(length_size_minus_one as usize + 1)
1218}
1219
1220#[inline]
1221fn read_length_prefixed_nal_size(
1222    sample: &[u8],
1223    pos: &mut usize,
1224    length_size: usize,
1225) -> Result<usize, DecodeError> {
1226    let end = pos
1227        .checked_add(length_size)
1228        .ok_or_else(|| DecodeError::Bitstream("NAL length offset overflows usize".into()))?;
1229    let bytes = sample
1230        .get(*pos..end)
1231        .ok_or_else(|| DecodeError::Bitstream("truncated HEIF NAL length prefix".into()))?;
1232    *pos = end;
1233
1234    let mut size = 0usize;
1235    for &byte in bytes {
1236        size = size
1237            .checked_shl(8)
1238            .and_then(|v| v.checked_add(byte as usize))
1239            .ok_or_else(|| DecodeError::Bitstream("HEIF NAL length overflows usize".into()))?;
1240    }
1241    Ok(size)
1242}
1243
1244fn configure_still_inter_state(
1245    decoder: &mut decode::FullDecoder<'_>,
1246    sps: &config::Sps,
1247    pps: &config::Pps,
1248    hdr: &decode::SliceHeader,
1249) -> Result<(), DecodeError> {
1250    if !(sps.curr_pic_ref_enabled && pps.curr_pic_ref_enabled) {
1251        return Ok(());
1252    }
1253
1254    // HEVC image items are independently decodable, so an SCC still picture
1255    // has no preceding DPB references. CurrPic is nevertheless a real active
1256    // long-term entry in RefPicList0/1. The video driver installs this entry;
1257    // the old HEIF path left both lists empty, causing every IBC PU to miss the
1258    // current-picture path and fall back to mid-grey prediction.
1259    let current = dpb::RefEntry {
1260        _dpb_index: usize::MAX,
1261        poc: 0,
1262        long_term: true,
1263    };
1264    let empty_rps = dpb::RpsPocs {
1265        before: Vec::new(),
1266        after: Vec::new(),
1267        lt: Vec::new(),
1268    };
1269    let (list0, list1) = dpb::Dpb::new(1).build_ref_lists(
1270        &empty_rps,
1271        hdr.num_ref_idx_l0,
1272        hdr.num_ref_idx_l1,
1273        hdr.slice_type == inter::SLICE_B,
1274        Some(current),
1275        &hdr.list_mod_l0,
1276        &hdr.list_mod_l1,
1277    )?;
1278    decoder.set_inter_state(0, list0, list1, Vec::new());
1279    Ok(())
1280}
1281
1282trait StillPictureOutput {
1283    type Planes;
1284
1285    fn finish(
1286        decoder: &mut decode::FullDecoder<'_>,
1287        pool: Option<&threadpool::ThreadPool>,
1288    ) -> Result<Self::Planes, DecodeError>;
1289}
1290
1291struct U16StillPicture;
1292struct NativeStillPicture;
1293
1294impl StillPictureOutput for U16StillPicture {
1295    type Planes = yuv::YuvPlanes;
1296
1297    #[inline]
1298    fn finish(
1299        decoder: &mut decode::FullDecoder<'_>,
1300        pool: Option<&threadpool::ThreadPool>,
1301    ) -> Result<Self::Planes, DecodeError> {
1302        decoder.finish_with(pool, pool)
1303    }
1304}
1305
1306impl StillPictureOutput for NativeStillPicture {
1307    type Planes = yuv::NativeYuvPlanes;
1308
1309    #[inline]
1310    fn finish(
1311        decoder: &mut decode::FullDecoder<'_>,
1312        pool: Option<&threadpool::ThreadPool>,
1313    ) -> Result<Self::Planes, DecodeError> {
1314        decoder.finish_native_with(pool, pool)
1315    }
1316}
1317
1318fn decode_hevc_item_as<O: StillPictureOutput>(
1319    sample: &[u8],
1320    hvcc: &[u8],
1321    exec: &exec::ExecContext,
1322    pool: Option<&threadpool::ThreadPool>,
1323) -> Result<(O::Planes, Cicp), DecodeError> {
1324    use config::parse_hvcc_full;
1325    use decode::{FullDecoder, SliceHeader, parse_slice_header_full};
1326
1327    struct StillSlice {
1328        source: Vec<u8>,
1329        rbsp: Vec<u8>,
1330        hdr: SliceHeader,
1331    }
1332
1333    let (sps, pps) = parse_hvcc_full(hvcc)?;
1334    let nal_length_size = hvcc_nal_length_size(hvcc)?;
1335
1336    let vui_color = Cicp {
1337        primaries: Primaries::from_u8(sps.color_primaries),
1338        transfer: TransferFunction::from_u8(sps.transfer_characteristics),
1339        matrix: MatrixCoefficients::from_u8(sps.matrix_coefficients),
1340        full_range: sps.video_full_range,
1341    };
1342
1343    // Collect the complete access unit before decoding. Besides making malformed
1344    // length prefixes fail loudly, this lets us enforce the same WPP rule as the
1345    // video path: the whole-picture wavefront is valid only for one independent
1346    // slice segment.
1347    let mut slices = Vec::new();
1348    let mut pos = 0usize;
1349    while pos < sample.len() {
1350        let nlen = read_length_prefixed_nal_size(sample, &mut pos, nal_length_size)?;
1351        if nlen < 2 {
1352            return Err(DecodeError::Bitstream(
1353                "HEIF sample contains an undersized NAL unit".into(),
1354            ));
1355        }
1356        let end = pos
1357            .checked_add(nlen)
1358            .ok_or_else(|| DecodeError::Bitstream("NAL payload offset overflows usize".into()))?;
1359        let nal_bytes = sample
1360            .get(pos..end)
1361            .ok_or_else(|| DecodeError::Bitstream("truncated HEIF NAL payload".into()))?;
1362        pos = end;
1363
1364        let nal_type = (nal_bytes[0] >> 1) & 0x3f;
1365        if nal_type > 31 {
1366            continue;
1367        }
1368
1369        let source = nal_bytes.to_vec();
1370        let rbsp = bitreader::unescape_rbsp(&source);
1371        let hdr = parse_slice_header_full(&rbsp, &sps, &pps, nal_type)?;
1372        slices.push(StillSlice { source, rbsp, hdr });
1373    }
1374
1375    let first = slices
1376        .first()
1377        .ok_or_else(|| DecodeError::Bitstream("no VCL slice NAL found".into()))?;
1378    if !first.hdr.first_slice_in_pic || first.hdr.dependent_slice_segment {
1379        return Err(DecodeError::Bitstream(
1380            "HEIF item does not begin with an independent first slice".into(),
1381        ));
1382    }
1383
1384    let first_cabac = &first.rbsp[first.hdr.cabac_offset.min(first.rbsp.len())..];
1385    let mut decoder = FullDecoder::new_with_exec(
1386        first_cabac,
1387        sps.clone(),
1388        pps.clone(),
1389        &first.hdr,
1390        exec.clone(),
1391    )?;
1392    configure_still_inter_state(&mut decoder, &sps, &pps, &first.hdr)?;
1393
1394    let first_sub_starts: Vec<usize> = if first.hdr.entry_points.is_empty() {
1395        Vec::new()
1396    } else {
1397        let src_of = bitreader::rbsp_src_map(&first.source);
1398        wpp::substream_starts_rbsp_rel(
1399            &src_of,
1400            first.hdr.cabac_offset,
1401            &first.hdr.entry_points,
1402            first.rbsp.len(),
1403        )
1404    };
1405
1406    // Match VideoDecoder: its row decoder currently supports only a single
1407    // whole-picture I-slice. Running it for the first segment of a multi-slice
1408    // HEIF item decodes past that segment and then decodes later segments again.
1409    let ran_wavefront = if slices.len() == 1 && first.hdr.slice_type == inter::SLICE_I {
1410        match pool {
1411            Some(p) => decoder.try_decode_wavefront(&first.rbsp, &first.source, &first.hdr, p)?,
1412            None => false,
1413        }
1414    } else {
1415        false
1416    };
1417    if !ran_wavefront {
1418        let starts = if first_sub_starts.is_empty() {
1419            None
1420        } else {
1421            Some((first_cabac, first_sub_starts.as_slice()))
1422        };
1423        decoder.decode_slice_ctx(first.hdr.slice_segment_address, starts)?;
1424    }
1425
1426    for segment in slices.iter().skip(1) {
1427        if segment.hdr.first_slice_in_pic {
1428            return Err(DecodeError::Bitstream(
1429                "HEIF image item contains more than one coded picture".into(),
1430            ));
1431        }
1432        if !segment.hdr.dependent_slice_segment {
1433            configure_still_inter_state(&mut decoder, &sps, &pps, &segment.hdr)?;
1434        }
1435
1436        let sub_starts: Vec<usize> = if segment.hdr.entry_points.is_empty() {
1437            Vec::new()
1438        } else {
1439            let src_of = bitreader::rbsp_src_map(&segment.source);
1440            wpp::substream_starts_rbsp_rel(
1441                &src_of,
1442                segment.hdr.cabac_offset,
1443                &segment.hdr.entry_points,
1444                segment.rbsp.len(),
1445            )
1446        };
1447        let cabac = &segment.rbsp[segment.hdr.cabac_offset.min(segment.rbsp.len())..];
1448        decoder.decode_segment(cabac, &segment.hdr, &sub_starts)?;
1449    }
1450
1451    // Keep the still-image path bit-identical to VideoDecoder. The parallel
1452    // chroma deblock kernel is intentionally not selected there because it is
1453    // not yet bit-exact; SAO can remain parallel.
1454    Ok((O::finish(&mut decoder, pool)?, vui_color))
1455}
1456
1457fn decode_hevc_item(
1458    sample: &[u8],
1459    hvcc: &[u8],
1460    exec: &exec::ExecContext,
1461    pool: Option<&threadpool::ThreadPool>,
1462) -> Result<(yuv::YuvPlanes, Cicp), DecodeError> {
1463    decode_hevc_item_as::<U16StillPicture>(sample, hvcc, exec, pool)
1464}
1465
1466fn decode_hevc_item_native(
1467    sample: &[u8],
1468    hvcc: &[u8],
1469    exec: &exec::ExecContext,
1470    pool: Option<&threadpool::ThreadPool>,
1471) -> Result<(yuv::NativeYuvPlanes, Cicp), DecodeError> {
1472    decode_hevc_item_as::<NativeStillPicture>(sample, hvcc, exec, pool)
1473}
1474
1475/// Decode a HEIF/HEIC file to display-ready pixels using a default [`Decoder`].
1476pub fn decode_heic(file: &[u8]) -> Result<DecodedImage, DecodeError> {
1477    Decoder::default().decode(file)
1478}
1479
1480/// Decode a display-ready image with one explicit settings value. This creates
1481/// a decoder and worker pool for the call; reuse [`Decoder::from_settings`] for
1482/// a sequence of images.
1483pub fn decode_heic_with_settings(
1484    file: &[u8],
1485    settings: &HeicSettings,
1486) -> Result<DecodedImage, DecodeError> {
1487    Decoder::from_settings(*settings).decode(file)
1488}
1489
1490pub(crate) fn decode_heic_with(
1491    decoder: &Decoder,
1492    file: &[u8],
1493) -> Result<DecodedImage, DecodeError> {
1494    decode_heic_impl(decoder, file, true)
1495}
1496
1497fn decode_heic_impl(
1498    decoder: &Decoder,
1499    file: &[u8],
1500    decode_auxiliary_images: bool,
1501) -> Result<DecodedImage, DecodeError> {
1502    let heif = heif::parse(
1503        file,
1504        decoder.limits(),
1505        heif::HeifParseOptions {
1506            load_alpha: decode_auxiliary_images && decoder.decodes_alpha(),
1507            load_gain_map: decode_auxiliary_images && decoder.decodes_gain_map(),
1508        },
1509    )?;
1510
1511    if let Some(grid) = &heif.grid {
1512        return decode_grid(decoder, file, grid, &heif, decode_auxiliary_images);
1513    }
1514
1515    let start = heif.primary.data_offset as usize;
1516    let Some(end) = start.checked_add(heif.primary.data_length as usize) else {
1517        return Err(DecodeError::Bitstream(
1518            "image data offset/length overflows usize".into(),
1519        ));
1520    };
1521    if end > file.len() {
1522        return Err(DecodeError::Bitstream(
1523            "image data extends past file end".into(),
1524        ));
1525    }
1526    let (yuv_planes, vui_color) = decode_hevc_item(
1527        &file[start..end],
1528        &heif.primary.hvcc,
1529        decoder.exec(),
1530        Some(decoder.pool()),
1531    )?;
1532    let dw = heif.primary.display_w as usize;
1533    let dh = heif.primary.display_h as usize;
1534    decoder.check_dims(dw, dh)?;
1535
1536    // Color encoding for the YCbCr→RGB step.
1537    // The VUI matrix/range values describe how the YCbCr was encoded; prefer
1538    // those.  If VUI says "unspecified" (matrix==2), fall back to the HEIF
1539    // `colr` box, and if that is an ICC profile (no explicit CICP), default
1540    // to sRGB (full-range BT.709).
1541    // Priority: VUI (from HEVC SPS) > CICP from colr box > sRGB fallback
1542    let color_enc = if vui_color.matrix != MatrixCoefficients::Unspecified {
1543        vui_color
1544    } else {
1545        heif.primary.color.cicp.unwrap_or_else(Cicp::srgb)
1546    };
1547    let crop = visible_crop_from_hvcc(&heif.primary.hvcc);
1548    let rgb = yuv::yuv_to_rgb_window_with_color_pool(
1549        &yuv_planes,
1550        dw,
1551        dh,
1552        crop.left,
1553        crop.top,
1554        &color_enc,
1555        Some(decoder.pool()),
1556    )?;
1557
1558    let alpha = if decode_auxiliary_images && decoder.decodes_alpha() {
1559        decode_alpha_plane(decoder, file, &heif, dw, dh)?
1560    } else {
1561        None
1562    };
1563    let gain_map = if decode_auxiliary_images && decoder.decodes_gain_map() {
1564        decode_gain_map(decoder, file, &heif, true)?
1565    } else {
1566        None
1567    };
1568
1569    let oriented = apply_orientation(dw as u32, dh as u32, rgb, alpha, heif.primary.orientation)?;
1570
1571    Ok(DecodedImage {
1572        width: oriented.width,
1573        height: oriented.height,
1574        pixels: oriented.pixels,
1575        alpha: oriented.alpha,
1576        gain_map,
1577        bit_depth: yuv_planes.bit_depth,
1578        color: heif.primary.color,
1579        orientation: heif.primary.orientation,
1580        content_light_level: heif.primary.cll,
1581        clean_aperture: heif.primary.clap,
1582        pixel_aspect_ratio: heif.primary.pasp,
1583        exif: heif.exif,
1584    })
1585}
1586
1587/// Immutable per-grid context shared by every band worker. Holding only shared
1588/// references keeps it `Sync`, so it can be borrowed across the thread pool.
1589struct GridCtx<'a> {
1590    file: &'a [u8],
1591    exec: &'a exec::ExecContext,
1592    tiles: &'a [heif::HeifItem],
1593    fallback_hvcc: &'a [u8],
1594    color_enc: &'a Cicp,
1595    cols: usize,
1596    out_w: usize,
1597    out_h: usize,
1598    tile_w: usize,
1599    tile_h: usize,
1600    tile_crop_left: usize,
1601    tile_crop_top: usize,
1602    channels: usize,
1603}
1604
1605/// Decode every tile in grid-row `band_row` and stitch it into `band`, the
1606/// contiguous output slice for rows `[band_row*tile_h, ..)`. The band's local
1607/// row 0 is global row `band_row*tile_h`, so the vertical destination within the
1608/// band is simply `y`. `pick` pulls the matching-depth samples out of the
1609/// per-tile RGB/luma buffer. `T` is `u8` (8-bit) or `u16` (10/12-bit).
1610fn stitch_grid_band<T: Copy>(
1611    ctx: &GridCtx<'_>,
1612    band_row: usize,
1613    band: &mut [T],
1614    pick: &(dyn Fn(&ImageBuffer) -> Option<&[T]> + Sync),
1615) -> Result<(), DecodeError> {
1616    let dst_y_base = band_row * ctx.tile_h;
1617    for col in 0..ctx.cols {
1618        let tile_idx = band_row * ctx.cols + col;
1619        let tile = match ctx.tiles.get(tile_idx) {
1620            Some(tile) => tile,
1621            None => break,
1622        };
1623        let start = tile.data_offset as usize;
1624        let Some(end) = start.checked_add(tile.data_length as usize) else {
1625            continue;
1626        };
1627        if end > ctx.file.len() {
1628            continue;
1629        }
1630        let hvcc = if !tile.hvcc.is_empty() {
1631            &tile.hvcc
1632        } else {
1633            ctx.fallback_hvcc
1634        };
1635        if hvcc.is_empty() {
1636            continue;
1637        }
1638        let Some((yuv, _)) = optional_auxiliary(decode_hevc_item(
1639            &ctx.file[start..end],
1640            hvcc,
1641            ctx.exec,
1642            None,
1643        ))?
1644        else {
1645            continue;
1646        };
1647
1648        let dst_x = col * ctx.tile_w;
1649        let copy_w = ctx.tile_w.min(ctx.out_w.saturating_sub(dst_x));
1650        let copy_h = ctx.tile_h.min(ctx.out_h.saturating_sub(dst_y_base));
1651        if copy_w == 0 || copy_h == 0 {
1652            continue;
1653        }
1654
1655        let tile_buf = yuv::yuv_to_rgb_window_with_color_pool(
1656            &yuv,
1657            ctx.tile_w,
1658            ctx.tile_h,
1659            ctx.tile_crop_left,
1660            ctx.tile_crop_top,
1661            ctx.color_enc,
1662            None,
1663        )?;
1664        let src = match pick(&tile_buf) {
1665            Some(samples) => samples,
1666            None => continue,
1667        };
1668        let channels = ctx.channels;
1669        for y in 0..copy_h {
1670            let src_start = y * ctx.tile_w * channels;
1671            let dst_start = (y * ctx.out_w + dst_x) * channels;
1672            band[dst_start..dst_start + copy_w * channels]
1673                .copy_from_slice(&src[src_start..src_start + copy_w * channels]);
1674        }
1675    }
1676    Ok(())
1677}
1678
1679/// Allocate the output plane and fill each of the `rows` horizontal bands.
1680fn fill_grid_bands<T: Copy + Default + Send>(
1681    decoder: &Decoder,
1682    total: usize,
1683    rows: usize,
1684    band_stride: usize,
1685    ctx: &GridCtx<'_>,
1686    pick: &(dyn Fn(&ImageBuffer) -> Option<&[T]> + Sync),
1687) -> Result<Vec<T>, DecodeError> {
1688    let pool = decoder.pool();
1689    if pool.threads() > 1 && rows > 1 {
1690        let output = threadpool::DisjointMut::new(try_vec![T::default(); total, "grid RGB output"]);
1691        let error = std::sync::Mutex::new(None);
1692        threadpool::parallel_for(pool, rows, |row| {
1693            if error.lock().unwrap().is_some() {
1694                return;
1695            }
1696            let lo = row * band_stride;
1697            let hi = ((row + 1) * band_stride).min(total);
1698            if lo >= hi {
1699                return;
1700            }
1701            let mut band = output.slice_mut(lo..hi);
1702            if let Err(err) = stitch_grid_band(ctx, row, &mut band, pick) {
1703                *error.lock().unwrap() = Some(err);
1704            }
1705        });
1706        if let Some(err) = error.into_inner().unwrap() {
1707            return Err(err);
1708        }
1709        return Ok(output.into_inner());
1710    }
1711
1712    let mut output = try_vec![T::default(); total, "grid RGB output"];
1713    for row in 0..rows {
1714        let lo = row * band_stride;
1715        let hi = ((row + 1) * band_stride).min(total);
1716        if lo < hi {
1717            stitch_grid_band(ctx, row, &mut output[lo..hi], pick)?;
1718        }
1719    }
1720    Ok(output)
1721}
1722
1723/// Decode a tiled (grid) HEIC: decode each tile independently then stitch.
1724fn decode_grid(
1725    decoder: &Decoder,
1726    file: &[u8],
1727    grid: &heif::GridInfo,
1728    heif_file: &heif::HeifFile,
1729    decode_auxiliary_images: bool,
1730) -> Result<DecodedImage, DecodeError> {
1731    let out_w = grid.output_width as usize;
1732    let out_h = grid.output_height as usize;
1733    if out_w == 0 || out_h == 0 || grid.tiles.is_empty() {
1734        return Err(DecodeError::Bitstream(
1735            "grid has zero size or no tiles".into(),
1736        ));
1737    }
1738    decoder.check_dims(out_w, out_h)?;
1739
1740    let cols = grid.cols as usize;
1741    let rows = grid.rows as usize;
1742
1743    // Apple HEIC sometimes doesn't per-associate hvcC via ipma; all tiles share
1744    // the same SPS/PPS so reusing the first non-empty one is correct.
1745    let fallback_hvcc: Vec<u8> = grid
1746        .tiles
1747        .iter()
1748        .find(|t| !t.hvcc.is_empty())
1749        .map(|t| t.hvcc.clone())
1750        .unwrap_or_default();
1751
1752    // Priority: (1) SPS conformance window, (2) ispe (sanity-checked),
1753    // (3) infer from grid layout.
1754    let (tile_w, tile_h) = {
1755        let hvcc_ref = if !grid.tiles[0].hvcc.is_empty() {
1756            &grid.tiles[0].hvcc
1757        } else {
1758            &fallback_hvcc
1759        };
1760        let from_sps = if !hvcc_ref.is_empty() {
1761            config::parse_hvcc_full(hvcc_ref).ok().and_then(|(sps, _)| {
1762                let w = sps.width.saturating_sub(sps.crop_left + sps.crop_right) as usize;
1763                let h = sps.height.saturating_sub(sps.crop_top + sps.crop_bottom) as usize;
1764                if w > 0 && h > 0 { Some((w, h)) } else { None }
1765            })
1766        } else {
1767            None
1768        };
1769
1770        from_sps
1771            .or_else(|| {
1772                let t = &grid.tiles[0];
1773                if t.display_w > 0 && t.display_h > 0 {
1774                    // Reject if ispe equals the full output size — means the tile
1775                    // accidentally inherited the grid's ispe property.
1776                    let same_as_output = t.display_w == grid.output_width
1777                        && t.display_h == grid.output_height
1778                        && (cols > 1 || rows > 1);
1779                    if !same_as_output {
1780                        Some((t.display_w as usize, t.display_h as usize))
1781                    } else {
1782                        None
1783                    }
1784                } else {
1785                    None
1786                }
1787            })
1788            .unwrap_or_else(|| {
1789                let tw = out_w.div_ceil(cols);
1790                let th = out_h.div_ceil(rows);
1791                (tw, th)
1792            })
1793    };
1794
1795    if tile_w == 0 || tile_h == 0 {
1796        return Err(DecodeError::Bitstream(
1797            "cannot determine tile dimensions".into(),
1798        ));
1799    }
1800
1801    let color_enc = {
1802        let hvcc_ref = if !grid.tiles[0].hvcc.is_empty() {
1803            &grid.tiles[0].hvcc
1804        } else {
1805            &fallback_hvcc
1806        };
1807        if let Ok((sps, _)) = config::parse_hvcc_full(hvcc_ref) {
1808            Cicp {
1809                primaries: Primaries::from_u8(sps.color_primaries),
1810                transfer: TransferFunction::from_u8(sps.transfer_characteristics),
1811                matrix: MatrixCoefficients::from_u8(sps.matrix_coefficients),
1812                full_range: sps.video_full_range,
1813            }
1814        } else {
1815            heif_file.primary.color.cicp.unwrap_or_else(Cicp::srgb)
1816        }
1817    };
1818
1819    let bit_depth = {
1820        let hvcc_ref = if !grid.tiles[0].hvcc.is_empty() {
1821            &grid.tiles[0].hvcc
1822        } else {
1823            &fallback_hvcc
1824        };
1825        config::parse_hvcc_full(hvcc_ref)
1826            .ok()
1827            .map(|(sps, _)| match sps.bit_depth_luma {
1828                10 => BitDepth::Ten,
1829                12 => BitDepth::Twelve,
1830                _ => BitDepth::Eight,
1831            })
1832            .unwrap_or(BitDepth::Eight)
1833    };
1834
1835    let is_mono = {
1836        let hvcc_ref = if !grid.tiles[0].hvcc.is_empty() {
1837            &grid.tiles[0].hvcc
1838        } else {
1839            &fallback_hvcc
1840        };
1841        config::parse_hvcc_full(hvcc_ref)
1842            .ok()
1843            .map(|(sps, _)| sps.chroma.is_monochrome())
1844            .unwrap_or(false)
1845    };
1846
1847    let tile_crop = {
1848        let hvcc_ref = if !grid.tiles[0].hvcc.is_empty() {
1849            &grid.tiles[0].hvcc
1850        } else {
1851            &fallback_hvcc
1852        };
1853        visible_crop_from_hvcc(hvcc_ref)
1854    };
1855
1856    let channels = if is_mono { 1 } else { 3 };
1857
1858    // Bundle everything a band needs so the (possibly parallel) worker closures
1859    // stay small and capture only a shared `&`.
1860    let ctx = GridCtx {
1861        file,
1862        exec: decoder.exec(),
1863        tiles: &grid.tiles,
1864        fallback_hvcc: &fallback_hvcc,
1865        color_enc: &color_enc,
1866        cols,
1867        out_w,
1868        out_h,
1869        tile_w,
1870        tile_h,
1871        tile_crop_left: tile_crop.left,
1872        tile_crop_top: tile_crop.top,
1873        channels,
1874    };
1875
1876    // Rows are split into `rows` horizontal bands; band `r` owns the contiguous
1877    // output range `[r*band_stride, (r+1)*band_stride)`. Bands are disjoint, so
1878    // they can be filled concurrently. The `pick` closure selects the source
1879    // samples of the matching depth from each per-tile RGB/luma buffer.
1880    let band_stride = tile_h * out_w * channels;
1881
1882    let out_buf = if bit_depth == BitDepth::Eight {
1883        let v = fill_grid_bands::<u8>(
1884            decoder,
1885            out_w * out_h * channels,
1886            rows,
1887            band_stride,
1888            &ctx,
1889            &|b| match b {
1890                ImageBuffer::Rgb8(s) | ImageBuffer::Luma8(s) => Some(s.as_slice()),
1891                _ => None,
1892            },
1893        )?;
1894        if is_mono {
1895            ImageBuffer::Luma8(v)
1896        } else {
1897            ImageBuffer::Rgb8(v)
1898        }
1899    } else {
1900        let v = fill_grid_bands::<u16>(
1901            decoder,
1902            out_w * out_h * channels,
1903            rows,
1904            band_stride,
1905            &ctx,
1906            &|b| match b {
1907                ImageBuffer::Rgb16(s) | ImageBuffer::Luma16(s) => Some(s.as_slice()),
1908                _ => None,
1909            },
1910        )?;
1911        if is_mono {
1912            ImageBuffer::Luma16(v)
1913        } else {
1914            ImageBuffer::Rgb16(v)
1915        }
1916    };
1917
1918    let alpha = if decode_auxiliary_images && decoder.decodes_alpha() {
1919        decode_alpha_plane(decoder, file, heif_file, out_w, out_h)?
1920    } else {
1921        None
1922    };
1923    let gain_map = if decode_auxiliary_images && decoder.decodes_gain_map() {
1924        decode_gain_map(decoder, file, heif_file, true)?
1925    } else {
1926        None
1927    };
1928    let oriented = apply_orientation(out_w as u32, out_h as u32, out_buf, alpha, grid.orientation)?;
1929
1930    Ok(DecodedImage {
1931        width: oriented.width,
1932        height: oriented.height,
1933        pixels: oriented.pixels,
1934        alpha: oriented.alpha,
1935        gain_map,
1936        bit_depth,
1937        color: heif_file.primary.color.clone(),
1938        // store grid.orientation so the caller knows what rotation was applied
1939        // (pixels have already been rotated by apply_orientation above)
1940        orientation: grid.orientation,
1941        content_light_level: heif_file.primary.cll,
1942        clean_aperture: heif_file.primary.clap,
1943        pixel_aspect_ratio: heif_file.primary.pasp,
1944        exif: heif_file.exif.clone(),
1945    })
1946}
1947
1948/// Decode to display-ready packed 8-bit RGB using a default [`Decoder`].
1949/// Monochrome images are expanded to gray RGB. The returned buffer always has
1950/// exactly three bytes per pixel.
1951pub fn decode_heic_rgb8(file: &[u8]) -> Result<Rgb8Image, DecodeError> {
1952    Decoder::default().decode_rgb8(file)
1953}
1954
1955/// Decode packed RGB8 with one explicit settings value. This output cannot
1956/// expose auxiliary planes, so alpha and gain-map payloads are never decoded.
1957pub fn decode_heic_rgb8_with_settings(
1958    file: &[u8],
1959    settings: &HeicSettings,
1960) -> Result<Rgb8Image, DecodeError> {
1961    Decoder::from_settings(*settings).decode_rgb8(file)
1962}
1963
1964pub(crate) fn decode_heic_rgb8_with(
1965    decoder: &Decoder,
1966    file: &[u8],
1967) -> Result<Rgb8Image, DecodeError> {
1968    let img = decode_heic_impl(decoder, file, false)?;
1969    let shift = img.bit_depth.minus8();
1970    let pixels = match img.pixels {
1971        ImageBuffer::Rgb8(pixels) => pixels,
1972        ImageBuffer::Rgb16(samples) => {
1973            let mut pixels = try_vec![0u8; samples.len(), "RGB8 output"];
1974            for (dst, sample) in pixels.iter_mut().zip(samples) {
1975                *dst = (sample >> shift) as u8;
1976            }
1977            pixels
1978        }
1979        ImageBuffer::Luma8(samples) => {
1980            let len = samples.len().checked_mul(3).ok_or_else(|| {
1981                DecodeError::Bitstream("RGB8 output length overflows usize".into())
1982            })?;
1983            let mut pixels = try_vec![0u8; len, "RGB8 output"];
1984            for (luma, rgb) in samples.into_iter().zip(pixels.as_chunks_mut::<3>().0) {
1985                *rgb = [luma; 3];
1986            }
1987            pixels
1988        }
1989        ImageBuffer::Luma16(samples) => {
1990            let len = samples.len().checked_mul(3).ok_or_else(|| {
1991                DecodeError::Bitstream("RGB8 output length overflows usize".into())
1992            })?;
1993            let mut pixels = try_vec![0u8; len, "RGB8 output"];
1994            for (luma, rgb) in samples.into_iter().zip(pixels.as_chunks_mut::<3>().0) {
1995                *rgb = [(luma >> shift) as u8; 3];
1996            }
1997            pixels
1998        }
1999    };
2000    Ok(Rgb8Image {
2001        pixels,
2002        width: img.width,
2003        height: img.height,
2004    })
2005}
2006
2007fn oriented_dimensions(width: u32, height: u32, orientation: Orientation) -> (u32, u32) {
2008    match orientation {
2009        Orientation::Rotate90
2010        | Orientation::Rotate270
2011        | Orientation::Transverse
2012        | Orientation::Transpose => (height, width),
2013        _ => (width, height),
2014    }
2015}
2016
2017fn apply_orientation(
2018    width: u32,
2019    height: u32,
2020    pixels: ImageBuffer,
2021    alpha: Option<SampleBuf>,
2022    orientation: Orientation,
2023) -> Result<OrientedImage, DecodeError> {
2024    let (oriented_width, oriented_height) = oriented_dimensions(width, height, orientation);
2025    let pixels = match pixels {
2026        ImageBuffer::Luma8(samples) => ImageBuffer::Luma8(rotate_luma(
2027            width as usize,
2028            height as usize,
2029            samples,
2030            orientation,
2031        )?),
2032        ImageBuffer::Luma16(samples) => ImageBuffer::Luma16(rotate_luma(
2033            width as usize,
2034            height as usize,
2035            samples,
2036            orientation,
2037        )?),
2038        ImageBuffer::Rgb8(samples) => ImageBuffer::Rgb8(rotate_buf(
2039            width as usize,
2040            height as usize,
2041            samples,
2042            orientation,
2043        )?),
2044        ImageBuffer::Rgb16(samples) => ImageBuffer::Rgb16(rotate_buf(
2045            width as usize,
2046            height as usize,
2047            samples,
2048            orientation,
2049        )?),
2050    };
2051    let alpha = alpha
2052        .map(|samples| rotate_sample_buf(samples, width, height, orientation))
2053        .transpose()?;
2054    Ok(OrientedImage {
2055        width: oriented_width,
2056        height: oriented_height,
2057        pixels,
2058        alpha,
2059    })
2060}
2061
2062/// Single-channel rotation for luma-only buffers. Normal and 180° orientation
2063/// reuse the original allocation; orientations requiring a new layout allocate
2064/// through `try_vec!`.
2065fn rotate_luma<T: Copy + Default>(
2066    w: usize,
2067    h: usize,
2068    mut pixels: Vec<T>,
2069    orientation: Orientation,
2070) -> Result<Vec<T>, DecodeError> {
2071    match orientation {
2072        Orientation::Normal => Ok(pixels),
2073        Orientation::Rotate180 => {
2074            pixels.reverse();
2075            Ok(pixels)
2076        }
2077        Orientation::FlipH => {
2078            let mut out = try_vec![T::default(); pixels.len(), "oriented luma image"];
2079            for (src_row, dst_row) in pixels.chunks_exact(w).zip(out.chunks_exact_mut(w)) {
2080                for (dst, &src) in dst_row.iter_mut().rev().zip(src_row) {
2081                    *dst = src;
2082                }
2083            }
2084            Ok(out)
2085        }
2086        Orientation::FlipV => {
2087            let mut out = try_vec![T::default(); pixels.len(), "oriented luma image"];
2088            for (src_row, dst_row) in pixels.chunks_exact(w).zip(out.chunks_exact_mut(w).rev()) {
2089                dst_row.copy_from_slice(src_row);
2090            }
2091            Ok(out)
2092        }
2093        Orientation::Rotate90 => {
2094            let mut out = try_vec![T::default(); pixels.len(), "oriented luma image"];
2095            for row in 0..h {
2096                for col in 0..w {
2097                    out[col * h + (h - 1 - row)] = pixels[row * w + col];
2098                }
2099            }
2100            Ok(out)
2101        }
2102        Orientation::Rotate270 => {
2103            let mut out = try_vec![T::default(); pixels.len(), "oriented luma image"];
2104            for row in 0..h {
2105                for col in 0..w {
2106                    out[(w - 1 - col) * h + row] = pixels[row * w + col];
2107                }
2108            }
2109            Ok(out)
2110        }
2111        _ => Ok(pixels),
2112    }
2113}
2114
2115/// Generic three-channel pixel-buffer rotation.
2116fn rotate_buf<T: Copy + Default>(
2117    w: usize,
2118    h: usize,
2119    mut pixels: Vec<T>,
2120    orientation: Orientation,
2121) -> Result<Vec<T>, DecodeError> {
2122    match orientation {
2123        Orientation::Normal => Ok(pixels),
2124        Orientation::Rotate180 => {
2125            let count = pixels.len() / 3;
2126            for left in 0..count / 2 {
2127                let right = count - 1 - left;
2128                for channel in 0..3 {
2129                    pixels.swap(left * 3 + channel, right * 3 + channel);
2130                }
2131            }
2132            Ok(pixels)
2133        }
2134        Orientation::FlipH => {
2135            let mut out = try_vec![T::default(); pixels.len(), "oriented RGB image"];
2136            for (src_row, dst_row) in pixels.chunks_exact(w * 3).zip(out.chunks_exact_mut(w * 3)) {
2137                for (src, dst) in src_row
2138                    .as_chunks::<3>()
2139                    .0
2140                    .iter()
2141                    .rev()
2142                    .zip(dst_row.as_chunks_mut::<3>().0.iter_mut())
2143                {
2144                    dst.copy_from_slice(src);
2145                }
2146            }
2147            Ok(out)
2148        }
2149        Orientation::FlipV => {
2150            let mut out = try_vec![T::default(); pixels.len(), "oriented RGB image"];
2151            for (src_row, dst_row) in pixels
2152                .chunks_exact(w * 3)
2153                .rev()
2154                .zip(out.chunks_exact_mut(w * 3))
2155            {
2156                dst_row.copy_from_slice(src_row);
2157            }
2158            Ok(out)
2159        }
2160        Orientation::Rotate90 => {
2161            let mut out = try_vec![T::default(); pixels.len(), "oriented RGB image"];
2162            const BLOCK: usize = 32;
2163            let mut row_base = 0;
2164            while row_base < h {
2165                let row_end = (row_base + BLOCK).min(h);
2166                let mut col_base = 0;
2167                while col_base < w {
2168                    let col_end = (col_base + BLOCK).min(w);
2169                    for row in row_base..row_end {
2170                        let src_row = row * w * 3;
2171                        let dst_col = h - 1 - row;
2172                        for col in col_base..col_end {
2173                            let src = src_row + col * 3;
2174                            let dst = (col * h + dst_col) * 3;
2175                            out[dst..dst + 3].copy_from_slice(&pixels[src..src + 3]);
2176                        }
2177                    }
2178                    col_base = col_end;
2179                }
2180                row_base = row_end;
2181            }
2182            Ok(out)
2183        }
2184        Orientation::Rotate270 => {
2185            let mut out = try_vec![T::default(); pixels.len(), "oriented RGB image"];
2186            const BLOCK: usize = 32;
2187            let mut row_base = 0;
2188            while row_base < h {
2189                let row_end = (row_base + BLOCK).min(h);
2190                let mut col_base = 0;
2191                while col_base < w {
2192                    let col_end = (col_base + BLOCK).min(w);
2193                    for row in row_base..row_end {
2194                        let src_row = row * w * 3;
2195                        for col in col_base..col_end {
2196                            let src = src_row + col * 3;
2197                            let dst = ((w - 1 - col) * h + row) * 3;
2198                            out[dst..dst + 3].copy_from_slice(&pixels[src..src + 3]);
2199                        }
2200                    }
2201                    col_base = col_end;
2202                }
2203                row_base = row_end;
2204            }
2205            Ok(out)
2206        }
2207        _ => Ok(pixels),
2208    }
2209}