1#![allow(unreachable_pub)]
31
32macro_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#[derive(Clone, Debug)]
277pub struct GainMap {
278 pub width: u32,
280 pub height: u32,
281 pub chroma_width: u32,
283 pub chroma_height: u32,
284 pub y: SampleBuf,
286 pub cb: Option<SampleBuf>,
288 pub cr: Option<SampleBuf>,
290 pub chroma: ChromaFormat,
294 pub bit_depth: BitDepth,
295 pub color: ColorMetadata,
298 pub orientation: Orientation,
302 pub metadata: Option<Vec<u8>>,
306}
307
308impl GainMap {
309 #[inline]
311 pub fn channels(&self) -> usize {
312 if self.chroma.is_monochrome() { 1 } else { 3 }
313 }
314}
315
316#[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#[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#[derive(Clone, Debug)]
430pub struct Rgb8Image {
431 pub pixels: Vec<u8>,
433 pub width: u32,
434 pub height: u32,
435}
436
437pub struct DecodedImage {
439 pub width: u32,
440 pub height: u32,
441 pub pixels: ImageBuffer,
443 pub alpha: Option<SampleBuf>,
444 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 pub clean_aperture: Option<CleanAperture>,
452 pub pixel_aspect_ratio: Option<PixelAspectRatio>,
454 pub exif: Option<Vec<u8>>,
455}
456
457fn 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
467fn 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
528fn 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
747pub fn decode_heic_yuv(file: &[u8]) -> Result<DecodedYuv, DecodeError> {
751 Decoder::default().decode_yuv(file)
752}
753
754pub 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
827struct 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
1174fn 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 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 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 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 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 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
1475pub fn decode_heic(file: &[u8]) -> Result<DecodedImage, DecodeError> {
1477 Decoder::default().decode(file)
1478}
1479
1480pub 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 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
1587struct 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
1605fn 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
1679fn 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
1723fn 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 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 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 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 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 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 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
1948pub fn decode_heic_rgb8(file: &[u8]) -> Result<Rgb8Image, DecodeError> {
1952 Decoder::default().decode_rgb8(file)
1953}
1954
1955pub 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
2062fn 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
2115fn 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}