1 | use crate::util::*;
|
2 | use crate::{ImageError, ImageResult, ImageSize};
|
3 |
|
4 | use std::convert::TryInto;
|
5 | use 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)]
|
9 | pub enum Compression {
|
10 | Av1,
|
11 | Hevc,
|
12 | Jpeg,
|
13 | Unknown,
|
14 | // unused(reuse in the future?)
|
15 | // Avc,
|
16 | // Vvc,
|
17 | // Evc,
|
18 | }
|
19 |
|
20 | pub 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 |
|
92 | pub 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 |
|
136 | fn 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 |
|
176 | fn 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 | |