Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions raves_metadata/src/providers/png/chunks/chrm.rs
Original file line number Diff line number Diff line change
@@ -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<Self, PngConstructionError> {
// 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<W: std::io::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(())
}
}
38 changes: 38 additions & 0 deletions raves_metadata/src/providers/png/chunks/gama.rs
Original file line number Diff line number Diff line change
@@ -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<Self, PngConstructionError> {
// 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<W: std::io::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(())
}
}
174 changes: 174 additions & 0 deletions raves_metadata/src/providers/png/chunks/iccp.rs
Original file line number Diff line number Diff line change
@@ -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<u8>, // 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<Self, PngConstructionError> {
// check that there's no context
debug_assert_eq!(context, ChunkContext::Other);

let mut profile_name_bytes: Vec<u8> = 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<u8> = blob.to_vec();
*blob = &[];

Ok(Self {
header,
profile_name,
compression_method,
compressed_profile,
})
}

fn write<W: std::io::Write>(&self, buf: &mut W) -> Result<(), PngWriteError> {
// write the header
self.header.write(buf)?;

let profile_name_bytes: Vec<u8> = 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::<Result<Vec<u8>, 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)
}
60 changes: 60 additions & 0 deletions raves_metadata/src/providers/png/chunks/idat.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
}

impl Chunk for Idat {
const TYPE: [u8; 4] = *b"IDAT";

fn read(
blob: &mut &[u8],
header: super::PngChunkHeader,
_context: ChunkContext,
) -> Result<Self, PngConstructionError> {
let mut entries: Vec<u8> = 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<W: std::io::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(())
}
}
39 changes: 39 additions & 0 deletions raves_metadata/src/providers/png/chunks/iend.rs
Original file line number Diff line number Diff line change
@@ -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<Self, PngConstructionError> {
// 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<W: std::io::Write>(&self, buf: &mut W) -> Result<(), PngWriteError> {
// write header
self.header.write(buf)?;
Ok(())
}
}
Loading
Loading