1use crate::util::*;
2use crate::{ImageError, ImageResult, ImageSize};
3
4use std::convert::TryInto;
5use std::io::{BufRead, Seek, SeekFrom};
6
7// REFS: https://github.com/strukturag/libheif/blob/f0c1a863cabbccb2d280515b7ecc73e6717702dc/libheif/heif.h#L600
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub enum Compression {
10 Av1,
11 Hevc,
12 Jpeg,
13 Unknown,
14 // unused(reuse in the future?)
15 // Avc,
16 // Vvc,
17 // Evc,
18}
19
20pub fn size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
21 reader.seek(SeekFrom::Start(0))?;
22 // Read the ftyp header size
23 let ftyp_size = read_u32(reader, &Endian::Big)?;
24
25 // Jump to the first actual box offset
26 reader.seek(SeekFrom::Start(ftyp_size.into()))?;
27
28 // Skip to meta tag which contains all the metadata
29 skip_to_tag(reader, b"meta")?;
30 read_u32(reader, &Endian::Big)?; // Meta has a junk value after it
31 skip_to_tag(reader, b"iprp")?; // Find iprp tag
32
33 let mut ipco_size = skip_to_tag(reader, b"ipco")? as usize; // Find ipco tag
34
35 // Keep track of the max size of ipco tag
36 let mut max_width = 0usize;
37 let mut max_height = 0usize;
38 let mut found_ispe = false;
39 let mut rotation = 0u8;
40
41 while let Ok((tag, size)) = read_tag(reader) {
42 // Size of tag length + tag cannot be under 8 (4 bytes each)
43 if size < 8 {
44 return Err(ImageError::CorruptedImage);
45 }
46
47 // ispe tag has a junk value followed by width and height as u32
48 if tag == "ispe" {
49 found_ispe = true;
50 read_u32(reader, &Endian::Big)?; // Discard junk value
51 let width = read_u32(reader, &Endian::Big)? as usize;
52 let height = read_u32(reader, &Endian::Big)? as usize;
53
54 // Assign new largest size by area
55 if width * height > max_width * max_height {
56 max_width = width;
57 max_height = height;
58 }
59 } else if tag == "irot" {
60 // irot is 9 bytes total: size, tag, 1 byte for rotation (0-3)
61 rotation = read_u8(reader)?;
62 } else if size >= ipco_size {
63 // If we've gone past the ipco boundary, then break
64 break;
65 } else {
66 // If we're still inside ipco, consume all bytes for
67 // the current tag, minus the bytes already read in `read_tag`
68 ipco_size -= size;
69 reader.seek(SeekFrom::Current(size as i64 - 8))?;
70 }
71 }
72
73 // If no ispe found, then we have no actual dimension data to use
74 if !found_ispe {
75 return Err(
76 std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Not enough data").into(),
77 );
78 }
79
80 // Rotation can only be 0-3. 1 and 3 are 90 and 270 degrees respectively (anti-clockwise)
81 // If we have 90 or 270 rotation, flip width and height
82 if rotation == 1 || rotation == 3 {
83 std::mem::swap(&mut max_width, &mut max_height);
84 }
85
86 Ok(ImageSize {
87 width: max_width,
88 height: max_height,
89 })
90}
91
92pub fn matches<R: BufRead + Seek>(header: &[u8], reader: &mut R) -> Option<Compression> {
93 if header.len() < 12 || &header[4..8] != b"ftyp" {
94 return None;
95 }
96
97 let brand: [u8; 4] = header[8..12].try_into().unwrap();
98
99 if let Some(compression) = inner_matches(&brand) {
100 // case 1: { heic, ... }
101 return Some(compression);
102 }
103
104 // REFS: https://github.com/nokiatech/heif/blob/be43efdf273ae9cf90e552b99f16ac43983f3d19/srcs/reader/heifreaderimpl.cpp#L738
105 let brands = [b"mif1", b"msf1", b"mif2", b"miaf"];
106
107 if brands.contains(&&brand) {
108 let mut buf = [0; 12];
109
110 if reader.read_exact(&mut buf).is_err() {
111 return Some(Compression::Unknown);
112 }
113
114 let brand2: [u8; 4] = buf[4..8].try_into().unwrap();
115
116 if let Some(compression) = inner_matches(&brand2) {
117 // case 2: { msf1, version, heic, msf1, ... }
118 // brand brand2 brand3
119 return Some(compression);
120 }
121
122 if brands.contains(&&brand2) {
123 // case 3: { msf1, version, msf1, heic, ... }
124 // brand brand2 brand3
125 let brand3: [u8; 4] = buf[8..12].try_into().unwrap();
126
127 if let Some(compression) = inner_matches(&brand3) {
128 return Some(compression);
129 }
130 }
131 }
132
133 Some(Compression::Unknown)
134}
135
136fn inner_matches(brand: &[u8; 4]) -> Option<Compression> {
137 // Since other non-heif files may contain ftype in the header
138 // we try to use brands to distinguish image files specifically.
139 // List of brands from here: https://mp4ra.org/#/brands
140 let hevc_brands = [
141 b"heic", b"heix", b"heis", b"hevs", b"heim", b"hevm", b"hevc", b"hevx",
142 ];
143 let av1_brands = [
144 b"avif", b"avio", b"avis",
145 // AVIF only
146 // REFS: https://rawcdn.githack.com/AOMediaCodec/av1-avif/67a92add6cd642a8863e386fa4db87954a6735d1/index.html#advanced-profile
147 b"MA1A", b"MA1B",
148 ];
149 let jpeg_brands = [b"jpeg", b"jpgs"];
150
151 // unused
152 // REFS: https://github.com/MPEGGroup/FileFormatConformance/blob/6eef4e4c8bc70e2af9aeb1d62e764a6235f9d6a6/data/standard_features/23008-12/brands.json
153 // let avc_brands = [b"avci", b"avcs"];
154 // let vvc_brands = [b"vvic", b"vvis"];
155 // let evc_brands = [b"evbi", b"evbs", b"evmi", b"evms"];
156
157 // Maybe unnecessary
158 // REFS: https://github.com/nokiatech/heif/blob/be43efdf273ae9cf90e552b99f16ac43983f3d19/srcs/reader/heifreaderimpl.cpp#L1415
159 // REFS: https://github.com/nokiatech/heif/blob/be43efdf273ae9cf90e552b99f16ac43983f3d19/srcs/api-cpp/ImageItem.h#L37
160 // let feature_brands = [b"pred", b"auxl", b"thmb", b"base", b"dimg"];
161 if hevc_brands.contains(&brand) {
162 return Some(Compression::Hevc);
163 }
164
165 if av1_brands.contains(&brand) {
166 return Some(Compression::Av1);
167 }
168
169 if jpeg_brands.contains(&brand) {
170 return Some(Compression::Jpeg);
171 }
172
173 None
174}
175
176fn skip_to_tag<R: BufRead + Seek>(reader: &mut R, tag: &[u8]) -> ImageResult<u32> {
177 let mut tag_buf: [u8; 4] = [0; 4];
178
179 loop {
180 let size: u32 = read_u32(reader, &Endian::Big)?;
181 reader.read_exact(&mut tag_buf)?;
182
183 if tag_buf == tag {
184 return Ok(size);
185 }
186
187 if size >= 8 {
188 reader.seek(pos:SeekFrom::Current(size as i64 - 8))?;
189 } else {
190 return Err(stdError::io::Error::new(
191 kind:std::io::ErrorKind::InvalidData,
192 error:format!("Invalid heif box size: {}", size),
193 )
194 .into());
195 }
196 }
197}
198