| 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 | |