diff --git a/raves_metadata/src/providers/png/chunks/chrm.rs b/raves_metadata/src/providers/png/chunks/chrm.rs new file mode 100644 index 0000000..8d337c8 --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/chrm.rs @@ -0,0 +1,84 @@ +use crate::providers::png::{ + chunks::{Chunk, ChunkContext, PngChunkHeader}, + error::{PngConstructionError, PngWriteError}, +}; + +/// The cHRM chunk has info about chromaticities and the white point. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Chrm { + header: PngChunkHeader, + + // note: these are (x, y) pairs. saves some space/typing. + white_point: (u32, u32), + red: (u32, u32), + green: (u32, u32), + blue: (u32, u32), +} + +impl Chunk for Chrm { + const TYPE: [u8; 4] = *b"cHRM"; + + fn read( + blob: &mut &[u8], + header: PngChunkHeader, + context: ChunkContext, + ) -> Result { + // check that there's no context + debug_assert_eq!(context, ChunkContext::Other); + + // grab white point + let white_point: (u32, u32) = ( + Self::read_u32(blob, "white_point_x")?, + Self::read_u32(blob, "white_point_y")?, + ); + + // grab red + let red: (u32, u32) = ( + Self::read_u32(blob, "red_x")?, + Self::read_u32(blob, "red_y")?, + ); + + // green + let green: (u32, u32) = ( + Self::read_u32(blob, "green_x")?, + Self::read_u32(blob, "green_y")?, + ); + + // blue + let blue: (u32, u32) = ( + Self::read_u32(blob, "blue_x")?, + Self::read_u32(blob, "blue_y")?, + ); + + Ok(Self { + header, + white_point, + red, + green, + blue, + }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + // write the header + self.header.write(buf)?; + + // white point + Self::write_u32(self.white_point.0, buf, "white_point_x")?; + Self::write_u32(self.white_point.1, buf, "white_point_y")?; + + // red + Self::write_u32(self.red.0, buf, "red_x")?; + Self::write_u32(self.red.1, buf, "red_y")?; + + // green + Self::write_u32(self.green.0, buf, "green_x")?; + Self::write_u32(self.green.1, buf, "green_y")?; + + // blue + Self::write_u32(self.blue.0, buf, "blue_x")?; + Self::write_u32(self.blue.1, buf, "blue_y")?; + + Ok(()) + } +} diff --git a/raves_metadata/src/providers/png/chunks/gama.rs b/raves_metadata/src/providers/png/chunks/gama.rs new file mode 100644 index 0000000..c7c41ea --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/gama.rs @@ -0,0 +1,38 @@ +use crate::providers::png::{ + chunks::{Chunk, ChunkContext, PngChunkHeader}, + error::{PngConstructionError, PngWriteError}, +}; + +/// The gAMA chunk provides a basic gamma (brightness) value. +/// +/// Other chunks do this better -- this chunk is pretty basic. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Gama { + header: PngChunkHeader, + gamma: u32, +} + +impl Chunk for Gama { + const TYPE: [u8; 4] = *b"gAMA"; + + fn read( + blob: &mut &[u8], + header: PngChunkHeader, + context: ChunkContext, + ) -> Result { + // check that there's no context + debug_assert_eq!(context, ChunkContext::Other); + + // grab gamma value + let gamma: u32 = Self::read_u32(blob, "gamma")?; + Ok(Self { header, gamma }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + // write the header, then the gamma + self.header.write(buf)?; + Self::write_u32(self.gamma, buf, "gamma")?; + + Ok(()) + } +} diff --git a/raves_metadata/src/providers/png/chunks/iccp.rs b/raves_metadata/src/providers/png/chunks/iccp.rs new file mode 100644 index 0000000..6c542fc --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/iccp.rs @@ -0,0 +1,174 @@ +use crate::providers::png::{ + chunks::{Chunk, ChunkContext, PngChunkHeader}, + error::{PngConstructionError, PngWriteError}, +}; + +/// The iCCP chunk provides an advanced color profile. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Iccp { + header: PngChunkHeader, + profile_name: String, // converting from the NUL-terminated grossness + compression_method: CompressionMethod, + compressed_profile: Vec, // not decompressing that. have fun nerds +} + +impl Chunk for Iccp { + const TYPE: [u8; 4] = *b"iCCP"; + + fn read( + blob: &mut &[u8], + header: PngChunkHeader, + context: ChunkContext, + ) -> Result { + // check that there's no context + debug_assert_eq!(context, ChunkContext::Other); + + let mut profile_name_bytes: Vec = Vec::with_capacity(79); + let mut found_null_terminator: bool = false; + + for _ in 0..79 { + let byte: u8 = Self::read_u8(blob, "profile name (one byte)")?; + if byte == 0x00 { + found_null_terminator = true; + break; + } + + profile_name_bytes.push(byte); + } + + if !found_null_terminator { + let null_separator: u8 = Self::read_u8(blob, "null separator")?; + if null_separator != 0x00 { + return Err(PngConstructionError::IccpInvalidProfileName { + reason: "profile name must be terminated by a NUL separator after 1-79 bytes", + }); + } + } + + // make sure the profile name is up to spec + validate_profile_name_bytes(&profile_name_bytes).map_err(|err| match err { + ProfileNameError::NotLatin1 { c } => { + PngConstructionError::IccpProfileNameNotLatin1 { c } + } + ProfileNameError::InvalidStructure { reason } => { + PngConstructionError::IccpInvalidProfileName { reason } + } + })?; + + // write the profile name into a string + let profile_name: String = profile_name_bytes.into_iter().map(char::from).collect(); + + // grab compression method + let compression_method_raw: u8 = Self::read_u8(blob, "compression method")?; + let compression_method: CompressionMethod = if compression_method_raw != 0 { + return Err(PngConstructionError::IccpUnknownCompressionValue { + value: compression_method_raw, + }); + } else { + CompressionMethod::ZlibDeflate + }; + + // read the rest of the compressed profile + let compressed_profile: Vec = blob.to_vec(); + *blob = &[]; + + Ok(Self { + header, + profile_name, + compression_method, + compressed_profile, + }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + // write the header + self.header.write(buf)?; + + let profile_name_bytes: Vec = self + .profile_name + .chars() + .map(|c: char| { + let code_point: u32 = c.into(); + u8::try_from(code_point).map_err(|_| PngWriteError::IccpInvalidProfileName { + reason: "profile name must contain only Latin-1 code points", + }) + }) + .collect::, PngWriteError>>()?; + + validate_profile_name_bytes(&profile_name_bytes).map_err(|err| match err { + ProfileNameError::NotLatin1 { c } => PngWriteError::IccpProfileNameNotLatin1 { c }, + ProfileNameError::InvalidStructure { reason } => { + PngWriteError::IccpInvalidProfileName { reason } + } + })?; + + // profile name + Self::write_byte_slice(&profile_name_bytes, buf, "profile name")?; + + // NUL terminator on profile name + Self::write_u8(0_u8, buf, "null separator")?; + + // compression method (will always be 0) + Self::write_u8(self.compression_method as u8, buf, "compression method")?; + + // compressed profile + Self::write_byte_slice(&self.compressed_profile, buf, "compressed profile")?; + + Ok(()) + } +} + +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Hash)] +pub enum CompressionMethod { + ZlibDeflate = 0, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ProfileNameError { + NotLatin1 { c: u8 }, + InvalidStructure { reason: &'static str }, +} + +fn validate_profile_name_bytes(bytes: &[u8]) -> Result<(), ProfileNameError> { + if bytes.is_empty() { + return Err(ProfileNameError::InvalidStructure { + reason: "profile name must contain at least one byte", + }); + } + + if bytes.len() > 79 { + return Err(ProfileNameError::InvalidStructure { + reason: "profile name must be 79 bytes or fewer", + }); + } + + let mut prev_was_space: bool = false; + for (idx, &byte) in bytes.iter().enumerate() { + if !profile_name_byte_is_allowed(byte) { + return Err(ProfileNameError::NotLatin1 { c: byte }); + } + + let is_space: bool = byte == b' '; + if (idx == 0 || idx + 1 == bytes.len()) && is_space { + return Err(ProfileNameError::InvalidStructure { + reason: "profile name may not have leading or trailing spaces", + }); + } + + if prev_was_space && is_space { + return Err(ProfileNameError::InvalidStructure { + reason: "profile name may not contain consecutive spaces", + }); + } + + prev_was_space = is_space; + } + + Ok(()) +} + +/// Checks if a byte in the profile name is allowed. +const fn profile_name_byte_is_allowed(c: u8) -> bool { + (c >= 0x20 && c <= 0x7E) || (c >= 0xA1) +} diff --git a/raves_metadata/src/providers/png/chunks/idat.rs b/raves_metadata/src/providers/png/chunks/idat.rs new file mode 100644 index 0000000..11b6477 --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/idat.rs @@ -0,0 +1,60 @@ +use winnow::{Parser, error::EmptyError, token::take}; + +use crate::providers::png::{ + PngConstructionError, + chunks::{Chunk, ChunkContext, PngChunkHeader}, + error::PngWriteError, +}; + +/// A chunk of image data. +/// +/// Compressed and unaltered -- this library isn't a decoder or encoder. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Idat { + header: PngChunkHeader, + data: Vec, +} + +impl Chunk for Idat { + const TYPE: [u8; 4] = *b"IDAT"; + + fn read( + blob: &mut &[u8], + header: super::PngChunkHeader, + _context: ChunkContext, + ) -> Result { + let mut entries: Vec = Vec::with_capacity(header.chunk_length as usize); + + let slice_len: usize = blob.len(); + let slice: &[u8] = + take(header.chunk_length) + .parse_next(blob) + .map_err(|_e: EmptyError| { + log::error!( + "When parsing `IDAT` chunk, failed to read all {} bytes.", + header.chunk_length + ); + PngConstructionError::OuttaBytes { + chunk_type: Idat::TYPE_STR, + expected: header.chunk_length, + remaining: slice_len as u32, + } + })?; + + entries.extend_from_slice(slice); + + Ok(Self { + header, + data: entries, + }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + // write header + self.header.write(buf)?; + + // write all the bytes + Self::write_byte_slice(&self.data, buf, "data")?; + Ok(()) + } +} diff --git a/raves_metadata/src/providers/png/chunks/iend.rs b/raves_metadata/src/providers/png/chunks/iend.rs new file mode 100644 index 0000000..f41f6d9 --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/iend.rs @@ -0,0 +1,39 @@ +use crate::providers::png::{ + chunks::{Chunk, ChunkContext, PngChunkHeader}, + error::{PngConstructionError, PngWriteError}, +}; + +/// A chunk stating that the PNG datastream has ended. +/// +/// It has no associated data -- just the header. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Iend { + header: PngChunkHeader, +} + +impl Chunk for Iend { + const TYPE: [u8; 4] = *b"IEND"; + + fn read( + _blob: &mut &[u8], + header: super::PngChunkHeader, + _context: ChunkContext, + ) -> Result { + // this chunk must be zero length. so let's check that! + if header.chunk_length != 0_u32 { + log::error!("IEND chunk has associated data! That's not allowed."); + return Err(PngConstructionError::IendHadData { + chunk_length: header.chunk_length, + }); + } + + // all good! just store its header + Ok(Self { header }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + // write header + self.header.write(buf)?; + Ok(()) + } +} diff --git a/raves_metadata/src/providers/png/chunks/ihdr.rs b/raves_metadata/src/providers/png/chunks/ihdr.rs new file mode 100644 index 0000000..12714a9 --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/ihdr.rs @@ -0,0 +1,228 @@ +use crate::providers::png::{ + PngConstructionError, + chunks::{ChunkContext, PngChunkHeader}, + error::PngWriteError, +}; + +/// The first chunk in the PNG datastream. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Ihdr { + header: PngChunkHeader, + + width_px: u32, + height_px: u32, + + bit_depth: BitDepth, + color_type: ColorType, + + compression_method: CompressionMethod, + filter_method: FilterMethod, + interlace_method: InterlaceMethod, +} + +impl super::Chunk for Ihdr { + const TYPE: [u8; 4] = [0x49, 0x48, 0x44, 0x52]; + + fn read( + blob: &mut &[u8], + header: PngChunkHeader, + _context: ChunkContext, + ) -> Result { + let width_px: u32 = Self::read_u32(blob, "width")?; + let height_px: u32 = Self::read_u32(blob, "height")?; + + let bit_depth: BitDepth = BitDepth::new(Self::read_u8(blob, "bit_depth")?)?; + let color_type: ColorType = ColorType::new(Self::read_u8(blob, "color_type")?, bit_depth)?; + + let compression_method: CompressionMethod = + CompressionMethod::new(Self::read_u8(blob, "compression_method")?)?; + let filter_method: FilterMethod = FilterMethod::new(Self::read_u8(blob, "filter_method")?)?; + let interlace_method: InterlaceMethod = + InterlaceMethod::new(Self::read_u8(blob, "interlace_method")?)?; + + Ok(Self { + header, + + width_px, + height_px, + + bit_depth, + color_type, + + compression_method, + filter_method, + interlace_method, + }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + let Self { + header, + width_px, + height_px, + bit_depth, + color_type, + compression_method, + filter_method, + interlace_method, + } = self; + + // write the header + header.write(buf)?; + + // write the sizes + Self::write_u32(*width_px, buf, "width_px")?; + Self::write_u32(*height_px, buf, "height_px")?; + + // write the bit/color stuff + Self::write_u8(*bit_depth as u8, buf, "bit_depth")?; + Self::write_u8(*color_type as u8, buf, "color_type")?; + + // write *_methods + Self::write_u8(*compression_method as u8, buf, "compression_method")?; + Self::write_u8(*filter_method as u8, buf, "filter_method")?; + Self::write_u8(*interlace_method as u8, buf, "interlace_method")?; + + Ok(()) + } +} + +/// The bit depth of the image. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BitDepth { + One = 1, + Two = 2, + Four = 4, + Eight = 8, + Sixteen = 16, +} + +impl BitDepth { + /// Attempts to create a new `BitDepth` given a byte value. + pub fn new(byte: u8) -> Result { + Ok(match byte { + 1 => BitDepth::One, + 2 => BitDepth::Two, + 4 => BitDepth::Four, + 8 => BitDepth::Eight, + 16 => BitDepth::Sixteen, + + other => { + return Err(PngConstructionError::DisallowedBitDepth { found_value: other }) + .inspect_err(|_e| { + log::error!("Disallowed bit depth found in IHDR chunk: `{other}`") + }); + } + }) + } +} + +/// The color type of the image. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ColorType { + Greyscale = 0, + Truecolor = 2, + Indexed = 3, + GreyscaleAlpha = 4, + TruecolorAlpha = 6, +} + +impl ColorType { + /// Attempts to create a new `ColorType`. + pub fn new(byte: u8, bit_depth: BitDepth) -> Result { + let color_type: ColorType = match byte { + 0 => Self::Greyscale, + 2 => Self::Truecolor, + 3 => Self::Indexed, + 4 => Self::GreyscaleAlpha, + 6 => Self::TruecolorAlpha, + + other => { + return Err(PngConstructionError::DisallowedColorType { found_value: other }) + .inspect_err(|_e| { + log::error!("Disallowed color type found in IHDR chunk: `{other}`") + }); + } + }; + + let compatible: bool = match color_type { + Self::Greyscale => true, + Self::Truecolor => matches!(bit_depth, BitDepth::Eight | BitDepth::Sixteen), + Self::Indexed => matches!( + bit_depth, + BitDepth::One | BitDepth::Two | BitDepth::Four | BitDepth::Eight + ), + Self::GreyscaleAlpha | Self::TruecolorAlpha => { + matches!(bit_depth, BitDepth::Eight | BitDepth::Sixteen) + } + }; + + if !compatible { + return Err(PngConstructionError::BitDepthIncompatibleWithColorType { + bit_depth, + incompatible_color_type: color_type, + }); + } + + Ok(color_type) + } +} + +/// The compression method used to compress the image data. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CompressionMethod { + Deflate = 0, +} + +impl CompressionMethod { + /// Attempts to create a new `CompressionMethod`. + pub fn new(byte: u8) -> Result { + if byte == 0 { + Ok(Self::Deflate) + } else { + Err(PngConstructionError::DisallowedCompressionMethod { found_value: byte }) + } + } +} + +/// The filter method used to filter the image data before compression. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FilterMethod { + Adaptive = 0, +} + +impl FilterMethod { + /// Attempts to create a new `FilterMethod`. + pub fn new(byte: u8) -> Result { + if byte == 0 { + Ok(Self::Adaptive) + } else { + Err(PngConstructionError::DisallowedFilterMethod { found_value: byte }) + } + } +} + +/// The interlace method used to interlace the image data, if any. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InterlaceMethod { + None = 0, + Adam7 = 1, +} + +impl InterlaceMethod { + /// Attempts to create a new `InterlaceMethod`. + pub fn new(byte: u8) -> Result { + if byte == 0 { + Ok(Self::None) + } else if byte == 7 { + Ok(Self::Adam7) + } else { + Err(PngConstructionError::DisallowedInterlaceMethod { found_value: byte }) + } + } +} diff --git a/raves_metadata/src/providers/png/chunks/mod.rs b/raves_metadata/src/providers/png/chunks/mod.rs new file mode 100644 index 0000000..762bc0b --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/mod.rs @@ -0,0 +1,250 @@ +//! Includes support for all the different kinds of chunks in a PNG image. +//! +//! Each chunk here has the ability to be read and written. + +use winnow::{Parser, binary::be_u32, error::EmptyError, token::take}; + +use crate::providers::png::{PngConstructionError, error::PngWriteError}; + +pub mod chrm; +pub mod gama; +pub mod iccp; +pub mod idat; +pub mod iend; +pub mod ihdr; +pub mod plte; +pub mod trns; + +/// A "chunk" of PNG data. +pub trait Chunk: Sized { + /// The "type" identifier for a PNG chunk. + /// + /// Each chunk has a unique type, meaning that this value identifies each + /// chunk exactly. + const TYPE: [u8; 4]; + + /// A printable, stringy version of the chunk type. + /// + /// Used for error messages and logging. + const TYPE_STR: &str = const { + // convert the `TYPE` byte array into a string. + // + // note that we have to handle the option that it's not UTF-8, but we + // do so in `const`, meaning that a panic here can only happen at + // compile-time ;D + let Ok(type_str) = core::str::from_utf8(&Self::TYPE) else { + panic!() + }; + + type_str + }; + + /// Reads this `Chunk` from the `blob`, given a chunk header that's already + /// been parsed out of the blob. + fn read( + blob: &mut &[u8], + header: PngChunkHeader, + context: ChunkContext, + ) -> Result; + + /// Writes this chunk into the given buffer, `buf`. + fn write(&self, buf: &mut W) -> Result<(), PngWriteError>; + + /// Reads a byte from the given `blob`. + fn read_u8(blob: &mut &[u8], field: &'static str) -> Result { + winnow::binary::u8 + .parse_next(blob) + .map_err(|_e: EmptyError| { + log::error!( + "Can't get `u8`! Outta bytes when attempting to read chunk: `{}` \ + for field: `{field}`.", + Self::TYPE_STR + ); + PngConstructionError::OuttaBytes { + chunk_type: Self::TYPE_STR, + expected: 1_u32, + remaining: 0_u32, + } + }) + } + + /// Writes a byte to the given `buf`. + fn write_u8( + value: u8, + buf: &mut W, + field: &'static str, + ) -> Result<(), PngWriteError> { + buf.write_all(&[value]).map_err(|_| { + log::error!( + "Failed to write `u8` value to buffer! \ + Buffer type: {}, \ + Chunk type: {}, \ + Field: {field}", + core::any::type_name_of_val(buf), + Self::TYPE_STR + ); + PngWriteError::CantWriteToBuf {} + }) + } + + /// Reads a `u16` from the given `blob`. + fn read_u16(blob: &mut &[u8], field: &'static str) -> Result { + winnow::binary::be_u16 + .parse_next(blob) + .map_err(|_e: EmptyError| { + log::error!( + "Can't get `u16`! Outta bytes when attempting to read chunk: `{}` \ + for field: `{field}`.", + Self::TYPE_STR + ); + PngConstructionError::OuttaBytes { + chunk_type: Self::TYPE_STR, + expected: 2_u32, + remaining: blob.len() as u32, + } + }) + } + + /// Writes a `u16` to the given `buf`. + fn write_u16( + value: u16, + buf: &mut W, + field: &'static str, + ) -> Result<(), PngWriteError> { + buf.write_all(&value.to_be_bytes()).map_err(|_| { + log::error!( + "Failed to write `u16` value to buffer! \ + Buffer type: {}, \ + Chunk type: {}, \ + Field: {field}", + core::any::type_name_of_val(buf), + Self::TYPE_STR + ); + PngWriteError::CantWriteToBuf {} + }) + } + + /// Reads a `u32` from the given `blob`. + fn read_u32(blob: &mut &[u8], field: &'static str) -> Result { + winnow::binary::be_u32 + .parse_next(blob) + .map_err(|_e: EmptyError| { + log::error!( + "Can't get `u32`! Outta bytes when attempting to read chunk: `{}` \ + for field: `{field}`.", + Self::TYPE_STR + ); + PngConstructionError::OuttaBytes { + chunk_type: Self::TYPE_STR, + expected: 4_u32, + remaining: blob.len() as u32, + } + }) + } + + /// Writes a `u32` to the given `buf`. + fn write_u32( + value: u32, + buf: &mut W, + field: &'static str, + ) -> Result<(), PngWriteError> { + buf.write_all(&value.to_be_bytes()).map_err(|_| { + log::error!( + "Failed to write `u32` value to buffer! \ + Buffer type: {}, \ + Chunk type: {}, \ + Field: {field}", + core::any::type_name_of_val(buf), + Self::TYPE_STR + ); + PngWriteError::CantWriteToBuf {} + }) + } + + /// Writes a byte slice to the given buffer. + fn write_byte_slice( + value: &[u8], + buf: &mut W, + field: &'static str, + ) -> Result<(), PngWriteError> { + buf.write_all(value).map_err(|_| { + log::error!( + "Failed to write byte slice to buffer! \ + Buffer type: {}, \ + Chunk type: {}, \ + Field: {field}", + core::any::type_name_of_val(buf), + Self::TYPE_STR + ); + PngWriteError::CantWriteToBuf {} + }) + } +} + +/// Context relating to parsing/writing a chunk. +/// +/// Note that the 'par lifetime is related to parser borrows -- meaning the +/// crate's internal parser provides the data. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub enum ChunkContext<'par> { + /// The primary transparency info chunk. + Trns(&'par trns::TrnsContext), + + /// This chunk doesn't take context. + Other, +} + +/// A header for a PNG chunk. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct PngChunkHeader { + pub chunk_length: u32, // in bytes + pub chunk_ident: [u8; 4], // four ascii letters +} + +impl PngChunkHeader { + /// Reads the chunk header from the blob (byte slice). + pub fn read(blob: &mut &[u8]) -> Result { + let chunk_length: u32 = be_u32.parse_next(blob).map_err(|_e: EmptyError| { + log::error!( + "Chunk length couldn't be parsed from the blob. \ + Outta bytes to consume!" + ); + PngConstructionError::NotEnoughBytesForChunkHeader + })?; + + let chunk_ident: [u8; 4] = take(4_usize) + .parse_next(blob) + .map_err(|_e: EmptyError| { + log::error!( + "Chunk identifier couldn't be parsed from the blob. \ + Outta bytes to consume!" + ); + PngConstructionError::NotEnoughBytesForChunkHeader + })? + .try_into() + .unwrap_or_else(|e| { + unreachable!("winnow already said this must be 4 bytes. but err: {e}") + }); + + Ok(PngChunkHeader { + chunk_length, + chunk_ident, + }) + } + + /// Writes a chunk header to the buffer, `buf`. + pub fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + buf.write_all(&self.chunk_length.to_be_bytes()) + .map_err(|_| { + log::error!("Failed to write chunk length!"); + PngWriteError::CantWriteToBuf {} + })?; + + buf.write_all(&self.chunk_ident).map_err(|_| { + log::error!("Failed to write chunk length!"); + PngWriteError::CantWriteToBuf {} + })?; + + Ok(()) + } +} diff --git a/raves_metadata/src/providers/png/chunks/plte.rs b/raves_metadata/src/providers/png/chunks/plte.rs new file mode 100644 index 0000000..4daadc3 --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/plte.rs @@ -0,0 +1,86 @@ +use crate::providers::png::{ + PngConstructionError, + chunks::{ChunkContext, PngChunkHeader}, + error::PngWriteError, +}; + +/// A collection of palettes. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Plte { + header: PngChunkHeader, + entries: Vec, +} + +/// The maximum number of palettes a `PLTE` chunk may carry. +pub const MAX_PALETTES: u32 = 256_u32; + +impl super::Chunk for Plte { + const TYPE: [u8; 4] = *b"PLTE"; + + fn read( + blob: &mut &[u8], + header: super::PngChunkHeader, + _context: ChunkContext, + ) -> Result { + // check if chunk len is messed up + if !header.chunk_length.is_multiple_of(3) || blob.is_empty() { + log::error!( + "PLTE chunk length is not a multiple of 3! \ + Can't continue parsing it..." + ); + return Err(PngConstructionError::PlteNotMultipleOfThree { + found_chunk_length: header.chunk_length, + }); + } + + // check if we've got too many palettes + let palette_count: u32 = header.chunk_length / 3; + if palette_count > MAX_PALETTES { + log::error!("PLTE chunk had too many palettes! ({palette_count})"); + return Err(PngConstructionError::PlteTooManyPalettes { + palette_ct: palette_count, + }); + } + + // nope, all good! write each palette + let mut entries: Vec = Vec::with_capacity(palette_count as usize); + for _ in 0..palette_count { + entries.push(Palette { + red: Self::read_u8(blob, "red")?, + green: Self::read_u8(blob, "green")?, + blue: Self::read_u8(blob, "blue")?, + }); + } + + Ok(Self { header, entries }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + // check palette ct before writing + if self.entries.is_empty() || self.entries.len() as u32 > MAX_PALETTES { + return Err(PngWriteError::PltePaletteCount { + palette_ct: self.entries.len() as u32, + }); + } + + // write header + self.header.write(buf)?; + + // write the palettes + for Palette { red, green, blue } in &self.entries { + Self::write_u8(*red, buf, "red")?; + Self::write_u8(*green, buf, "green")?; + Self::write_u8(*blue, buf, "blue")?; + } + + Ok(()) + } +} + +/// A color. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Palette { + pub red: u8, + pub green: u8, + pub blue: u8, +} diff --git a/raves_metadata/src/providers/png/chunks/trns.rs b/raves_metadata/src/providers/png/chunks/trns.rs new file mode 100644 index 0000000..58c6ff1 --- /dev/null +++ b/raves_metadata/src/providers/png/chunks/trns.rs @@ -0,0 +1,138 @@ +use crate::providers::png::{ + chunks::{Chunk, ChunkContext, PngChunkHeader}, + error::{PngConstructionError, PngWriteError}, +}; + +/// Specifies colors relating to transparency. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub struct Trns { + header: PngChunkHeader, + data: TrnsData, +} + +/// The data behind the `Trns` type. +/// +/// Separated to disallow access/mutation by crate users. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub enum TrnsData { + ColorType0 { + grey_sample_value: u16, + }, + + ColorType2 { + red_sample_value: u16, + green_sample_value: u16, + blue_sample_value: u16, + }, + + ColorType3 { + alpha_per_palette: Vec, + }, +} + +/// Context given to the tRNS reader. +/// +/// Based on the `color_type: ColorType` from the IHDR chunk. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub enum TrnsContext { + /// Type 0 doesn't need any info. + ColorType0, + + /// Neither does type 2. + ColorType2, + + /// Type 3 needs to know how many entries are in the PLTE chunk. + ColorType3 { number_of_plte_entries: u32 }, + + /// Other color types aren't permitted for tRNS -- error out! + NotPermitted, +} + +impl Chunk for Trns { + const TYPE: [u8; 4] = *b"tRNS"; + + fn read( + blob: &mut &[u8], + header: super::PngChunkHeader, + context: ChunkContext, + ) -> Result { + // ensure context is the right kind + let ChunkContext::Trns(trns_ctx) = context else { + log::error!( + "INTERNAL ERROR: `Trns::read` with wrong type of context! \ + Please create an issue on the `raves-project/raves_metadata` GitHub repo. \ + context: {context:?}" + ); + std::process::abort(); + }; + + // grab data (depending on color type) + let data: TrnsData = match trns_ctx { + TrnsContext::ColorType0 => TrnsData::ColorType0 { + grey_sample_value: Self::read_u16(blob, "grey_sample_value (type 0)")?, + }, + + TrnsContext::ColorType2 => TrnsData::ColorType2 { + red_sample_value: Self::read_u16(blob, "red_sample_value (type 2)")?, + green_sample_value: Self::read_u16(blob, "green_sample_value (type 2)")?, + blue_sample_value: Self::read_u16(blob, "blue_sample_value (type 2)")?, + }, + + TrnsContext::ColorType3 { + number_of_plte_entries, + } => { + let mut v: Vec<_> = Vec::with_capacity(*number_of_plte_entries as usize); + for _ in 0..*number_of_plte_entries { + v.push(Self::read_u8(blob, "alpha (type 3)")?); + } + TrnsData::ColorType3 { + alpha_per_palette: v, + } + } + + // error out on other types + TrnsContext::NotPermitted => { + log::error!( + "TRNS chunk was found, but the color type (from IHDR) \ + says that this chunk shouldn't be in the datastream. \ + Returning an error..." + ); + return Err(PngConstructionError::TrnsGivenDisallowedType); + } + }; + + // all good! just store its header + Ok(Self { header, data }) + } + + fn write(&self, buf: &mut W) -> Result<(), PngWriteError> { + // write header + self.header.write(buf)?; + + // write depending on the data... + match self.data { + TrnsData::ColorType0 { grey_sample_value } => { + Self::write_u16(grey_sample_value, buf, "grey_sample_value (type 0)")?; + } + TrnsData::ColorType2 { + red_sample_value, + green_sample_value, + blue_sample_value, + } => { + Self::write_u16(red_sample_value, buf, "red_sample_value (type 2)")?; + Self::write_u16(green_sample_value, buf, "green_sample_value (type 2)")?; + Self::write_u16(blue_sample_value, buf, "blue_sample_value (type 2)")?; + } + + TrnsData::ColorType3 { + ref alpha_per_palette, + } => { + for alpha_value in alpha_per_palette.iter() { + Self::write_u8(*alpha_value, buf, "alpha_value (type 3)")?; + } + } + } + + Ok(()) + } +} diff --git a/raves_metadata/src/providers/png/error.rs b/raves_metadata/src/providers/png/error.rs new file mode 100644 index 0000000..b146194 --- /dev/null +++ b/raves_metadata/src/providers/png/error.rs @@ -0,0 +1,179 @@ +use crate::providers::png::chunks::{ + ihdr::{BitDepth, ColorType}, + trns::TrnsContext, +}; + +/// An error that occurs when constructing a [`Png`] for its metadata. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub enum PngConstructionError { + /// The file ran out of bytes before we could check for a signature. + /// + /// It might be empty. + NoSignature, + + /// No PNG signature was detected. + NotAPng { + /// The signature that was found instead. + found: [u8; 8], + }, + + /// Unexpectedly ran out of bytes when parsing a chunk. + OuttaBytes { + /// The chunk's name. (e.g., `IHDR`) + chunk_type: &'static str, + /// The number of bytes that we tried to get. + expected: u32, + /// The number of bytes that were actually remaining. + remaining: u32, + }, + + /// Failed to parse chunk header. + NotEnoughBytesForChunkHeader, + + /// In the IHDR chunk, a disallowed bit depth was found. + DisallowedBitDepth { + /// The weird bit depth value we found. + found_value: u8, + }, + + /// In the IHDR chunk, a disallowed bit depth and color type pair was + /// found. + BitDepthIncompatibleWithColorType { + /// The bit depth value provided. + bit_depth: BitDepth, + + /// The expected color type. + incompatible_color_type: ColorType, + }, + + /// In the IHDR chunk, a disallowed color type byte was found. + DisallowedColorType { + /// The weird color type byte we found. + found_value: u8, + }, + + /// In the IHDR chunk, a weird compression method was found. + DisallowedCompressionMethod { + /// The disallowed byte value. + found_value: u8, + }, + + /// In the IHDR chunk, a weird filter method was found. + DisallowedFilterMethod { + /// The disallowed byte value. + found_value: u8, + }, + + /// In the IHDR chunk, a weird interlace method was found. + DisallowedInterlaceMethod { + /// The disallowed byte value. + found_value: u8, + }, + + /// In the PLTE chunk, the chunk length was not a multiple of 3. + PlteNotMultipleOfThree { + /// The chunk length we got. + found_chunk_length: u32, + }, + + /// In the PLTE chunk, there were too many palettes (>256). + PlteTooManyPalettes { + /// The number of palettes found. + palette_ct: u32, + }, + + /// The IEND chunk had a non-zero chunk length, but that's not allowed. + IendHadData { + /// The (non-zero) chunk length for this chunk. + chunk_length: u32, + }, + + /// The tRNS chunk should not exist if the color type is disallowed, but + /// the parser found a tRNS chunk in the datastream anyway. + TrnsGivenDisallowedType, + + /// An iCCP chunk profile name should be fully Latin-1-encoded, but a + /// character wasn't in Latin-1 bounds. + IccpProfileNameNotLatin1 { + /// The violating character. + c: u8, + }, + + /// The iCCP chunk profile name violated PNG's structural requirements. + IccpInvalidProfileName { + /// The reason the profile name was invalid. + reason: &'static str, + }, + + /// The iCCP chunk's compression method should always be zero, but another + /// value was found. + IccpUnknownCompressionValue { + /// The non-zero value that was found. + value: u8, + }, +} + +impl core::fmt::Display for PngConstructionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + const NOT_A_PNG_MSG: &str = "The given file's signature indicated it was not a PNG"; + + match self { + PngConstructionError::NoSignature => { + f.write_str("File didn't have enough bytes for a signature.") + } + + PngConstructionError::NotAPng { found } => match core::str::from_utf8(found) { + Ok(utf8_found) => write!( + f, + "{NOT_A_PNG_MSG}. Signature was: `{found:?}`. (UTF-8: `{utf8_found}`)" + ), + Err(_) => write!( + f, + "{NOT_A_PNG_MSG}. Signature was: `{found:?}`. (Not valid UTF-8.)`" + ), + }, + + Self::OuttaBytes { + chunk_type, + expected, + remaining, + } => write!( + f, + "Unexpectedly ran out of bytes when parsing chunk: `{chunk_type}`. \ + Expected `{expected}` more bytes, \ + but only found `{remaining}` bytes.", + ), + + other_TODO => todo!(), + } + } +} + +impl core::error::Error for PngConstructionError {} + +/// An error that can occur when writing a PNG back to disk. +#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] +pub enum PngWriteError { + CantWriteToBuf {}, + + /// On the PLTE chunk, the number of palettes was either 0 or >256. + PltePaletteCount { + /// The number of palettes. + palette_ct: u32, + }, + + /// An iCCP chunk profile name should be fully Latin-1-encoded, but a + /// character wasn't in Latin-1 bounds. + IccpProfileNameNotLatin1 { + /// The violating character. + c: u8, + }, + + /// The iCCP chunk profile name violated PNG's structural requirements. + IccpInvalidProfileName { + /// The reason the profile name was invalid. + reason: &'static str, + }, +} + +fn TODO_impl_error_for_PngWriteError() {} diff --git a/raves_metadata/src/providers/png.rs b/raves_metadata/src/providers/png/mod.rs similarity index 91% rename from raves_metadata/src/providers/png.rs rename to raves_metadata/src/providers/png/mod.rs index 1a478ae..42e50f3 100644 --- a/raves_metadata/src/providers/png.rs +++ b/raves_metadata/src/providers/png/mod.rs @@ -13,6 +13,11 @@ use winnow::{ token::{literal, rest, take}, }; +pub mod chunks; +mod error; + +pub use error::PngConstructionError; + /// A signature indicating that a file is a PNG. pub const PNG_SIGNATURE: &[u8; 8] = &[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; @@ -298,46 +303,6 @@ fn try_to_parse_xmp_from_itxt<'input>( }) } -/// An error that occurs when constructing a [`Png`] for its metadata. -#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)] -pub enum PngConstructionError { - /// The file ran out of bytes before we could check for a signature. - /// - /// It might be empty. - NoSignature, - - /// No PNG signature was detected. - NotAPng { - /// The signature that was found instead. - found: [u8; 8], - }, -} - -impl core::fmt::Display for PngConstructionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - const NOT_A_PNG_MSG: &str = "The given file's signature indicated it was not a PNG"; - - match self { - PngConstructionError::NoSignature => { - f.write_str("File didn't have enough bytes for a signature.") - } - - PngConstructionError::NotAPng { found } => match core::str::from_utf8(found) { - Ok(utf8_found) => write!( - f, - "{NOT_A_PNG_MSG}. Signature was: `{found:?}`. (UTF-8: `{utf8_found}`)" - ), - Err(_) => write!( - f, - "{NOT_A_PNG_MSG}. Signature was: `{found:?}`. (Not valid UTF-8.)`" - ), - }, - } - } -} - -impl core::error::Error for PngConstructionError {} - #[cfg(test)] mod tests { @@ -478,7 +443,7 @@ mod tests { #[test] fn blank_sample_with_exif() { logger(); - const BLOB: &[u8] = include_bytes!("../../assets/providers/png/exif.png"); + const BLOB: &[u8] = include_bytes!("../../../assets/providers/png/exif.png"); let png: Png = Png::new(&BLOB).expect("parse PNG");