imagesize/formats/
heif.rs

1use crate::util::*;
2use crate::{ImageError, ImageResult, ImageSize};
3
4use std::io::{BufRead, Seek, SeekFrom};
5
6pub fn size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
7    reader.seek(SeekFrom::Start(0))?;
8    //  Read the ftyp header size
9    let ftyp_size = read_u32(reader, &Endian::Big)?;
10
11    //  Jump to the first actual box offset
12    reader.seek(SeekFrom::Start(ftyp_size.into()))?;
13
14    //  Skip to meta tag which contains all the metadata
15    skip_to_tag(reader, b"meta")?;
16    read_u32(reader, &Endian::Big)?; //  Meta has a junk value after it
17    skip_to_tag(reader, b"iprp")?; //  Find iprp tag
18
19    let mut ipco_size = skip_to_tag(reader, b"ipco")? as usize; //  Find ipco tag
20
21    //  Keep track of the max size of ipco tag
22    let mut max_width = 0usize;
23    let mut max_height = 0usize;
24    let mut found_ispe = false;
25    let mut rotation = 0u8;
26
27    while let Ok((tag, size)) = read_tag(reader) {
28        //  Size of tag length + tag cannot be under 8 (4 bytes each)
29        if size < 8 {
30            return Err(ImageError::CorruptedImage);
31        }
32
33        //  ispe tag has a junk value followed by width and height as u32
34        if tag == "ispe" {
35            found_ispe = true;
36            read_u32(reader, &Endian::Big)?; //  Discard junk value
37            let width = read_u32(reader, &Endian::Big)? as usize;
38            let height = read_u32(reader, &Endian::Big)? as usize;
39
40            //  Assign new largest size by area
41            if width * height > max_width * max_height {
42                max_width = width;
43                max_height = height;
44            }
45        } else if tag == "irot" {
46            // irot is 9 bytes total: size, tag, 1 byte for rotation (0-3)
47            rotation = read_u8(reader)?;
48        } else if size >= ipco_size {
49            // If we've gone past the ipco boundary, then break
50            break;
51        } else {
52            // If we're still inside ipco, consume all bytes for
53            // the current tag, minus the bytes already read in `read_tag`
54            ipco_size -= size;
55            reader.seek(SeekFrom::Current(size as i64 - 8))?;
56        }
57    }
58
59    //  If no ispe found, then we have no actual dimension data to use
60    if !found_ispe {
61        return Err(
62            std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Not enough data").into(),
63        );
64    }
65
66    //  Rotation can only be 0-3. 1 and 3 are 90 and 270 degrees respectively (anti-clockwise)
67    //  If we have 90 or 270 rotation, flip width and height
68    if rotation == 1 || rotation == 3 {
69        std::mem::swap(&mut max_width, &mut max_height);
70    }
71
72    Ok(ImageSize {
73        width: max_width,
74        height: max_height,
75    })
76}
77
78pub fn matches(header: &[u8]) -> bool {
79    if header.len() < 12 || &header[4..8] != b"ftyp" {
80        return false;
81    }
82
83    let header_brand = &header[8..12];
84
85    // Since other non-heif files may contain ftype in the header
86    // we try to use brands to distinguish image files specifically.
87    // List of brands from here: https://mp4ra.org/#/brands
88    let valid_brands = [
89        // HEIF specific
90        b"avci", b"avcs", b"heic", b"heim",
91        b"heis", b"heix", b"hevc", b"hevm",
92        b"hevs", b"hevx", b"jpeg", b"jpgs",
93        b"mif1", b"msf1", b"mif2", b"pred",
94        // AVIF specific
95        b"avif", b"avio", b"avis", b"MA1A",
96        b"MA1B",
97    ];
98
99    for brand in valid_brands {
100        if brand == header_brand {
101            return true;
102        }
103    }
104    
105    false
106}
107
108fn skip_to_tag<R: BufRead + Seek>(reader: &mut R, tag: &[u8]) -> ImageResult<u32> {
109    let mut tag_buf = [0; 4];
110
111    loop {
112        let size = read_u32(reader, &Endian::Big)?;
113        reader.read_exact(&mut tag_buf)?;
114
115        if tag_buf == tag {
116            return Ok(size);
117        }
118
119        if size >= 8 {
120            reader.seek(SeekFrom::Current(size as i64 - 8))?;
121        } else {
122            return Err(std::io::Error::new(
123                std::io::ErrorKind::InvalidData,
124                format!("Invalid heif box size: {}", size),
125            )
126            .into());
127        }
128    }
129}