Skip to content

Commit 9afe256

Browse files
authored
Merge pull request #2567 from 1c3t3a/xmp-metadata
Provide a way to extract XMP metadata (png & webp & tiff only for now)
2 parents a24556b + 2686db5 commit 9afe256

File tree

8 files changed

+123
-0
lines changed

8 files changed

+123
-0
lines changed

src/codecs/png.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use crate::{
2525
// http://www.w3.org/TR/PNG-Structure.html
2626
// The first eight bytes of a PNG file always contain the following (decimal) values:
2727
pub(crate) const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
28+
const XMP_KEY: &str = "XML:com.adobe.xmp";
2829

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

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

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

191+
fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
192+
if let Some(mut itx_chunk) = self
193+
.reader
194+
.info()
195+
.utf8_text
196+
.iter()
197+
.find(|chunk| chunk.keyword.contains(XMP_KEY))
198+
.cloned()
199+
{
200+
itx_chunk.decompress_text().map_err(ImageError::from_png)?;
201+
return itx_chunk
202+
.get_text()
203+
.map(|text| Some(text.as_bytes().to_vec()))
204+
.map_err(ImageError::from_png);
205+
}
206+
Ok(None)
207+
}
208+
189209
fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> {
190210
use byteorder_lite::{BigEndian, ByteOrder, NativeEndian};
191211

src/codecs/tiff.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use crate::error::{
2020
use crate::metadata::Orientation;
2121
use crate::{utils, ImageDecoder, ImageEncoder, ImageFormat};
2222

23+
const TAG_XML_PACKET: Tag = Tag::Unknown(700);
24+
2325
/// Decoder for TIFF images.
2426
pub struct TiffDecoder<R>
2527
where
@@ -264,6 +266,24 @@ impl<R: BufRead + Seek> ImageDecoder for TiffDecoder<R> {
264266
}
265267
}
266268

269+
fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
270+
let Some(decoder) = &mut self.inner else {
271+
return Ok(None);
272+
};
273+
274+
let value = match decoder.get_tag(TAG_XML_PACKET) {
275+
Ok(value) => value,
276+
Err(tiff::TiffError::FormatError(tiff::TiffFormatError::RequiredTagNotFound(_))) => {
277+
return Ok(None);
278+
}
279+
Err(err) => return Err(ImageError::from_tiff_decode(err)),
280+
};
281+
value
282+
.into_u8_vec()
283+
.map(Some)
284+
.map_err(ImageError::from_tiff_decode)
285+
}
286+
267287
fn orientation(&mut self) -> ImageResult<Orientation> {
268288
if let Some(decoder) = &mut self.inner {
269289
Ok(decoder

src/codecs/webp/decoder.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ impl<R: BufRead + Seek> ImageDecoder for WebPDecoder<R> {
8484
Ok(exif)
8585
}
8686

87+
fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
88+
self.inner
89+
.xmp_metadata()
90+
.map_err(ImageError::from_webp_decode)
91+
}
92+
8793
fn orientation(&mut self) -> ImageResult<Orientation> {
8894
// `exif_metadata` caches the orientation, so call it if `orientation` hasn't been set yet.
8995
if self.orientation.is_none() {

src/io/decoder.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ pub trait ImageDecoder {
3131
Ok(None)
3232
}
3333

34+
/// Returns the raw [XMP](https://en.wikipedia.org/wiki/Extensible_Metadata_Platform) chunk, if it is present.
35+
/// A third-party crate such as [`roxmltree`](https://docs.rs/roxmltree/) is required to actually parse it.
36+
///
37+
/// For formats that don't support embedded profiles this function should always return `Ok(None)`.
38+
fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
39+
Ok(None)
40+
}
41+
3442
/// Returns the orientation of the image.
3543
///
3644
/// This is usually obtained from the Exif metadata, if present. Formats that don't support
@@ -134,6 +142,9 @@ impl<T: ?Sized + ImageDecoder> ImageDecoder for Box<T> {
134142
fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
135143
(**self).exif_metadata()
136144
}
145+
fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
146+
(**self).xmp_metadata()
147+
}
137148
fn orientation(&mut self) -> ImageResult<Orientation> {
138149
(**self).orientation()
139150
}
1.91 KB
Loading
4.33 KB
Binary file not shown.
Binary file not shown.

tests/metadata.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
use std::str::FromStr;
4+
5+
use image::ImageDecoder;
6+
7+
#[cfg(feature = "png")]
8+
use image::codecs::png::PngDecoder;
9+
#[cfg(feature = "tiff")]
10+
use image::codecs::tiff::TiffDecoder;
11+
#[cfg(feature = "webp")]
12+
use image::codecs::webp::WebPDecoder;
13+
14+
extern crate glob;
15+
extern crate image;
16+
17+
const XMP_PNG_PATH: &str = "tests/images/png/transparency/tp1n3p08_xmp.png";
18+
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'?>";
19+
20+
const XMP_WEBP_PATH: &str = "tests/images/webp/lossless_images/simple_xmp.webp";
21+
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'?>";
22+
23+
const XMP_TIFF_PATH: &str = "tests/images/tiff/testsuite/l1_xmp.tiff";
24+
25+
#[test]
26+
#[cfg(feature = "png")]
27+
fn test_read_xmp_png() -> Result<(), image::ImageError> {
28+
let img_path = PathBuf::from_str(XMP_PNG_PATH).unwrap();
29+
30+
let data = fs::read(img_path)?;
31+
let mut png_decoder = PngDecoder::new(std::io::Cursor::new(data))?;
32+
let metadata = png_decoder.xmp_metadata()?;
33+
assert!(metadata.is_some());
34+
assert_eq!(EXPECTED_PNG_METADATA.as_bytes(), metadata.unwrap());
35+
36+
Ok(())
37+
}
38+
39+
#[test]
40+
#[cfg(feature = "webp")]
41+
fn test_read_xmp_webp() -> Result<(), image::ImageError> {
42+
let img_path = PathBuf::from_str(XMP_WEBP_PATH).unwrap();
43+
44+
let data = fs::read(img_path)?;
45+
let mut webp_decoder = WebPDecoder::new(std::io::Cursor::new(data))?;
46+
let metadata = webp_decoder.xmp_metadata()?;
47+
48+
assert!(metadata.is_some());
49+
assert_eq!(EXPECTED_WEBP_TIFF_METADATA.as_bytes(), metadata.unwrap());
50+
51+
Ok(())
52+
}
53+
54+
#[test]
55+
#[cfg(feature = "tiff")]
56+
fn test_read_xmp_tiff() -> Result<(), image::ImageError> {
57+
let img_path = PathBuf::from_str(XMP_TIFF_PATH).unwrap();
58+
59+
let data = fs::read(img_path)?;
60+
let mut tiff_decoder = TiffDecoder::new(std::io::Cursor::new(data))?;
61+
let metadata = tiff_decoder.xmp_metadata()?;
62+
assert!(metadata.is_some());
63+
assert_eq!(EXPECTED_WEBP_TIFF_METADATA.as_bytes(), metadata.unwrap());
64+
65+
Ok(())
66+
}

0 commit comments

Comments
 (0)