Support Exif in PNG.

This commit is contained in:
KAMADA Ken'ichi 2020-08-03 22:57:50 +09:00
parent b99c21236e
commit 6e3c37d591
6 changed files with 159 additions and 1 deletions

3
README
View File

@ -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

View File

@ -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;

130
src/png.rs Normal file
View File

@ -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<R>(reader: &mut R)
-> Result<Vec<u8>, 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<R>(reader: &mut R)
-> Result<Vec<u8>, 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]);
}
}

View File

@ -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");
}
}

View File

@ -48,6 +48,16 @@ pub fn read64<R>(reader: &mut R) -> Result<u64, io::Error> where R: io::Read {
Ok(u64::from_be_bytes(buf))
}
pub fn discard_exact<R>(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<u16, Error> {
if cfg!(debug_assertions) && bytes.len() >= 5 {

BIN
tests/exif.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B