1use std::time::Duration;
51
52use crate::error::{AvifError as Error, Result};
53use crate::image::{AvifFrame, AvifPixelFormat, AvifPlane};
54use crate::meta::Colr;
55use oxideav_heif::props as hprops;
56
57pub type PixelFormat = AvifPixelFormat;
60
61pub type Plane = AvifPlane;
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
71pub enum ColorRange {
72 #[default]
76 Unspecified,
77 Limited,
79 Full,
81}
82
83impl ColorRange {
84 pub fn is_full(self) -> bool {
87 self != ColorRange::Limited
88 }
89
90 pub fn from_flag(full_range: bool) -> Self {
92 if full_range {
93 ColorRange::Full
94 } else {
95 ColorRange::Limited
96 }
97 }
98}
99
100#[non_exhaustive]
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub struct ColorInfo {
111 pub range: ColorRange,
113 pub primaries: u8,
116 pub transfer: u8,
119 pub matrix: u8,
122}
123
124impl ColorInfo {
125 pub const fn new(range: ColorRange, primaries: u8, transfer: u8, matrix: u8) -> Self {
127 Self {
128 range,
129 primaries,
130 transfer,
131 matrix,
132 }
133 }
134
135 pub const fn miaf_default() -> Self {
138 Self::new(ColorRange::Full, 1, 13, 6)
139 }
140
141 pub const fn identity_full_range() -> Self {
145 Self::new(ColorRange::Full, 1, 13, 0)
146 }
147
148 pub fn from_colr(colr: Option<&Colr>) -> Self {
153 match colr {
154 Some(Colr::Nclx {
155 colour_primaries,
156 transfer_characteristics,
157 matrix_coefficients,
158 full_range,
159 }) => {
160 let code = |v: u16| u8::try_from(v).unwrap_or(2);
161 Self::new(
162 ColorRange::from_flag(*full_range),
163 code(*colour_primaries),
164 code(*transfer_characteristics),
165 code(*matrix_coefficients),
166 )
167 }
168 _ => Self::miaf_default(),
169 }
170 }
171
172 pub fn to_colr(&self) -> Colr {
174 Colr::Nclx {
175 colour_primaries: u16::from(self.primaries),
176 transfer_characteristics: u16::from(self.transfer),
177 matrix_coefficients: u16::from(self.matrix),
178 full_range: self.range.is_full(),
179 }
180 }
181
182 pub fn is_identity_matrix(&self) -> bool {
184 self.matrix == 0
185 }
186
187 pub fn with_range(mut self, range: ColorRange) -> Self {
189 self.range = range;
190 self
191 }
192 pub fn with_primaries(mut self, primaries: u8) -> Self {
194 self.primaries = primaries;
195 self
196 }
197 pub fn with_transfer(mut self, transfer: u8) -> Self {
199 self.transfer = transfer;
200 self
201 }
202 pub fn with_matrix(mut self, matrix: u8) -> Self {
204 self.matrix = matrix;
205 self
206 }
207
208 fn to_heif_colr(self) -> hprops::Colr {
209 hprops::Colr::from(&self.to_colr())
210 }
211}
212
213impl Default for ColorInfo {
214 fn default() -> Self {
215 Self::miaf_default()
216 }
217}
218
219#[non_exhaustive]
230#[derive(Clone, Debug, Default, PartialEq)]
231pub struct Metadata {
232 pub icc: Option<Vec<u8>>,
233 pub exif: Option<Vec<u8>>,
234 pub xmp: Option<Vec<u8>>,
235 pub gamma: Option<f32>,
236}
237
238impl Metadata {
239 pub fn new(
241 icc: Option<Vec<u8>>,
242 exif: Option<Vec<u8>>,
243 xmp: Option<Vec<u8>>,
244 gamma: Option<f32>,
245 ) -> Self {
246 Self {
247 icc,
248 exif,
249 xmp,
250 gamma,
251 }
252 }
253 pub fn with_icc(mut self, icc: Option<Vec<u8>>) -> Self {
255 self.icc = icc;
256 self
257 }
258 pub fn with_exif(mut self, exif: Option<Vec<u8>>) -> Self {
260 self.exif = exif;
261 self
262 }
263 pub fn with_xmp(mut self, xmp: Option<Vec<u8>>) -> Self {
265 self.xmp = xmp;
266 self
267 }
268 pub fn with_gamma(mut self, gamma: Option<f32>) -> Self {
270 self.gamma = gamma;
271 self
272 }
273
274 pub fn is_empty(&self) -> bool {
276 self.icc.is_none() && self.exif.is_none() && self.xmp.is_none() && self.gamma.is_none()
277 }
278}
279
280#[cfg_attr(not(feature = "registry"), allow(dead_code))]
285pub(crate) fn exif_tiff_bytes(body: &[u8]) -> Option<Vec<u8>> {
286 if body.len() < 4 {
287 return None;
288 }
289 let offset = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
290 let start = 4usize.checked_add(offset)?;
291 if start > body.len() {
292 return None;
293 }
294 Some(body[start..].to_vec())
295}
296
297#[cfg_attr(not(feature = "registry"), allow(dead_code))]
300pub(crate) fn exif_item_body(tiff: &[u8]) -> Vec<u8> {
301 let mut out = Vec::with_capacity(4 + tiff.len());
302 out.extend_from_slice(&[0, 0, 0, 0]);
303 out.extend_from_slice(tiff);
304 out
305}
306
307#[non_exhaustive]
314#[derive(Clone, Debug, PartialEq, Eq)]
315pub struct RgbImage {
316 pub width: u32,
317 pub height: u32,
318 pub data: Vec<u8>,
320}
321
322impl RgbImage {
323 pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
325 Self {
326 width,
327 height,
328 data,
329 }
330 }
331 pub fn as_bytes(&self) -> &[u8] {
333 &self.data
334 }
335 pub fn into_raw(self) -> Vec<u8> {
337 self.data
338 }
339}
340
341#[non_exhaustive]
344#[derive(Clone, Debug, PartialEq, Eq)]
345pub struct RgbaImage {
346 pub width: u32,
347 pub height: u32,
348 pub data: Vec<u8>,
350}
351
352impl RgbaImage {
353 pub fn new(width: u32, height: u32, data: Vec<u8>) -> Self {
355 Self {
356 width,
357 height,
358 data,
359 }
360 }
361 pub fn as_bytes(&self) -> &[u8] {
363 &self.data
364 }
365 pub fn into_raw(self) -> Vec<u8> {
367 self.data
368 }
369}
370
371#[non_exhaustive]
388#[derive(Clone, Debug, PartialEq)]
389pub struct AvifImage {
390 pub width: u32,
391 pub height: u32,
392 pub format: PixelFormat,
394 pub planes: Vec<Plane>,
396 pub color: ColorInfo,
398 pub metadata: Metadata,
400 pub bit_depth: u8,
404}
405
406impl AvifImage {
407 pub fn new(width: u32, height: u32, format: PixelFormat, planes: Vec<Plane>) -> Result<Self> {
415 let img = Self {
416 width,
417 height,
418 format,
419 planes,
420 color: ColorInfo::default(),
421 metadata: Metadata::default(),
422 bit_depth: format.bit_depth(),
423 };
424 img.validate_geometry()?;
425 Ok(img)
426 }
427
428 pub(crate) fn validate_geometry(&self) -> Result<()> {
432 if self.width == 0 || self.height == 0 {
433 return Err(Error::invalid(
434 "avif: picture dimensions must be at least 1x1",
435 ));
436 }
437 let expect = self.format.plane_count();
438 if self.planes.len() != expect {
439 return Err(Error::invalid(format!(
440 "avif: {:?} needs {expect} planes, picture carries {}",
441 self.format,
442 self.planes.len()
443 )));
444 }
445 let bps = self.format.bytes_per_sample();
446 let (sx, sy) = self.format.chroma_subsampling();
447 let (w, h) = (self.width as usize, self.height as usize);
448 for (i, plane) in self.planes.iter().enumerate() {
449 let (cols, rows) = if self.format.is_packed_ya() {
450 (w * 2, h)
451 } else if i == 1 || i == 2 {
452 (w.div_ceil(1 << sx), h.div_ceil(1 << sy))
453 } else {
454 (w, h)
455 };
456 let row_bytes = cols * bps;
457 let need = plane
458 .stride
459 .checked_mul(rows - 1)
460 .and_then(|v| v.checked_add(row_bytes));
461 let short = match need {
462 Some(n) => plane.data.len() < n,
463 None => true,
464 };
465 if plane.stride < row_bytes || short {
466 return Err(Error::invalid(format!(
467 "avif: plane {i} ({} bytes, stride {}) does not cover {rows} rows of \
468 {row_bytes} bytes",
469 plane.data.len(),
470 plane.stride
471 )));
472 }
473 }
474 Ok(())
475 }
476
477 pub fn from_rgb8(width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
483 let n = width as usize * height as usize;
484 if width == 0 || height == 0 || data.len() != n * 3 {
485 return Err(Error::invalid(format!(
486 "avif: from_rgb8 {width}x{height} needs {} RGB bytes, got {}",
487 n * 3,
488 data.len()
489 )));
490 }
491 let mut g = Vec::with_capacity(n);
492 let mut b = Vec::with_capacity(n);
493 let mut r = Vec::with_capacity(n);
494 for px in data.chunks_exact(3) {
495 r.push(px[0]);
496 g.push(px[1]);
497 b.push(px[2]);
498 }
499 let w = width as usize;
500 let planes = vec![Plane::new(w, g), Plane::new(w, b), Plane::new(w, r)];
501 Ok(Self::new(width, height, PixelFormat::Yuv444P, planes)?
502 .with_color(ColorInfo::identity_full_range()))
503 }
504
505 pub fn from_rgba8(width: u32, height: u32, data: Vec<u8>) -> Result<Self> {
511 let n = width as usize * height as usize;
512 if width == 0 || height == 0 || data.len() != n * 4 {
513 return Err(Error::invalid(format!(
514 "avif: from_rgba8 {width}x{height} needs {} RGBA bytes, got {}",
515 n * 4,
516 data.len()
517 )));
518 }
519 let mut g = Vec::with_capacity(n);
520 let mut b = Vec::with_capacity(n);
521 let mut r = Vec::with_capacity(n);
522 let mut a = Vec::with_capacity(n);
523 for px in data.chunks_exact(4) {
524 r.push(px[0]);
525 g.push(px[1]);
526 b.push(px[2]);
527 a.push(px[3]);
528 }
529 let w = width as usize;
530 let planes = vec![
531 Plane::new(w, g),
532 Plane::new(w, b),
533 Plane::new(w, r),
534 Plane::new(w, a),
535 ];
536 Ok(Self::new(width, height, PixelFormat::Yuva444P, planes)?
537 .with_color(ColorInfo::identity_full_range()))
538 }
539
540 pub fn with_color(mut self, color: ColorInfo) -> Self {
542 self.color = color;
543 self
544 }
545 pub fn with_metadata(mut self, metadata: Metadata) -> Self {
547 self.metadata = metadata;
548 self
549 }
550 pub fn with_bit_depth(mut self, bit_depth: u8) -> Self {
553 self.bit_depth = bit_depth;
554 self
555 }
556
557 pub fn width(&self) -> u32 {
559 self.width
560 }
561 pub fn height(&self) -> u32 {
563 self.height
564 }
565 pub fn format(&self) -> PixelFormat {
567 self.format
568 }
569 pub fn bit_depth(&self) -> u8 {
571 self.bit_depth
572 }
573 pub fn has_alpha(&self) -> bool {
575 self.format.has_alpha()
576 }
577
578 pub fn as_bytes(&self) -> Option<&[u8]> {
582 if self.planes.len() == 1 {
583 self.planes.first().map(|p| p.data.as_slice())
584 } else {
585 None
586 }
587 }
588
589 pub fn into_raw(self) -> Vec<u8> {
592 let total: usize = self.planes.iter().map(|p| p.data.len()).sum();
593 let mut out = Vec::with_capacity(total);
594 for p in self.planes {
595 out.extend_from_slice(&p.data);
596 }
597 out
598 }
599
600 pub fn to_rgb8(&self) -> Vec<u8> {
617 self.rgb_bytes_or_bt601(3)
618 .expect("AvifImage::to_rgb8: planes inconsistent with width/height/format")
619 }
620
621 pub fn to_rgba8(&self) -> Vec<u8> {
628 self.rgb_bytes_or_bt601(4)
629 .expect("AvifImage::to_rgba8: planes inconsistent with width/height/format")
630 }
631
632 pub fn try_to_rgb8(&self) -> Result<Vec<u8>> {
636 self.rgb_bytes(3, self.color)
637 }
638
639 pub fn try_to_rgba8(&self) -> Result<Vec<u8>> {
641 self.rgb_bytes(4, self.color)
642 }
643
644 fn rgb_bytes_or_bt601(&self, channels: usize) -> Result<Vec<u8>> {
647 match self.rgb_bytes(channels, self.color) {
648 Err(Error::Unsupported(_)) => self.rgb_bytes(channels, self.color.with_matrix(6)),
649 other => other,
650 }
651 }
652
653 pub(crate) fn to_heif_frame(&self) -> Result<oxideav_heif::HeifFrame> {
656 let frame = AvifFrame::new(None, self.planes.clone());
657 crate::frame_bridge::to_heif(&frame, self.format, self.bit_depth, self.width, self.height)
658 }
659
660 fn rgb_bytes(&self, channels: usize, color: ColorInfo) -> Result<Vec<u8>> {
663 if !matches!(self.bit_depth, 8 | 10 | 12) {
664 return Err(Error::invalid(format!(
665 "avif: bit_depth {} is not 8, 10 or 12",
666 self.bit_depth
667 )));
668 }
669 let frame = self.to_heif_frame()?;
670 let colr = color.to_heif_colr();
671 let rgb = oxideav_heif::rgb::to_rgb(&frame, Some(&colr))?;
672 let n = self.width as usize * self.height as usize;
673 if rgb.channels < 3 || rgb.data.len() < n * rgb.channels {
674 return Err(Error::invalid(
675 "avif: RGB conversion returned fewer samples than the picture holds",
676 ));
677 }
678 let depth = rgb.bit_depth;
679 let max = (1u32 << depth) - 1;
680 let half = max / 2;
681 let to8 = |v: u16| -> u8 {
682 if depth == 8 {
683 v as u8
684 } else {
685 ((u32::from(v).min(max) * 255 + half) / max) as u8
686 }
687 };
688 let mut out = Vec::with_capacity(n * channels);
689 for px in rgb.data.chunks_exact(rgb.channels).take(n) {
690 out.push(to8(px[0]));
691 out.push(to8(px[1]));
692 out.push(to8(px[2]));
693 if channels == 4 {
694 out.push(if rgb.channels >= 4 { to8(px[3]) } else { 255 });
695 }
696 }
697 Ok(out)
698 }
699}
700
701#[non_exhaustive]
709#[derive(Clone, Debug, PartialEq)]
710pub struct Frame {
711 pub image: AvifImage,
712 pub delay: Option<Duration>,
715}
716
717impl Frame {
718 pub fn new(image: AvifImage, delay: Option<Duration>) -> Self {
720 Self { image, delay }
721 }
722}
723
724#[non_exhaustive]
727#[derive(Clone, Debug, PartialEq, Eq)]
728pub struct ImageInfo {
729 pub width: u32,
732 pub height: u32,
733 pub format: PixelFormat,
736 pub frames: u32,
739 pub has_alpha: bool,
740 pub color: ColorInfo,
742 pub has_icc: bool,
743 pub has_exif: bool,
744 pub has_xmp: bool,
745 pub bit_depth: u8,
747 pub is_sequence: bool,
749}
750
751impl ImageInfo {
752 #[allow(clippy::too_many_arguments)]
754 pub fn new(
755 width: u32,
756 height: u32,
757 format: PixelFormat,
758 frames: u32,
759 has_alpha: bool,
760 color: ColorInfo,
761 has_icc: bool,
762 has_exif: bool,
763 has_xmp: bool,
764 bit_depth: u8,
765 is_sequence: bool,
766 ) -> Self {
767 Self {
768 width,
769 height,
770 format,
771 frames,
772 has_alpha,
773 color,
774 has_icc,
775 has_exif,
776 has_xmp,
777 bit_depth,
778 is_sequence,
779 }
780 }
781}
782
783#[non_exhaustive]
787#[derive(Clone, Copy, Debug, PartialEq)]
788pub struct DecodeOptions {
789 pub max_width: Option<u32>,
792 pub max_height: Option<u32>,
794 pub max_pixels: Option<u64>,
797 pub max_bytes: Option<u64>,
800 pub strict: bool,
806 pub tone_mapped: bool,
811 pub reference_white_nits: f64,
814 pub layer: Option<u16>,
818 pub threads: Option<usize>,
824}
825
826impl Default for DecodeOptions {
827 fn default() -> Self {
828 Self {
829 max_width: Some(16_384),
830 max_height: Some(16_384),
831 max_pixels: Some(1 << 28),
832 max_bytes: Some(1 << 30),
833 strict: false,
834 tone_mapped: false,
835 reference_white_nits: oxideav_heif::gainmap::DEFAULT_HDR_REFERENCE_WHITE_NITS,
836 layer: None,
837 threads: None,
838 }
839 }
840}
841
842impl DecodeOptions {
843 pub fn new() -> Self {
845 Self::default()
846 }
847 pub fn with_max_width(mut self, max_width: Option<u32>) -> Self {
849 self.max_width = max_width;
850 self
851 }
852 pub fn with_max_height(mut self, max_height: Option<u32>) -> Self {
854 self.max_height = max_height;
855 self
856 }
857 pub fn with_max_pixels(mut self, max_pixels: Option<u64>) -> Self {
859 self.max_pixels = max_pixels;
860 self
861 }
862 pub fn with_max_bytes(mut self, max_bytes: Option<u64>) -> Self {
864 self.max_bytes = max_bytes;
865 self
866 }
867 pub fn with_strict(mut self, strict: bool) -> Self {
869 self.strict = strict;
870 self
871 }
872 pub fn with_tone_mapped(mut self, tone_mapped: bool) -> Self {
874 self.tone_mapped = tone_mapped;
875 self
876 }
877 pub fn with_reference_white_nits(mut self, reference_white_nits: f64) -> Self {
879 self.reference_white_nits = reference_white_nits;
880 self
881 }
882 pub fn with_layer(mut self, layer: Option<u16>) -> Self {
884 self.layer = layer;
885 self
886 }
887 pub fn with_threads(mut self, threads: Option<usize>) -> Self {
889 self.threads = threads;
890 self
891 }
892
893 pub fn worker_count(&self) -> usize {
895 self.threads.unwrap_or(1).max(1)
896 }
897
898 pub fn check_bytes(&self, len: usize) -> Result<()> {
901 if let Some(max) = self.max_bytes {
902 if len as u64 > max {
903 return Err(Error::limit(format!(
904 "avif: input is {len} bytes, more than the {max} allowed"
905 )));
906 }
907 }
908 Ok(())
909 }
910
911 pub fn check_dims(&self, width: u32, height: u32) -> Result<()> {
914 if self.max_width.is_some_and(|m| width > m) || self.max_height.is_some_and(|m| height > m)
915 {
916 return Err(Error::limit(format!(
917 "avif: picture is {width}x{height}, more than the {:?}x{:?} allowed",
918 self.max_width, self.max_height
919 )));
920 }
921 let pixels = u64::from(width) * u64::from(height);
922 if let Some(max) = self.max_pixels {
923 if pixels > max {
924 return Err(Error::limit(format!(
925 "avif: picture holds {pixels} pixels, more than the {max} allowed"
926 )));
927 }
928 }
929 Ok(())
930 }
931}
932
933#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
936pub enum StillChroma {
937 #[default]
940 Yuv420,
941 Yuv422,
945 Yuv444,
947 Monochrome,
949}
950
951#[cfg_attr(not(feature = "registry"), allow(dead_code))]
952impl StillChroma {
953 pub(crate) fn subsampling(self) -> (u32, u32) {
955 match self {
956 StillChroma::Yuv420 => (1, 1),
957 StillChroma::Yuv422 => (1, 0),
958 StillChroma::Yuv444 => (0, 0),
959 StillChroma::Monochrome => (0, 0), }
961 }
962
963 pub(crate) fn has_chroma(self) -> bool {
964 self != StillChroma::Monochrome
965 }
966}
967
968#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
971pub enum EncodeSpeed {
972 #[default]
976 Fast,
977 Balanced,
981 Thorough,
984}
985
986#[non_exhaustive]
992#[derive(Clone, Copy, Debug, PartialEq, Eq)]
993pub struct EncodeOptions {
994 pub base_q_idx: u8,
998 pub alpha_q_idx: u8,
1002 pub premultiplied_alpha: bool,
1006 pub chroma: StillChroma,
1010 pub embed_exif: bool,
1012 pub embed_xmp: bool,
1014 pub embed_icc: bool,
1017 pub speed: EncodeSpeed,
1019 pub threads: usize,
1023}
1024
1025impl Default for EncodeOptions {
1026 fn default() -> Self {
1027 Self {
1028 base_q_idx: 0,
1029 alpha_q_idx: 0,
1030 premultiplied_alpha: false,
1031 chroma: StillChroma::Yuv420,
1032 embed_exif: true,
1033 embed_xmp: true,
1034 embed_icc: true,
1035 speed: EncodeSpeed::Fast,
1036 threads: 1,
1037 }
1038 }
1039}
1040
1041impl EncodeOptions {
1042 pub fn new(base_q_idx: u8, alpha_q_idx: u8, premultiplied_alpha: bool) -> Self {
1046 Self {
1047 base_q_idx,
1048 alpha_q_idx,
1049 premultiplied_alpha,
1050 ..Self::default()
1051 }
1052 }
1053 pub fn with_base_q_idx(mut self, base_q_idx: u8) -> Self {
1055 self.base_q_idx = base_q_idx;
1056 self
1057 }
1058 pub fn with_alpha_q_idx(mut self, alpha_q_idx: u8) -> Self {
1060 self.alpha_q_idx = alpha_q_idx;
1061 self
1062 }
1063 pub fn with_premultiplied_alpha(mut self, premultiplied_alpha: bool) -> Self {
1065 self.premultiplied_alpha = premultiplied_alpha;
1066 self
1067 }
1068 pub fn with_chroma(mut self, chroma: StillChroma) -> Self {
1070 self.chroma = chroma;
1071 self
1072 }
1073 pub fn with_embed_exif(mut self, embed_exif: bool) -> Self {
1075 self.embed_exif = embed_exif;
1076 self
1077 }
1078 pub fn with_embed_xmp(mut self, embed_xmp: bool) -> Self {
1080 self.embed_xmp = embed_xmp;
1081 self
1082 }
1083 pub fn with_embed_icc(mut self, embed_icc: bool) -> Self {
1085 self.embed_icc = embed_icc;
1086 self
1087 }
1088 pub fn with_speed(mut self, speed: EncodeSpeed) -> Self {
1090 self.speed = speed;
1091 self
1092 }
1093 pub fn with_threads(mut self, threads: usize) -> Self {
1095 self.threads = threads.max(1);
1096 self
1097 }
1098
1099 pub fn with_quality(mut self, quality: u8) -> Self {
1104 let q = u32::from(quality.min(100));
1105 self.base_q_idx = (((100 - q) * 255 + 50) / 100) as u8;
1106 self
1107 }
1108}
1109
1110const BRAND_AVIF: [u8; 4] = *b"avif";
1115const BRAND_AVIS: [u8; 4] = *b"avis";
1116const BRAND_AVIO: [u8; 4] = *b"avio";
1117
1118pub fn probe(bytes: &[u8]) -> bool {
1123 if bytes.len() < 16 || &bytes[4..8] != b"ftyp" {
1124 return false;
1125 }
1126 let size32 = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
1127 let (size, brands_start) = match size32 {
1128 0 => (bytes.len(), 16),
1129 1 => {
1130 if bytes.len() < 24 {
1131 return false;
1132 }
1133 let large = u64::from_be_bytes([
1134 bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14],
1135 bytes[15],
1136 ]);
1137 (usize::try_from(large).unwrap_or(usize::MAX), 24)
1138 }
1139 n => (n, 16),
1140 };
1141 let major_at = brands_start - 8;
1142 if size < brands_start {
1143 return false;
1144 }
1145 let end = size.min(bytes.len());
1146 let is_avif = |b: &[u8]| b == BRAND_AVIF || b == BRAND_AVIS || b == BRAND_AVIO;
1147 if is_avif(&bytes[major_at..major_at + 4]) {
1148 return true;
1149 }
1150 bytes[brands_start..end].chunks_exact(4).any(is_avif)
1151}
1152
1153pub fn info(bytes: &[u8]) -> Result<ImageInfo> {
1158 let (ftyp_payload, _) = crate::box_parser::find_box(bytes, b"ftyp")?
1159 .ok_or_else(|| Error::invalid("avif: missing ftyp"))?;
1160 let (major, _minor, compat) = crate::parser::parse_ftyp(ftyp_payload)?;
1161 let brands = crate::parser::classify_brands(&major, &compat)?;
1162 if (brands.is_sequence || brands.has_msf1) && has_moov(bytes) {
1163 return sequence_info(bytes);
1164 }
1165 still_info(bytes)
1166}
1167
1168pub(crate) fn has_moov(bytes: &[u8]) -> bool {
1169 crate::box_parser::find_box(bytes, b"moov")
1170 .ok()
1171 .flatten()
1172 .is_some()
1173}
1174
1175pub(crate) fn item_colrs(
1179 meta: &crate::meta::Meta,
1180 item_id: u32,
1181) -> (Option<Colr>, Option<Vec<u8>>) {
1182 let mut nclx = None;
1183 let mut icc = None;
1184 for prop in meta.properties_for(item_id) {
1185 if let crate::meta::Property::Colr(c) = prop {
1186 match c {
1187 Colr::Nclx { .. } if nclx.is_none() => nclx = Some(c.clone()),
1188 Colr::Icc(bytes) if icc.is_none() => icc = Some(bytes.clone()),
1189 _ => {}
1190 }
1191 }
1192 }
1193 (nclx, icc)
1194}
1195
1196pub(crate) fn layout_of_info(
1199 bit_depth: u8,
1200 monochrome: bool,
1201 chroma: Option<(bool, bool)>,
1202 has_alpha: bool,
1203) -> Result<PixelFormat> {
1204 let (sx, sy) = match chroma {
1205 Some((x, y)) => (u8::from(x), u8::from(y)),
1206 None => (1, 1),
1207 };
1208 PixelFormat::from_layout(bit_depth, sx, sy, monochrome, has_alpha).ok_or_else(|| {
1209 Error::unsupported(format!(
1210 "avif: no pixel layout for {bit_depth}-bit subsampling ({sx}, {sy}) \
1211 monochrome={monochrome} alpha={has_alpha}"
1212 ))
1213 })
1214}
1215
1216pub(crate) fn burst_members(meta: &crate::meta::Meta, item_id: u32) -> Vec<u32> {
1219 meta.groups()
1220 .unwrap_or_default()
1221 .into_iter()
1222 .find(|g| g.is_burst() && g.entity_ids.contains(&item_id))
1223 .map(|g| g.entity_ids)
1224 .unwrap_or_default()
1225}
1226
1227fn still_info(bytes: &[u8]) -> Result<ImageInfo> {
1228 let hdr = crate::parser::parse_header(bytes)?;
1229 let primary_id = hdr
1230 .meta
1231 .primary_item_id
1232 .ok_or_else(|| Error::invalid("avif: missing pitm"))?;
1233 let info = crate::inspect::inspect(bytes)?;
1234 let bit_depth = info.bit_depth.unwrap_or(8);
1235 let format = layout_of_info(
1236 bit_depth,
1237 info.monochrome,
1238 info.chroma_subsampling,
1239 info.has_alpha,
1240 )?;
1241 let (nclx, icc) = item_colrs(&hdr.meta, primary_id);
1242 let colour = nclx.or_else(|| match &info.colour {
1245 Some(c @ Colr::Nclx { .. }) => Some(c.clone()),
1246 _ => None,
1247 });
1248 let has_icc = icc.is_some() || matches!(info.colour, Some(Colr::Icc(_)));
1249 let burst = burst_members(&hdr.meta, primary_id);
1250 let frames = if burst.is_empty() {
1251 1
1252 } else {
1253 u32::try_from(burst.len()).unwrap_or(u32::MAX)
1254 };
1255 Ok(ImageInfo::new(
1256 info.width,
1257 info.height,
1258 format,
1259 frames,
1260 info.has_alpha,
1261 ColorInfo::from_colr(colour.as_ref()),
1262 has_icc,
1263 info.exif_item_id.is_some(),
1264 info.xmp_item_id.is_some(),
1265 bit_depth,
1266 false,
1267 ))
1268}
1269
1270fn sequence_info(bytes: &[u8]) -> Result<ImageInfo> {
1271 let meta = crate::avis::parse_avis(bytes)?;
1272 let av1c = meta.av1_codec_config.as_deref().ok_or_else(|| {
1273 Error::invalid("avis: track stsd → av01 → av1C is missing (av1-avif §2.2.1)")
1274 })?;
1275 let cfg = oxideav_heif::Av1Config::parse(av1c)?;
1276 let bit_depth = cfg.bit_depth();
1277 let format = layout_of_info(
1278 bit_depth,
1279 cfg.monochrome,
1280 Some((cfg.chroma_subsampling_x, cfg.chroma_subsampling_y)),
1281 false,
1282 )?;
1283 let (width, height) = meta.display_dims.unwrap_or((0, 0));
1284 let (has_exif, has_xmp, has_icc) = match crate::parser::parse_header(bytes) {
1285 Ok(hdr) => {
1286 let primary = hdr.meta.primary_item_id;
1289 match primary.and_then(|_| crate::inspect::inspect(bytes).ok()) {
1290 Some(i) => (
1291 i.exif_item_id.is_some(),
1292 i.xmp_item_id.is_some(),
1293 matches!(i.colour, Some(Colr::Icc(_)))
1294 || primary
1295 .map(|p| item_colrs(&hdr.meta, p).1.is_some())
1296 .unwrap_or(false),
1297 ),
1298 None => (false, false, false),
1299 }
1300 }
1301 Err(_) => (false, false, false),
1302 };
1303 Ok(ImageInfo::new(
1304 width,
1305 height,
1306 format,
1307 u32::try_from(meta.samples.len()).unwrap_or(u32::MAX),
1308 false,
1309 ColorInfo::from_colr(meta.colr.as_ref()),
1310 has_icc,
1311 has_exif,
1312 has_xmp,
1313 bit_depth,
1314 true,
1315 ))
1316}
1317
1318#[cfg(test)]
1319mod tests {
1320 use super::*;
1321
1322 fn ftyp(major: &[u8; 4], compat: &[&[u8; 4]]) -> Vec<u8> {
1323 let mut v = Vec::new();
1324 let size = 16 + 4 * compat.len();
1325 v.extend_from_slice(&(size as u32).to_be_bytes());
1326 v.extend_from_slice(b"ftyp");
1327 v.extend_from_slice(major);
1328 v.extend_from_slice(&[0, 0, 0, 0]);
1329 for c in compat {
1330 v.extend_from_slice(*c);
1331 }
1332 v
1333 }
1334
1335 #[test]
1336 fn probe_accepts_avif_brands_only() {
1337 assert!(probe(&ftyp(b"avif", &[b"mif1", b"miaf"])));
1338 assert!(probe(&ftyp(b"mif1", &[b"avif"])));
1339 assert!(probe(&ftyp(b"avis", &[b"msf1"])));
1340 assert!(probe(&ftyp(b"avio", &[])));
1341 assert!(!probe(&ftyp(b"heic", &[b"mif1", b"miaf"])));
1342 assert!(!probe(&ftyp(b"mif1", &[b"heic"])));
1343 assert!(!probe(b""));
1344 assert!(!probe(b"\0\0\0\x10ftypavif"));
1345 assert!(!probe(b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0"));
1346 let mut t = ftyp(b"mif1", &[b"miaf", b"avif"]);
1348 t.truncate(22);
1349 assert!(!probe(&t));
1350 let mut large = Vec::new();
1352 large.extend_from_slice(&1u32.to_be_bytes());
1353 large.extend_from_slice(b"ftyp");
1354 large.extend_from_slice(&28u64.to_be_bytes());
1355 large.extend_from_slice(b"avif");
1356 large.extend_from_slice(&[0, 0, 0, 0]);
1357 large.extend_from_slice(b"mif1");
1358 assert!(probe(&large));
1359 }
1360
1361 #[test]
1362 fn rgb_round_trips_through_identity_planes() {
1363 let rgb: Vec<u8> = (0..3 * 4 * 2).map(|i| (i * 7 % 256) as u8).collect();
1364 let img = AvifImage::from_rgb8(4, 2, rgb.clone()).unwrap();
1365 assert_eq!(img.format(), PixelFormat::Yuv444P);
1366 assert!(img.color.is_identity_matrix());
1367 assert_eq!(img.to_rgb8(), rgb);
1368 let rgba = img.to_rgba8();
1369 assert_eq!(rgba.len(), 4 * 2 * 4);
1370 assert!(rgba.chunks_exact(4).all(|p| p[3] == 255));
1371 let with_alpha: Vec<u8> = (0..4 * 4 * 2).map(|i| (i * 13 % 256) as u8).collect();
1372 let img = AvifImage::from_rgba8(4, 2, with_alpha.clone()).unwrap();
1373 assert_eq!(img.format(), PixelFormat::Yuva444P);
1374 assert_eq!(img.to_rgba8(), with_alpha);
1375 assert_eq!(
1376 img.to_rgb8(),
1377 with_alpha
1378 .chunks_exact(4)
1379 .flat_map(|p| p[..3].to_vec())
1380 .collect::<Vec<_>>()
1381 );
1382 }
1383
1384 #[test]
1385 fn limited_range_luma_maps_to_rgb_extremes() {
1386 let planes = vec![
1388 Plane::new(2, vec![16, 235, 128, 128]),
1389 Plane::new(1, vec![128]),
1390 Plane::new(1, vec![128]),
1391 ];
1392 let img = AvifImage::new(2, 2, PixelFormat::Yuv420P, planes)
1393 .unwrap()
1394 .with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 1));
1395 let rgb = img.to_rgb8();
1396 assert_eq!(&rgb[0..3], &[0, 0, 0]);
1397 assert_eq!(&rgb[3..6], &[255, 255, 255]);
1398 assert_eq!(&rgb[6..9], &[130, 130, 130]);
1400 let full = img
1402 .clone()
1403 .with_color(ColorInfo::new(ColorRange::Full, 1, 1, 1));
1404 let rgb = full.to_rgb8();
1405 assert_eq!(&rgb[0..3], &[16, 16, 16]);
1406 assert_eq!(&rgb[3..6], &[235, 235, 235]);
1407 }
1408
1409 #[test]
1410 fn deep_samples_scale_to_eight_bits_by_rounding() {
1411 let words: Vec<u8> = [0u16, 1023, 512, 2]
1413 .iter()
1414 .flat_map(|v| v.to_le_bytes())
1415 .collect();
1416 let img = AvifImage::new(4, 1, PixelFormat::Gray10Le, vec![Plane::new(8, words)]).unwrap();
1417 let rgb = img.to_rgb8();
1418 assert_eq!(rgb[0], 0);
1419 assert_eq!(rgb[3], 255);
1420 assert_eq!(rgb[6], 128);
1421 assert_eq!(rgb[9], 0); assert_eq!(img.as_bytes().map(|b| b.len()), Some(8));
1423 }
1424
1425 #[test]
1426 fn unknown_matrix_falls_back_to_bt601_in_the_infallible_view() {
1427 let planes = vec![
1428 Plane::new(2, vec![16, 235, 128, 128]),
1429 Plane::new(1, vec![128]),
1430 Plane::new(1, vec![128]),
1431 ];
1432 let img = AvifImage::new(2, 2, PixelFormat::Yuv420P, planes)
1433 .unwrap()
1434 .with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 131));
1435 assert!(matches!(img.try_to_rgb8(), Err(Error::Unsupported(_))));
1436 let bt601 = img
1437 .clone()
1438 .with_color(ColorInfo::new(ColorRange::Limited, 1, 1, 6));
1439 assert_eq!(img.to_rgb8(), bt601.to_rgb8());
1440 assert_eq!(img.to_rgba8(), bt601.to_rgba8());
1441 }
1442
1443 #[test]
1444 fn packed_ya_carries_alpha() {
1445 let img = AvifImage::new(
1446 2,
1447 1,
1448 PixelFormat::Ya8,
1449 vec![Plane::new(4, vec![200, 10, 50, 255])],
1450 )
1451 .unwrap();
1452 let rgba = img.to_rgba8();
1453 assert_eq!(rgba, vec![200, 200, 200, 10, 50, 50, 50, 255]);
1454 assert!(img.has_alpha());
1455 }
1456
1457 #[test]
1458 fn inconsistent_planes_cannot_be_constructed() {
1459 assert!(matches!(
1461 AvifImage::new(4, 4, PixelFormat::Yuv420P, vec![Plane::new(4, vec![0; 4])]),
1462 Err(Error::InvalidData(_))
1463 ));
1464 let short = vec![
1466 Plane::new(4, vec![0; 16]),
1467 Plane::new(2, vec![0; 3]),
1468 Plane::new(2, vec![0; 4]),
1469 ];
1470 assert!(AvifImage::new(4, 4, PixelFormat::Yuv420P, short).is_err());
1471 assert!(AvifImage::new(0, 4, PixelFormat::Gray8, vec![Plane::new(0, vec![])]).is_err());
1473 let odd = vec![
1475 Plane::new(3, vec![0; 9]),
1476 Plane::new(4, vec![0; 8]),
1477 Plane::new(2, vec![0; 4]),
1478 ];
1479 let img = AvifImage::new(3, 3, PixelFormat::Yuv420P, odd).unwrap();
1480 assert!(img.as_bytes().is_none());
1481 assert_eq!(img.to_rgb8().len(), 27);
1482 let mut broken =
1485 AvifImage::new(2, 2, PixelFormat::Gray8, vec![Plane::new(2, vec![0; 4])]).unwrap();
1486 broken.planes.clear();
1487 assert!(broken.try_to_rgb8().is_err());
1488 assert_eq!(broken.into_raw().len(), 0);
1489 }
1490
1491 #[test]
1492 fn quality_scale_maps_to_q_idx() {
1493 assert_eq!(EncodeOptions::default().with_quality(100).base_q_idx, 0);
1494 assert_eq!(EncodeOptions::default().with_quality(90).base_q_idx, 26);
1495 assert_eq!(EncodeOptions::default().with_quality(50).base_q_idx, 128);
1496 assert_eq!(EncodeOptions::default().with_quality(0).base_q_idx, 255);
1497 assert_eq!(EncodeOptions::default().with_quality(200).base_q_idx, 0);
1498 let o = EncodeOptions::new(3, 4, true);
1499 assert_eq!(
1500 (o.base_q_idx, o.alpha_q_idx, o.premultiplied_alpha),
1501 (3, 4, true)
1502 );
1503 assert_eq!(o.chroma, StillChroma::Yuv420);
1504 assert!(o.embed_exif && o.embed_xmp && o.embed_icc);
1505 }
1506
1507 #[test]
1508 fn exif_offset_word_is_stripped_and_rewritten() {
1509 let body = [0, 0, 0, 2, 0xAA, 0xBB, b'M', b'M', 0, 42];
1510 assert_eq!(exif_tiff_bytes(&body), Some(vec![b'M', b'M', 0, 42]));
1511 assert_eq!(exif_tiff_bytes(&[0, 0, 0, 9, 1]), None);
1512 assert_eq!(exif_tiff_bytes(&[0, 0]), None);
1513 assert_eq!(exif_item_body(b"II"), vec![0, 0, 0, 0, b'I', b'I']);
1514 }
1515
1516 #[test]
1517 fn decode_limits_are_reported_as_limit_exceeded() {
1518 let o = DecodeOptions::default()
1519 .with_max_width(Some(8))
1520 .with_max_pixels(Some(10));
1521 assert!(matches!(o.check_dims(9, 1), Err(Error::LimitExceeded(_))));
1522 assert!(matches!(o.check_dims(4, 4), Err(Error::LimitExceeded(_))));
1523 assert!(o.check_dims(5, 2).is_ok());
1524 assert!(matches!(
1525 DecodeOptions::default()
1526 .with_max_bytes(Some(3))
1527 .check_bytes(4),
1528 Err(Error::LimitExceeded(_))
1529 ));
1530 let unlimited = DecodeOptions::default()
1531 .with_max_width(None)
1532 .with_max_height(None)
1533 .with_max_pixels(None)
1534 .with_max_bytes(None);
1535 assert!(unlimited.check_dims(u32::MAX, u32::MAX).is_ok());
1536 assert!(unlimited.check_bytes(usize::MAX).is_ok());
1537 }
1538
1539 #[test]
1540 fn info_describes_fixture_headers() {
1541 let bytes = std::fs::read(concat!(
1542 env!("CARGO_MANIFEST_DIR"),
1543 "/tests/fixtures/yuv420_10bit_full.avif"
1544 ))
1545 .unwrap();
1546 assert!(probe(&bytes));
1547 let i = info(&bytes).unwrap();
1548 assert_eq!(i.format, PixelFormat::Yuv420P10Le);
1549 assert_eq!(i.bit_depth, 10);
1550 assert_eq!(i.frames, 1);
1551 assert!(!i.is_sequence);
1552 assert_eq!(i.color.range, ColorRange::Full);
1553 assert!(i.width > 0 && i.height > 0);
1554
1555 let bytes = std::fs::read(concat!(
1556 env!("CARGO_MANIFEST_DIR"),
1557 "/tests/fixtures/alpha_video.avif"
1558 ))
1559 .unwrap();
1560 let i = info(&bytes).unwrap();
1561 assert!(i.is_sequence);
1562 assert!(i.frames > 1);
1563
1564 let bytes = std::fs::read(concat!(
1565 env!("CARGO_MANIFEST_DIR"),
1566 "/tests/fixtures/monochrome.avif"
1567 ))
1568 .unwrap();
1569 let i = info(&bytes).unwrap();
1570 assert_eq!(i.format, PixelFormat::Gray8);
1571 assert!(!i.has_alpha);
1572 assert!(info(b"not an avif file at all, just bytes").is_err());
1573 }
1574}