Skip to main content

imagesize/container/
heif.rs

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            if tag == "mdat" {
45                // We've likely hit the end, so continue with what we have
46                // If this does not work for some images, then investigate
47                // looking into the 'iloc' container which should map out
48                // the sizes of these types of containers.
49                break;
50            }
51            return Err(ImageError::CorruptedImage);
52        }
53
54        //  ispe tag has a junk value followed by width and height as u32
55        if tag == "ispe" {
56            found_ispe = true;
57            read_u32(reader, &Endian::Big)?; //  Discard junk value
58            let width = read_u32(reader, &Endian::Big)? as usize;
59            let height = read_u32(reader, &Endian::Big)? as usize;
60
61            //  Assign new largest size by area
62            if width * height > max_width * max_height {
63                max_width = width;
64                max_height = height;
65            }
66        } else if tag == "irot" {
67            // irot is 9 bytes total: size, tag, 1 byte for rotation (0-3)
68            rotation = read_u8(reader)?;
69        } else if size >= ipco_size {
70            // If we've gone past the ipco boundary, then break
71            break;
72        } else {
73            // If we're still inside ipco, consume all bytes for
74            // the current tag, minus the bytes already read in `read_tag`
75            ipco_size -= size;
76            reader.seek(SeekFrom::Current(size as i64 - 8))?;
77        }
78    }
79
80    //  If no ispe found, then we have no actual dimension data to use
81    if !found_ispe {
82        return Err(
83            std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Not enough data").into(),
84        );
85    }
86
87    //  Rotation can only be 0-3. 1 and 3 are 90 and 270 degrees respectively (anti-clockwise)
88    //  If we have 90 or 270 rotation, flip width and height
89    if rotation == 1 || rotation == 3 {
90        std::mem::swap(&mut max_width, &mut max_height);
91    }
92
93    Ok(ImageSize {
94        width: max_width,
95        height: max_height,
96    })
97}
98
99pub fn matches<R: BufRead + Seek>(header: &[u8], reader: &mut R) -> Option<Compression> {
100    if header.len() < 12 || &header[4..8] != b"ftyp" {
101        return None;
102    }
103
104    let brand: [u8; 4] = header[8..12].try_into().unwrap();
105
106    if let Some(compression) = inner_matches(&brand) {
107        // case 1: { heic, ... }
108        return Some(compression);
109    }
110
111    // REFS: https://github.com/nokiatech/heif/blob/be43efdf273ae9cf90e552b99f16ac43983f3d19/srcs/reader/heifreaderimpl.cpp#L738
112    let brands = [b"mif1", b"msf1", b"mif2", b"miaf"];
113
114    if brands.contains(&&brand) {
115        let mut buf = [0; 12];
116
117        if reader.read_exact(&mut buf).is_err() {
118            return Some(Compression::Unknown);
119        }
120
121        let brand2: [u8; 4] = buf[4..8].try_into().unwrap();
122
123        if let Some(compression) = inner_matches(&brand2) {
124            // case 2: { msf1, version, heic,  msf1, ... }
125            //           brand          brand2 brand3
126            return Some(compression);
127        }
128
129        if brands.contains(&&brand2) {
130            // case 3: { msf1, version, msf1,  heic, ... }
131            //           brand          brand2 brand3
132            let brand3: [u8; 4] = buf[8..12].try_into().unwrap();
133
134            if let Some(compression) = inner_matches(&brand3) {
135                return Some(compression);
136            }
137        }
138    }
139
140    Some(Compression::Unknown)
141}
142
143fn inner_matches(brand: &[u8; 4]) -> Option<Compression> {
144    // Since other non-heif files may contain ftype in the header
145    // we try to use brands to distinguish image files specifically.
146    // List of brands from here: https://mp4ra.org/#/brands
147    let hevc_brands = [
148        b"heic", b"heix", b"heis", b"hevs", b"heim", b"hevm", b"hevc", b"hevx",
149    ];
150    let av1_brands = [
151        b"avif", b"avio", b"avis",
152        // AVIF only
153        // REFS: https://rawcdn.githack.com/AOMediaCodec/av1-avif/67a92add6cd642a8863e386fa4db87954a6735d1/index.html#advanced-profile
154        b"MA1A", b"MA1B",
155    ];
156    let jpeg_brands = [b"jpeg", b"jpgs"];
157
158    // unused
159    // REFS: https://github.com/MPEGGroup/FileFormatConformance/blob/6eef4e4c8bc70e2af9aeb1d62e764a6235f9d6a6/data/standard_features/23008-12/brands.json
160    // let avc_brands = [b"avci", b"avcs"];
161    // let vvc_brands = [b"vvic", b"vvis"];
162    // let evc_brands = [b"evbi", b"evbs", b"evmi", b"evms"];
163
164    // Maybe unnecessary
165    // REFS: https://github.com/nokiatech/heif/blob/be43efdf273ae9cf90e552b99f16ac43983f3d19/srcs/reader/heifreaderimpl.cpp#L1415
166    // REFS: https://github.com/nokiatech/heif/blob/be43efdf273ae9cf90e552b99f16ac43983f3d19/srcs/api-cpp/ImageItem.h#L37
167    // let feature_brands = [b"pred", b"auxl", b"thmb", b"base", b"dimg"];
168    if hevc_brands.contains(&brand) {
169        return Some(Compression::Hevc);
170    }
171
172    if av1_brands.contains(&brand) {
173        return Some(Compression::Av1);
174    }
175
176    if jpeg_brands.contains(&brand) {
177        return Some(Compression::Jpeg);
178    }
179
180    None
181}
182
183fn skip_to_tag<R: BufRead + Seek>(reader: &mut R, tag: &[u8]) -> ImageResult<u32> {
184    let mut tag_buf = [0; 4];
185
186    loop {
187        let size = read_u32(reader, &Endian::Big)?;
188        reader.read_exact(&mut tag_buf)?;
189
190        if tag_buf == tag {
191            return Ok(size);
192        }
193
194        if size >= 8 {
195            reader.seek(SeekFrom::Current(size as i64 - 8))?;
196        } else {
197            return Err(std::io::Error::new(
198                std::io::ErrorKind::InvalidData,
199                format!("Invalid heif box size: {size}"),
200            )
201            .into());
202        }
203    }
204}