use indexmap::IndexSet; use log::warn; use rgb::{RGB16, RGBA8}; use crate::{ colors::{BitDepth, ColorType}, deflate::{crc32, inflate}, display_chunks::DISPLAY_CHUNKS, error::PngError, interlace::Interlacing, AtomicMin, Deflaters, PngResult, }; #[derive(Debug, Clone)] /// Headers from the IHDR chunk of the image pub struct IhdrData { /// The width of the image in pixels pub width: u32, /// The height of the image in pixels pub height: u32, /// The color type of the image pub color_type: ColorType, /// The bit depth of the image pub bit_depth: BitDepth, /// The interlacing mode of the image pub interlaced: Interlacing, } impl IhdrData { /// Bits per pixel #[must_use] #[inline] pub fn bpp(&self) -> usize { self.bit_depth as usize * self.color_type.channels_per_pixel() as usize } /// Byte length of IDAT that is correct for this IHDR #[must_use] pub fn raw_data_size(&self) -> usize { let w = self.width as usize; let h = self.height as usize; let bpp = self.bpp(); fn bitmap_size(bpp: usize, w: usize, h: usize) -> usize { ((w * bpp + 7) / 8) * h } if self.interlaced == Interlacing::None { bitmap_size(bpp, w, h) + h } else { let mut size = bitmap_size(bpp, (w + 7) >> 3, (h + 7) >> 3) + ((h + 7) >> 3); if w > 4 { size += bitmap_size(bpp, (w + 3) >> 3, (h + 7) >> 3) + ((h + 7) >> 3); } size += bitmap_size(bpp, (w + 3) >> 2, (h + 3) >> 3) + ((h + 3) >> 3); if w > 2 { size += bitmap_size(bpp, (w + 1) >> 2, (h + 3) >> 2) + ((h + 3) >> 2); } size += bitmap_size(bpp, (w + 1) >> 1, (h + 1) >> 2) + ((h + 1) >> 2); if w > 1 { size += bitmap_size(bpp, w >> 1, (h + 1) >> 1) + ((h + 1) >> 1); } size + bitmap_size(bpp, w, h >> 1) + (h >> 1) } } } #[derive(Debug, Clone)] pub struct Chunk { pub name: [u8; 4], pub data: Vec, } #[derive(Debug, PartialEq, Eq, Clone)] /// Options to use when stripping chunks pub enum StripChunks { /// None None, /// Remove specific chunks Strip(IndexSet<[u8; 4]>), /// Remove all chunks that won't affect image display Safe, /// Remove all non-critical chunks except these Keep(IndexSet<[u8; 4]>), /// All non-critical chunks All, } impl StripChunks { pub(crate) fn keep(&self, name: &[u8; 4]) -> bool { match &self { StripChunks::None => true, StripChunks::Keep(names) => names.contains(name), StripChunks::Strip(names) => !names.contains(name), StripChunks::Safe => DISPLAY_CHUNKS.contains(name), StripChunks::All => false, } } } #[inline] pub fn file_header_is_valid(bytes: &[u8]) -> bool { let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; *bytes == expected_header } #[derive(Debug, Clone, Copy)] pub struct RawChunk<'a> { pub name: [u8; 4], pub data: &'a [u8], } pub fn parse_next_chunk<'a>( byte_data: &'a [u8], byte_offset: &mut usize, fix_errors: bool, ) -> PngResult>> { let length = read_be_u32( byte_data .get(*byte_offset..*byte_offset + 4) .ok_or(PngError::TruncatedData)?, ); *byte_offset += 4; let chunk_start = *byte_offset; let chunk_name = byte_data .get(chunk_start..chunk_start + 4) .ok_or(PngError::TruncatedData)?; if chunk_name == b"IEND" { // End of data return Ok(None); } *byte_offset += 4; let data = byte_data .get(*byte_offset..*byte_offset + length as usize) .ok_or(PngError::TruncatedData)?; *byte_offset += length as usize; let crc = read_be_u32( byte_data .get(*byte_offset..*byte_offset + 4) .ok_or(PngError::TruncatedData)?, ); *byte_offset += 4; let chunk_bytes = byte_data .get(chunk_start..chunk_start + 4 + length as usize) .ok_or(PngError::TruncatedData)?; if !fix_errors && crc32(chunk_bytes) != crc { return Err(PngError::new(&format!( "CRC Mismatch in {} chunk; May be recoverable by using --fix", String::from_utf8_lossy(chunk_name) ))); } let name: [u8; 4] = chunk_name.try_into().unwrap(); Ok(Some(RawChunk { name, data })) } pub fn parse_ihdr_chunk( byte_data: &[u8], palette_data: Option>, trns_data: Option>, ) -> PngResult { // This eliminates bounds checks for the rest of the function let interlaced = byte_data.get(12).copied().ok_or(PngError::TruncatedData)?; Ok(IhdrData { color_type: match byte_data[9] { 0 => ColorType::Grayscale { transparent_shade: trns_data .filter(|t| t.len() >= 2) .map(|t| u16::from_be_bytes([t[0], t[1]])), }, 2 => ColorType::RGB { transparent_color: trns_data.filter(|t| t.len() >= 6).map(|t| RGB16 { r: u16::from_be_bytes([t[0], t[1]]), g: u16::from_be_bytes([t[2], t[3]]), b: u16::from_be_bytes([t[4], t[5]]), }), }, 3 => ColorType::Indexed { palette: palette_to_rgba(palette_data, trns_data).unwrap_or_default(), }, 4 => ColorType::GrayscaleAlpha, 6 => ColorType::RGBA, _ => return Err(PngError::new("Unexpected color type in header")), }, bit_depth: byte_data[8].try_into()?, width: read_be_u32(&byte_data[0..4]), height: read_be_u32(&byte_data[4..8]), interlaced: interlaced.try_into()?, }) } /// Construct an RGBA palette from the raw palette and transparency data fn palette_to_rgba( palette_data: Option>, trns_data: Option>, ) -> Result, PngError> { let palette_data = palette_data.ok_or_else(|| PngError::new("no palette in indexed image"))?; let mut palette: Vec<_> = palette_data .chunks(3) .map(|color| RGBA8::new(color[0], color[1], color[2], 255)) .collect(); if let Some(trns_data) = trns_data { for (color, trns) in palette.iter_mut().zip(trns_data) { color.a = trns; } } Ok(palette) } #[inline] fn read_be_u32(bytes: &[u8]) -> u32 { u32::from_be_bytes(bytes.try_into().unwrap()) } /// Extract and decompress the ICC profile from an iCCP chunk pub fn extract_icc(iccp: &Chunk) -> Option> { // Skip (useless) profile name let mut data = iccp.data.as_slice(); loop { let (&n, rest) = data.split_first()?; data = rest; if n == 0 { break; } } let (&compression_method, compressed_data) = data.split_first()?; if compression_method != 0 { return None; // The profile is supposed to be compressed (method 0) } // The decompressed size is unknown so we have to guess the required buffer size let max_size = compressed_data.len() * 2 + 1000; match inflate(compressed_data, max_size) { Ok(icc) => Some(icc), Err(e) => { // Log the error so we can know if the buffer size needs to be adjusted warn!("Failed to decompress icc: {}", e); None } } } /// Construct an iCCP chunk by compressing the ICC profile pub fn construct_iccp(icc: &[u8], deflater: Deflaters) -> PngResult { let mut compressed = deflater.deflate(icc, &AtomicMin::new(None))?; let mut data = Vec::with_capacity(compressed.len() + 5); data.extend(b"icc"); // Profile name - generally unused, can be anything data.extend([0, 0]); // Null separator, zlib compression method data.append(&mut compressed); Ok(Chunk { name: *b"iCCP", data, }) } /// If the profile is sRGB, extracts the rendering intent value from it pub fn srgb_rendering_intent(icc_data: &[u8]) -> Option { let rendering_intent = *icc_data.get(67)?; // The known profiles are the same as in libpng's `png_sRGB_checks`. // The Profile ID header of ICC has a fixed layout, // and is supposed to contain MD5 of profile data at this offset match icc_data.get(84..100)? { b"\x29\xf8\x3d\xde\xaf\xf2\x55\xae\x78\x42\xfa\xe4\xca\x83\x39\x0d" | b"\xc9\x5b\xd6\x37\xe9\x5d\x8a\x3b\x0d\xf3\x8f\x99\xc1\x32\x03\x89" | b"\xfc\x66\x33\x78\x37\xe2\x88\x6b\xfd\x72\xe9\x83\x82\x28\xf1\xb8" | b"\x34\x56\x2a\xbf\x99\x4c\xcd\x06\x6d\x2c\x57\x21\xd0\xd6\x8c\x5d" => { Some(rendering_intent) } b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" => { // Known-bad profiles are identified by their CRC match (crc32(icc_data), icc_data.len()) { (0x5d51_29ce, 3024) | (0x182e_a552, 3144) | (0xf29e_526d, 3144) => { Some(rendering_intent) } _ => None, } } _ => None, } }