Skip to content
Merged
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
20 changes: 20 additions & 0 deletions src/codecs/png.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
// http://www.w3.org/TR/PNG-Structure.html
// The first eight bytes of a PNG file always contain the following (decimal) values:
pub(crate) const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
const XMP_KEY: &str = "XML:com.adobe.xmp";

/// PNG decoder
pub struct PngDecoder<R: BufRead + Seek> {
Expand All @@ -45,6 +46,7 @@ impl<R: BufRead + Seek> PngDecoder<R> {

let max_bytes = usize::try_from(limits.max_alloc.unwrap_or(u64::MAX)).unwrap_or(usize::MAX);
let mut decoder = png::Decoder::new_with_limits(r, png::Limits { bytes: max_bytes });
decoder.set_ignore_text_chunk(false);

let info = decoder.read_header_info().map_err(ImageError::from_png)?;
limits.check_dimensions(info.width, info.height)?;
Expand Down Expand Up @@ -186,6 +188,24 @@ impl<R: BufRead + Seek> ImageDecoder for PngDecoder<R> {
.map(|x| x.to_vec()))
}

fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
if let Some(mut itx_chunk) = self
.reader
.info()
.utf8_text
.iter()
.find(|chunk| chunk.keyword.contains(XMP_KEY))
.cloned()
{
itx_chunk.decompress_text().map_err(ImageError::from_png)?;
return itx_chunk
.get_text()
.map(|text| Some(text.as_bytes().to_vec()))
.map_err(ImageError::from_png);
}
Ok(None)
}

fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> {
use byteorder_lite::{BigEndian, ByteOrder, NativeEndian};

Expand Down
20 changes: 20 additions & 0 deletions src/codecs/tiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use crate::error::{
use crate::metadata::Orientation;
use crate::{utils, ImageDecoder, ImageEncoder, ImageFormat};

const TAG_XML_PACKET: Tag = Tag::Unknown(700);

/// Decoder for TIFF images.
pub struct TiffDecoder<R>
where
Expand Down Expand Up @@ -264,6 +266,24 @@ impl<R: BufRead + Seek> ImageDecoder for TiffDecoder<R> {
}
}

fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
let Some(decoder) = &mut self.inner else {
return Ok(None);
};

let value = match decoder.get_tag(TAG_XML_PACKET) {
Ok(value) => value,
Err(tiff::TiffError::FormatError(tiff::TiffFormatError::RequiredTagNotFound(_))) => {
return Ok(None);
}
Err(err) => return Err(ImageError::from_tiff_decode(err)),
};
value
.into_u8_vec()
.map(Some)
.map_err(ImageError::from_tiff_decode)
}

fn orientation(&mut self) -> ImageResult<Orientation> {
if let Some(decoder) = &mut self.inner {
Ok(decoder
Expand Down
6 changes: 6 additions & 0 deletions src/codecs/webp/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ impl<R: BufRead + Seek> ImageDecoder for WebPDecoder<R> {
Ok(exif)
}

fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
self.inner
.xmp_metadata()
.map_err(ImageError::from_webp_decode)
}

fn orientation(&mut self) -> ImageResult<Orientation> {
// `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet.
if self.orientation.is_none() {
Expand Down
11 changes: 11 additions & 0 deletions src/io/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ pub trait ImageDecoder {
Ok(None)
}

/// Returns the raw [XMP](https://en.wikipedia.org/wiki/Extensible_Metadata_Platform) chunk, if it is present.
/// A third-party crate such as [`roxmltree`](https://docs.rs/roxmltree/) is required to actually parse it.
///
/// For formats that don't support embedded profiles this function should always return `Ok(None)`.
fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
Ok(None)
}

/// Returns the orientation of the image.
///
/// This is usually obtained from the Exif metadata, if present. Formats that don't support
Expand Down Expand Up @@ -134,6 +142,9 @@ impl<T: ?Sized + ImageDecoder> ImageDecoder for Box<T> {
fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
(**self).exif_metadata()
}
fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
(**self).xmp_metadata()
}
fn orientation(&mut self) -> ImageResult<Orientation> {
(**self).orientation()
}
Expand Down
Binary file added tests/images/png/transparency/tp1n3p08_xmp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/images/tiff/testsuite/l1_xmp.tiff
Binary file not shown.
Binary file added tests/images/webp/lossless_images/simple_xmp.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions tests/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;

use image::ImageDecoder;

#[cfg(feature = "png")]
use image::codecs::png::PngDecoder;
#[cfg(feature = "tiff")]
use image::codecs::tiff::TiffDecoder;
#[cfg(feature = "webp")]
use image::codecs::webp::WebPDecoder;

extern crate glob;
extern crate image;

const XMP_PNG_PATH: &str = "tests/images/png/transparency/tp1n3p08_xmp.png";
const EXPECTED_PNG_METADATA: &str = "<?xpacket begin='\u{feff}' id='W5M0MpCehiHzreSzNTczkc9d'?>\n<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 13.25'>\n<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n\n <rdf:Description rdf:about=''\n xmlns:dc='http://purl.org/dc/elements/1.1/'>\n <dc:subject>\n <rdf:Bag>\n <rdf:li>sunset, mountains, nature</rdf:li>\n </rdf:Bag>\n </dc:subject>\n </rdf:Description>\n</rdf:RDF>\n</x:xmpmeta>\n<?xpacket end='r'?>";

const XMP_WEBP_PATH: &str = "tests/images/webp/lossless_images/simple_xmp.webp";
const EXPECTED_WEBP_TIFF_METADATA: &str = "<?xpacket begin='\u{feff}' id='W5M0MpCehiHzreSzNTczkc9d'?>\n<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 13.25'>\n<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>\n\n <rdf:Description rdf:about=''\n xmlns:dc='http://purl.org/dc/elements/1.1/'>\n <dc:subject>\n <rdf:Bag>\n <rdf:li>sunset, mountains, nature</rdf:li>\n </rdf:Bag>\n </dc:subject>\n </rdf:Description>\n</rdf:RDF>\n</x:xmpmeta>\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n<?xpacket end='w'?>";

const XMP_TIFF_PATH: &str = "tests/images/tiff/testsuite/l1_xmp.tiff";

#[test]
#[cfg(feature = "png")]
fn test_read_xmp_png() -> Result<(), image::ImageError> {
let img_path = PathBuf::from_str(XMP_PNG_PATH).unwrap();

let data = fs::read(img_path)?;
let mut png_decoder = PngDecoder::new(std::io::Cursor::new(data))?;
let metadata = png_decoder.xmp_metadata()?;
assert!(metadata.is_some());
assert_eq!(EXPECTED_PNG_METADATA.as_bytes(), metadata.unwrap());

Ok(())
}

#[test]
#[cfg(feature = "webp")]
fn test_read_xmp_webp() -> Result<(), image::ImageError> {
let img_path = PathBuf::from_str(XMP_WEBP_PATH).unwrap();

let data = fs::read(img_path)?;
let mut webp_decoder = WebPDecoder::new(std::io::Cursor::new(data))?;
let metadata = webp_decoder.xmp_metadata()?;

assert!(metadata.is_some());
assert_eq!(EXPECTED_WEBP_TIFF_METADATA.as_bytes(), metadata.unwrap());

Ok(())
}

#[test]
#[cfg(feature = "tiff")]
fn test_read_xmp_tiff() -> Result<(), image::ImageError> {
let img_path = PathBuf::from_str(XMP_TIFF_PATH).unwrap();

let data = fs::read(img_path)?;
let mut tiff_decoder = TiffDecoder::new(std::io::Cursor::new(data))?;
let metadata = tiff_decoder.xmp_metadata()?;
assert!(metadata.is_some());
assert_eq!(EXPECTED_WEBP_TIFF_METADATA.as_bytes(), metadata.unwrap());

Ok(())
}
Loading