diff --git a/README b/README index 145f4bc..84fd8df 100644 --- a/README +++ b/README @@ -9,6 +9,7 @@ Exif parsing library written in pure Rust - TIFF and some RAW image formats based on it - JPEG - HEIF and coding-specific variations including HEIC and AVIF + - PNG Usage ----- @@ -42,3 +43,5 @@ Standards - TIFF Revision 6.0 - ISO/IEC 14496-12:2015 - ISO/IEC 23008-12:2017 + - PNG Specification, Version 1.2 + - Extensions to the PNG 1.2 Specification, version 1.5.0 diff --git a/src/lib.rs b/src/lib.rs index 706a802..4c3cf30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ //! //! This library parses Exif attributes in a raw Exif data block. //! It can also read Exif data directly from some image formats -//! including TIFF, JPEG, and HEIF. +//! including TIFF, JPEG, HEIF, and PNG. //! //! # Examples //! @@ -187,6 +187,7 @@ mod endian; mod error; mod isobmff; mod jpeg; +mod png; mod reader; mod tag; mod tiff; diff --git a/src/png.rs b/src/png.rs new file mode 100644 index 0000000..5980060 --- /dev/null +++ b/src/png.rs @@ -0,0 +1,130 @@ +// +// Copyright (c) 2020 KAMADA Ken'ichi. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +// SUCH DAMAGE. +// + +use std::io; +use std::io::Read; + +use crate::endian::{Endian, BigEndian}; +use crate::error::Error; +use crate::util::discard_exact; + +// PNG file signature [PNG12 12.12]. +const PNG_SIG: [u8; 8] = *b"\x89PNG\x0d\x0a\x1a\x0a"; +// The four-byte chunk type for Exif data. +const EXIF_CHUNK_TYPE: [u8; 4] = *b"eXIf"; + +// Get the contents of the eXIf chunk from a PNG file. +pub fn get_exif_attr(reader: &mut R) + -> Result, Error> where R: io::BufRead { + match get_exif_attr_sub(reader) { + Err(Error::Io(ref e)) if e.kind() == io::ErrorKind::UnexpectedEof => + Err(Error::InvalidFormat("Broken PNG file")), + r => r, + } +} + +// The location of the eXIf chunk is restricted [PNGEXT150 3.7], but this +// reader is liberal about it. +fn get_exif_attr_sub(reader: &mut R) + -> Result, Error> where R: io::BufRead { + let mut sig = [0u8; 8]; + reader.read_exact(&mut sig)?; + if sig != PNG_SIG { + return Err(Error::InvalidFormat("Not a PNG file")); + } + // Scan the series of chunks. + loop { + let mut lenbuf = Vec::new(); + match reader.by_ref().take(4).read_to_end(&mut lenbuf)? { + 0 => return Err(Error::NotFound("PNG")), + 1..=3 => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, + "truncated chunk").into()), + _ => {}, + } + let len = BigEndian::loadu32(&lenbuf, 0) as usize; + let mut ctype = [0u8; 4]; + reader.read_exact(&mut ctype)?; + if ctype == EXIF_CHUNK_TYPE { + let mut data = Vec::new(); + reader.by_ref().take(len as u64).read_to_end(&mut data)?; + if data.len() != len { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, + "truncated chunk").into()); + } + return Ok(data); + } + // Chunk data and CRC. + discard_exact(reader, len + 4)?; + } +} + +pub fn is_png(buf: &[u8]) -> bool { + buf.starts_with(&PNG_SIG) +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + use super::*; + + #[test] + fn truncated() { + let sets: &[&[u8]] = &[ + b"", + b"\x89", + b"\x89PNG\x0d\x0a\x1a", + ]; + for &data in sets { + assert_err_pat!(get_exif_attr(&mut Cursor::new(data)), + Error::InvalidFormat("Broken PNG file")); + } + + let mut data = b"\x89PNG\x0d\x0a\x1a\x0a\0\0\0\x04eXIfExif".to_vec(); + get_exif_attr(&mut Cursor::new(&data)).unwrap(); + while let Some(_) = data.pop() { + get_exif_attr(&mut &data[..]).unwrap_err(); + } + } + + #[test] + fn no_exif() { + let data = b"\x89PNG\x0d\x0a\x1a\x0a"; + assert_err_pat!(get_exif_attr(&mut Cursor::new(data)), + Error::NotFound(_)); + } + + #[test] + fn empty() { + let data = b"\x89PNG\x0d\x0a\x1a\x0a\0\0\0\0eXIfCRC_"; + assert_ok!(get_exif_attr(&mut Cursor::new(data)), []); + } + + #[test] + fn non_empty() { + let data = b"\x89PNG\x0d\x0a\x1a\x0a\0\0\0\x02eXIf\xbe\xadCRC_"; + assert_ok!(get_exif_attr(&mut Cursor::new(data)), [0xbe, 0xad]); + } +} diff --git a/src/reader.rs b/src/reader.rs index 6e0b86d..6c21915 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -31,6 +31,7 @@ use std::io::Read; use crate::error::Error; use crate::isobmff; use crate::jpeg; +use crate::png; use crate::tag::Tag; use crate::tiff; use crate::tiff::{Field, IfdEntry, In, ProvideUnit}; @@ -89,6 +90,7 @@ impl Reader { /// - TIFF and some RAW image formats based on it /// - JPEG /// - HEIF and coding-specific variations including HEIC and AVIF + /// - PNG /// /// This method is provided for the convenience even though /// parsing containers is basically out of the scope of this library. @@ -100,6 +102,8 @@ impl Reader { reader.read_to_end(&mut buf)?; } else if jpeg::is_jpeg(&buf) { buf = jpeg::get_exif_attr(&mut buf.chain(reader))?; + } else if png::is_png(&buf) { + buf = png::get_exif_attr(&mut buf.chain(reader))?; } else if isobmff::is_heif(&buf) { reader.seek(io::SeekFrom::Start(0))?; buf = isobmff::get_exif_attr(reader)?; @@ -241,4 +245,14 @@ mod tests { let exifver = exif.get_field(Tag::ExifVersion, In::PRIMARY).unwrap(); assert_eq!(exifver.display_value().to_string(), "2.31"); } + + #[test] + fn png() { + let file = std::fs::File::open("tests/exif.png").unwrap(); + let exif = Reader::new().read_from_container( + &mut std::io::BufReader::new(&file)).unwrap(); + assert_eq!(exif.fields().len(), 6); + let exifver = exif.get_field(Tag::ExifVersion, In::PRIMARY).unwrap(); + assert_eq!(exifver.display_value().to_string(), "2.32"); + } } diff --git a/src/util.rs b/src/util.rs index 57d5ede..0b042d8 100644 --- a/src/util.rs +++ b/src/util.rs @@ -48,6 +48,16 @@ pub fn read64(reader: &mut R) -> Result where R: io::Read { Ok(u64::from_be_bytes(buf)) } +pub fn discard_exact(reader: &mut R, mut len: usize) + -> Result<(), io::Error> where R: io::BufRead { + while len > 0 { + let consume_len = reader.fill_buf()?.len().min(len); + reader.consume(consume_len); + len -= consume_len; + } + Ok(()) +} + // This function must not be called with more than 4 bytes. pub fn atou16(bytes: &[u8]) -> Result { if cfg!(debug_assertions) && bytes.len() >= 5 { diff --git a/tests/exif.png b/tests/exif.png new file mode 100644 index 0000000..fea60b1 Binary files /dev/null and b/tests/exif.png differ