use parking_lot::RwLock;
use std::sync::Arc;
use crate::{MaybeParsedExif, MaybeParsedXmp, MetadataProvider, MetadataProviderRaw};
use winnow::{
binary::be_u32,
combinator::peek,
error::{ContextError, EmptyError, ErrMode, StrContext, StrContextValue},
prelude::*,
token::{literal, rest, take},
};
pub const PNG_SIGNATURE: &[u8; 8] = &[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
#[derive(Clone, Debug)]
pub struct Png {
exif: Arc<RwLock<Option<MaybeParsedExif>>>,
xmp: Arc<RwLock<Option<MaybeParsedXmp>>>,
}
impl MetadataProviderRaw for Png {
fn exif_raw(&self) -> Arc<RwLock<Option<MaybeParsedExif>>> {
Arc::clone(&self.exif)
}
fn xmp_raw(&self) -> Arc<RwLock<Option<MaybeParsedXmp>>> {
Arc::clone(&self.xmp)
}
}
impl MetadataProvider for Png {
type ConstructionError = PngConstructionError;
fn magic_number(input: &[u8]) -> bool {
let mut input = input;
parse_signature(&mut input).is_ok()
}
fn new(
input: &impl AsRef<[u8]>,
) -> Result<Self, <Self as MetadataProvider>::ConstructionError> {
let mut input = input.as_ref();
parse_signature(&mut input)?;
log::trace!("Found a PNG signature! Continuing with chunk parsing.");
let GetMetadata { exif, xmp } = get_metadata(&mut input);
Ok(Self {
exif: Arc::new(RwLock::new(exif.map(|p| MaybeParsedExif::Raw(p.into())))),
xmp: Arc::new(RwLock::new(xmp.map(|r| MaybeParsedXmp::Raw(r.into())))),
})
}
}
fn parse_signature(input: &mut &[u8]) -> Result<(), PngConstructionError> {
log::trace!("Attempting to parse out the PNG signature...");
let signature: &[u8; 8] = take(8_usize)
.parse_next(input)
.map(|s| TryInto::try_into(s).unwrap_or_else(|_| unreachable!()))
.map_err(|e: ContextError| {
log::warn!(
"This \"PNG\" didn't contain its required PNG signature. \
Is it actually a PNG..? \
err: {e}"
);
PngConstructionError::NoSignature
})?;
parse_png_signature.parse(signature).map_err(|e| {
log::warn!(
"Signature obtained from given file did not match a PNG! \
err: {e}, \
found: `{signature:?}`
"
);
PngConstructionError::NotAPng { found: *signature }
})?;
Ok(())
}
struct PngChunkHeader {
pub chunk_length: u32, pub chunk_ident: [u8; 4], }
fn parse_png_signature(input: &mut &[u8]) -> ModalResult<(), ContextError> {
literal(PNG_SIGNATURE).void().parse_next(input)
}
fn parse_chunk_header(input: &mut &[u8]) -> ModalResult<PngChunkHeader, ContextError> {
let chunk_length: u32 = be_u32
.context(StrContext::Label("chunk length"))
.parse_next(input)?;
let chunk_ident: [u8; 4] = take(4_usize)
.context(StrContext::Label("ASCII chunk identifier"))
.parse_next(input)?
.try_into()
.unwrap_or_else(|e| unreachable!("winnow already said this must be 4 bytes. but err: {e}"));
Ok(PngChunkHeader {
chunk_length,
chunk_ident,
})
}
struct GetMetadata<'input> {
exif: Option<&'input [u8]>,
xmp: Option<&'input str>,
}
pub const EXIF_CHUNK_IDENT: [u8; 4] = *b"eXIf";
fn get_metadata<'input>(input: &mut &'input [u8]) -> GetMetadata<'input> {
let mut metadata: GetMetadata = GetMetadata {
exif: None,
xmp: None,
};
while !input.is_empty() {
if metadata.exif.is_some() && metadata.xmp.is_some() {
break;
}
let Ok(PngChunkHeader {
chunk_length,
chunk_ident,
}) = parse_chunk_header.parse_next(input)
else {
log::warn!("Failed to parse PNG chunk header!");
break;
};
log::trace!(
"Found chunk with ident: `{}`",
core::str::from_utf8(&chunk_ident).unwrap_or("not UTF-8")
);
if chunk_ident == EXIF_CHUNK_IDENT {
match peek(take::<_, _, EmptyError>(chunk_length)).parse_next(input) {
Ok(exif_blob) => {
_ = take::<_, _, EmptyError>(chunk_length)
.void()
.parse_next(input);
_ = take::<_, _, EmptyError>(4_usize).void().parse_next(input); log::trace!("Chunk had Exif data!");
metadata.exif = Some(exif_blob);
continue;
}
Err(_) => {
log::error!("Failed to parse out Exif blob from Exif chunk!");
}
}
}
if &chunk_ident == b"iTXt" {
log::trace!("Chunk is iTXt. Checking if it contains XMP...");
let Ok::<_, EmptyError>(ref mut chunk_data) =
peek(take(chunk_length as usize)).parse_next(input)
else {
log::warn!(
"Couldn't find enough data inside `iTXt`! expected: `{chunk_length}`, got: `{}`",
input.len()
);
break;
};
let Ok(maybe_xmp) = try_to_parse_xmp_from_itxt(chunk_data) else {
log::warn!("Failed to parse any XMP data from chunk!");
break;
};
if let Some(xmp) = maybe_xmp {
log::trace!("Chunk contained XMP data!");
metadata.xmp = Some(xmp);
}
}
if take::<_, _, EmptyError>(chunk_length as usize)
.void()
.parse_next(input)
.is_err()
{
break;
};
if take::<_, _, EmptyError>(4_usize)
.void()
.parse_next(input)
.is_err()
{
break;
};
}
metadata
}
fn try_to_parse_xmp_from_itxt<'input>(
input: &mut &'input [u8],
) -> ModalResult<Option<&'input str>, ContextError> {
if !input.starts_with(b"XML:com.adobe.xmp") {
log::trace!("Input doesn't contain the desired XMP keyword (marker). Moving on...");
return Ok(None);
}
log::trace!("Found expected keyword for XMP!");
literal(b"XML:com.adobe.xmp").void().parse_next(input)?;
log::trace!("Ate XMP keyword. Continuing to grab from input...");
literal(0_u8).void().parse_next(input)?;
literal(0_u8)
.context(StrContext::Expected(StrContextValue::Description(
"to be marked as uncompressed text (0x0)",
)))
.void()
.parse_next(input)?;
literal(0_u8)
.context(StrContext::Expected(StrContextValue::Description(
"no specified compression method (0x0)",
)))
.void()
.parse_next(input)?;
literal(0_u8).void().parse_next(input)?;
literal(0_u8).void().parse_next(input)?;
let the_rest: &[u8] = rest.parse_next(input)?;
core::str::from_utf8(the_rest)
.map(|s: &str| Some(s))
.map_err(|_e| {
let mut ce = ContextError::new();
ce.push(StrContext::Expected(
winnow::error::StrContextValue::StringLiteral("XMP wasn't UTF-8!"),
));
ErrMode::Cut(ce)
})
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Hash)]
pub enum PngConstructionError {
NoSignature,
NotAPng {
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 {
use raves_metadata_types::{
exif::{
Field, FieldData, FieldTag,
primitives::{Primitive, Rational},
tags::{Ifd0Tag, KnownTag},
},
xmp::{XmpElement, XmpValue},
};
use crate::{MetadataProvider as _, providers::png::Png, util::logger};
#[test]
fn png_signature_parsing() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
assert_eq!(
Ok(()),
super::parse_png_signature(
&mut [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].as_slice()
),
"we should successfully parse a PNG signature",
)
}
#[test]
fn png_containing_xmp_parses_correctly() {
_ = env_logger::builder()
.filter_level(log::LevelFilter::max())
.format_file(true)
.format_line_number(true)
.try_init();
#[rustfmt::skip]
let technically_a_png: Vec<u8> = [
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].as_slice(),
[
0x0, 0x0, 0x0, 0xD, b'I', b'H', b'D', b'R', 0x0, 0x0, 0xA, 0x0, 0x0, 0x0, 0x5, 0xA0, 8, 6, 0, 0, 0, 0xB3, 0xE4, 0x34, 0x52, ].as_slice(),
[
0x0, 0x0, 0x0, 0x1D, b'i', b'T', b'X', b't', b'S', b'o', b'f', b't', b'w', b'a', b'r', b'e', b'\0', 0x00, 0x00, b'e', b'n', b'-', b'U', b'S', b'\0', b'S', b'o', b'f', b't', b'w', b'a', b'r', b'e', b'\0', b'H', b'i', b'!', 0x69, 0x5C, 0x21, 0xB2, ].as_slice(),
[
[
0x0, 0x0, 0x1, 0x67, b'i', b'T', b'X', b't', b'X', b'M', b'L', b':', b'c', b'o', b'm', b'.', b'a', b'd', b'o', b'b', b'e', b'.', b'x', b'm', b'p', b'\0', b'\0', b'\0', 0x00, 0x00, ].as_slice(),
r#"<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:my_ns="https://barretts.club">
<my_ns:MyStruct>
<rdf:Description />
</my_ns:MyStruct>
</rdf:Description>
</rdf:RDF>"#.as_bytes(),
[0xDC, 0xFD, 0x6E, 0x88].as_slice(), ]
.into_iter()
.flat_map(|sli| sli.iter().copied())
.collect::<Vec<_>>()
.as_slice(),
[0x49, 0x45, 0x4E, 0x44].as_slice(),
]
.into_iter()
.flat_map(|sli| sli.iter().copied())
.collect();
let png: Png = Png::new(&technically_a_png).expect("is a png");
let xmp = png
.xmp()
.expect("this PNG has XMP")
.expect("get XMP from PNG");
let locked_xmp = xmp.read();
assert_eq!(
locked_xmp.document().values_ref().len(),
1_usize,
"should only parse that one struct"
);
assert_eq!(
locked_xmp
.document()
.values_ref()
.first()
.expect("must have an item"),
&XmpElement {
namespace: "https://barretts.club".into(),
prefix: "my_ns".into(),
name: "MyStruct".into(),
value: XmpValue::Struct(Vec::new()),
},
"found struct should match the expected (right) side"
)
}
#[test]
fn blank_sample_with_exif() {
logger();
const BLOB: &[u8] = include_bytes!("../../assets/providers/png/exif.png");
let png: Png = Png::new(&BLOB).expect("parse PNG");
let exif = png
.exif()
.expect("PNG contains Exif")
.expect("Exif is well-formed");
let exif_locked = exif.read();
let a = exif_locked.ifds.first().unwrap();
let expected_field_tag = FieldTag::Known(KnownTag::Ifd0Tag(Ifd0Tag::XResolution));
assert_eq!(
*a.fields
.iter()
.flatten()
.find(|f| f.tag == expected_field_tag)
.expect("find xres field"),
Field {
tag: expected_field_tag,
data: FieldData::Primitive(Primitive::Rational(Rational {
numerator: 144,
denominator: 1
}))
}
)
}
}