From f57a972e2d76d7d039f93c6b6f814ec62b4858c9 Mon Sep 17 00:00:00 2001 From: KAMADA Ken'ichi Date: Mon, 14 Dec 2020 21:12:19 +0900 Subject: [PATCH] Support Exif in WebP. --- README | 2 + src/lib.rs | 3 +- src/reader.rs | 16 ++++++ src/webp.rs | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ tests/exif.webp | Bin 0 -> 400 bytes tests/rwrcmp.rs | 5 ++ 6 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/webp.rs create mode 100644 tests/exif.webp diff --git a/README b/README index 84fd8df..dfbb637 100644 --- a/README +++ b/README @@ -10,6 +10,7 @@ Exif parsing library written in pure Rust - JPEG - HEIF and coding-specific variations including HEIC and AVIF - PNG + - WebP Usage ----- @@ -45,3 +46,4 @@ Standards - ISO/IEC 23008-12:2017 - PNG Specification, Version 1.2 - Extensions to the PNG 1.2 Specification, version 1.5.0 + - WebP Container Specification, committed on 2018-04-20 diff --git a/src/lib.rs b/src/lib.rs index 4c3cf30..ab1031b 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, HEIF, and PNG. +//! including TIFF, JPEG, HEIF, PNG, and WebP. //! //! # Examples //! @@ -193,4 +193,5 @@ mod tag; mod tiff; mod util; mod value; +mod webp; mod writer; diff --git a/src/reader.rs b/src/reader.rs index 6c21915..0a9bd1f 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -35,6 +35,7 @@ use crate::png; use crate::tag::Tag; use crate::tiff; use crate::tiff::{Field, IfdEntry, In, ProvideUnit}; +use crate::webp; /// A struct to parse the Exif attributes and /// create an `Exif` instance that holds the results. @@ -91,6 +92,7 @@ impl Reader { /// - JPEG /// - HEIF and coding-specific variations including HEIC and AVIF /// - PNG + /// - WebP /// /// This method is provided for the convenience even though /// parsing containers is basically out of the scope of this library. @@ -107,6 +109,8 @@ impl Reader { } else if isobmff::is_heif(&buf) { reader.seek(io::SeekFrom::Start(0))?; buf = isobmff::get_exif_attr(reader)?; + } else if webp::is_webp(&buf) { + buf = webp::get_exif_attr(&mut buf.chain(reader))?; } else { return Err(Error::InvalidFormat("Unknown image format")); } @@ -255,4 +259,16 @@ mod tests { let exifver = exif.get_field(Tag::ExifVersion, In::PRIMARY).unwrap(); assert_eq!(exifver.display_value().to_string(), "2.32"); } + + #[test] + fn webp() { + let file = std::fs::File::open("tests/exif.webp").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"); + let desc = exif.get_field(Tag::ImageDescription, In::PRIMARY).unwrap(); + assert_eq!(desc.display_value().to_string(), "\"WebP test\""); + } } diff --git a/src/webp.rs b/src/webp.rs new file mode 100644 index 0000000..bc140c7 --- /dev/null +++ b/src/webp.rs @@ -0,0 +1,140 @@ +// +// 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, LittleEndian}; +use crate::error::Error; +use crate::util::BufReadExt; + +// Chunk identifiers for RIFF. +const FCC_RIFF: [u8; 4] = *b"RIFF"; +const FCC_WEBP: [u8; 4] = *b"WEBP"; +const FCC_EXIF: [u8; 4] = *b"EXIF"; + +// Get the contents of the Exif chunk from a WebP 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 WebP file")), + r => r, + } +} + +fn get_exif_attr_sub(reader: &mut R) + -> Result, Error> where R: io::BufRead { + let mut sig = [0; 12]; + reader.read_exact(&mut sig)?; + if sig[0..4] != FCC_RIFF || sig[8..12] != FCC_WEBP { + return Err(Error::InvalidFormat("Not a WebP file")); + } + let mut file_size = LittleEndian::loadu32(&sig, 4); + file_size = file_size.checked_sub(4) + .ok_or(Error::InvalidFormat("Invalid header file size"))?; + + // Scan the series of chunks. + while file_size > 0 { + file_size = file_size.checked_sub(8) + .ok_or(Error::InvalidFormat("Chunk overflowing parent"))?; + let mut cheader = [0; 8]; + reader.read_exact(&mut cheader)?; + let mut size = LittleEndian::loadu32(&cheader, 4); + file_size = file_size.checked_sub(size) + .ok_or(Error::InvalidFormat("Chunk overflowing parent"))?; + if cheader[0..4] == FCC_EXIF { + let mut payload = Vec::new(); + reader.by_ref().take(size.into()).read_to_end(&mut payload)?; + if payload.len() != size as usize { + return Err(io::Error::new(io::ErrorKind::UnexpectedEof, + "truncated chunk").into()); + } + return Ok(payload); + } + if size % 2 != 0 && file_size > 0 { + file_size -= 1; + size = size.checked_add(1) + .ok_or(Error::InvalidFormat("Chunk size too big"))?; + } + reader.discard_exact(size as usize)?; + } + return Err(Error::NotFound("WebP")); +} + +pub fn is_webp(buf: &[u8]) -> bool { + buf.len() >= 12 && buf[0..4] == FCC_RIFF && buf[8..12] == FCC_WEBP +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn truncated() { + let mut data = b"RIFF\x10\0\0\0WEBPEXIF\x04\0\0\0Exif".to_vec(); + assert_eq!(get_exif_attr(&mut &data[..]).unwrap(), b"Exif"); + while let Some(_) = data.pop() { + get_exif_attr(&mut &data[..]).unwrap_err(); + } + } + + #[test] + fn no_exif() { + let data = b"RIFF\x0c\0\0\0WEBPwhat\0\0\0\0"; + assert_err_pat!(get_exif_attr(&mut &data[..]), Error::NotFound(_)); + } + + #[test] + fn empty() { + let data = b"RIFF\x16\0\0\0WEBPodd_\x01\0\0\0X\0EXIF\0\0\0\0"; + assert_ok!(get_exif_attr(&mut &data[..]), b""); + } + + #[test] + fn non_empty() { + let data = b"RIFF\x1a\0\0\0WEBPeven\x02\0\0\0XYEXIF\x03\0\0\0abcd"; + assert_ok!(get_exif_attr(&mut &data[..]), b"abc"); + } + + #[test] + fn read_first() { + let data = b"RIFF\x18\0\0\0WEBPEXIF\x02\0\0\0abEXIF\x02\0\0\0cd"; + assert_ok!(get_exif_attr(&mut &data[..]), b"ab"); + } + + #[test] + fn out_of_toplevel_chunk() { + let data = b"RIFF\x0e\0\0\0WEBPwhat\x02\0\0\0abEXIF\x02\0\0\0cd"; + assert_err_pat!(get_exif_attr(&mut &data[..]), Error::NotFound(_)); + } + + #[test] + fn odd_at_last_without_padding() { + let data = b"RIFF\x17\0\0\0WEBPwhat\0\0\0\0EXIF\x03\0\0\0abc"; + assert_ok!(get_exif_attr(&mut &data[..]), b"abc"); + } +} diff --git a/tests/exif.webp b/tests/exif.webp new file mode 100644 index 0000000000000000000000000000000000000000..6481e7250dcff68c158512549e316a86cba698c6 GIT binary patch literal 400 zcmWIYbaU%qWMBw)bqWXzu!!JdU|`??Vm<~2HUtFUG5lmWt+1T&5A(%2ei zuik&oKO6sN+fnP|x7JJE>|HN4|Nj!34CKo-cu8H`d4tUwwV4h%(%(r|VaqXq*rSRAO5(GaX&7RYaE&tzbMst*IwKuMry zfq}!oVqj!!1QG~OO$tybNi8k`aXf$+WHy*~_w)@=&{oLIEyzi&05Y-@a}!e%vvpG| IGSe6s0N#j$O8@`> literal 0 HcmV?d00001 diff --git a/tests/rwrcmp.rs b/tests/rwrcmp.rs index e2334cc..7a5254e 100644 --- a/tests/rwrcmp.rs +++ b/tests/rwrcmp.rs @@ -60,6 +60,11 @@ fn exif_tif() { rwr_compare("tests/exif.tif"); } +#[test] +fn exif_webp() { + rwr_compare("tests/exif.webp"); +} + fn main() { for path in env::args_os().skip(1) { rwr_compare(&path);