From db0131c47e09754777bd642313a0e37a5a910ca3 Mon Sep 17 00:00:00 2001 From: Josh Holmer Date: Thu, 1 Feb 2018 08:10:51 -0500 Subject: [PATCH] Initial work on APNG support --- .gitignore | 1 + src/headers.rs | 16 ++++++++-- src/png/apng.rs | 75 +++++++++++++++++++++++++++++++++++++++++++ src/png/mod.rs | 85 +++++++++++++++++++++++++++++++++++++------------ tests/lib.rs | 37 +++++++++++---------- 5 files changed, 175 insertions(+), 39 deletions(-) create mode 100644 src/png/apng.rs diff --git a/.gitignore b/.gitignore index c03f402b..69933201 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ target .DS_Store *.out.png /.idea +/.vscode /node_modules diff --git a/src/headers.rs b/src/headers.rs index fff2847d..20af5410 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -36,6 +36,18 @@ pub enum Headers { All, } +#[derive(Debug, Clone)] +pub struct Header { + pub key: String, + pub data: Vec, +} + +impl Header { + pub fn new(key: String, data: Vec) -> Self { + Header { key, data } + } +} + #[inline] pub fn file_header_is_valid(bytes: &[u8]) -> bool { let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; @@ -47,7 +59,7 @@ pub fn parse_next_header( byte_data: &[u8], byte_offset: &mut usize, fix_errors: bool, -) -> Result)>, PngError> { +) -> Result, PngError> { let mut rdr = Cursor::new( byte_data .iter() @@ -106,7 +118,7 @@ pub fn parse_next_header( ))); } - Ok(Some((header, data))) + Ok(Some(Header::new(header, data))) } pub fn parse_ihdr_header(byte_data: &[u8]) -> Result { diff --git a/src/png/apng.rs b/src/png/apng.rs new file mode 100644 index 00000000..6d76a958 --- /dev/null +++ b/src/png/apng.rs @@ -0,0 +1,75 @@ +use std::io::Cursor; +use byteorder::{BigEndian, ReadBytesExt}; + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum DisposalType { + None = 0, + Background = 1, + Previous = 2, +} + +impl From for DisposalType { + fn from(val: u8) -> Self { + match val { + 0 => DisposalType::None, + 1 => DisposalType::Background, + 2 => DisposalType::Previous, + _ => panic!("Unrecognized disposal type"), + } + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum BlendType { + Source = 0, + Over = 1, +} + +impl From for BlendType { + fn from(val: u8) -> Self { + match val { + 0 => BlendType::Source, + 1 => BlendType::Over, + _ => panic!("Unrecognized blend type"), + } + } +} + +#[derive(Debug, Clone)] +pub struct ApngFrame { + pub sequence_number: u32, + pub width: u32, + pub height: u32, + pub x_offset: u32, + pub y_offset: u32, + pub delay_num: u16, + pub delay_den: u16, + pub dispose_op: DisposalType, + pub blend_op: BlendType, + /// The compressed, filtered data from the fdAT chunks + pub frame_data: Vec, + /// The uncompressed, optionally filtered data from the fdAT chunks + pub raw_data: Vec, +} + +impl<'a> From<&'a [u8]> for ApngFrame { + /// Converts a fcTL header to an `ApngFrame`. Will panic if `data` is less than 26 bytes. + fn from(data: &[u8]) -> Self { + let mut cursor = Cursor::new(data); + ApngFrame { + sequence_number: cursor.read_u32::().unwrap(), + width: cursor.read_u32::().unwrap(), + height: cursor.read_u32::().unwrap(), + x_offset: cursor.read_u32::().unwrap(), + y_offset: cursor.read_u32::().unwrap(), + delay_num: cursor.read_u16::().unwrap(), + delay_den: cursor.read_u16::().unwrap(), + dispose_op: cursor.read_u8().unwrap().into(), + blend_op: cursor.read_u8().unwrap().into(), + frame_data: Vec::new(), + raw_data: Vec::new(), + } + } +} diff --git a/src/png/mod.rs b/src/png/mod.rs index 0cf6d7af..714de2ff 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -1,5 +1,5 @@ use bit_vec::BitVec; -use byteorder::{BigEndian, WriteBytesExt}; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use colors::{AlphaOptim, BitDepth, ColorType}; use crc::crc32; use deflate; @@ -12,7 +12,7 @@ use reduction::bit_depth::*; use reduction::color::*; use std::collections::{HashMap, HashSet}; use std::fs::File; -use std::io::{Read, Seek, SeekFrom}; +use std::io::{Cursor, Read, Seek, SeekFrom}; use std::iter::Iterator; use std::path::Path; @@ -21,17 +21,19 @@ const STD_STRATEGY: u8 = 2; // Huffman only const STD_WINDOW: u8 = 15; const STD_FILTERS: [u8; 2] = [0, 5]; +mod apng; mod scan_lines; +use self::apng::ApngFrame; use self::scan_lines::{ScanLine, ScanLines}; #[derive(Debug, Clone)] /// Contains all data relevant to a PNG image pub struct PngData { - /// The filtered and compressed data of the IDAT chunk - pub idat_data: Vec, /// The headers stored in the IHDR chunk pub ihdr_data: IhdrData, + /// The filtered and compressed data of the IDAT chunk + pub idat_data: Vec, /// The uncompressed, optionally filtered data from the IDAT chunk pub raw_data: Vec, /// The palette containing colors used in an Indexed image @@ -43,11 +45,16 @@ pub struct PngData { pub transparency_palette: Option>, /// All non-critical headers from the PNG are stored here pub aux_headers: HashMap>, + /// Header data for an animated PNG: Number of frames and number of plays + pub apng_headers: Option<(u32, u32)>, + /// Frame data for an animated PNG + pub apng_data: Option>, } impl PngData { /// Create a new `PngData` struct by opening a file #[inline] + #[allow(dead_code)] pub fn new(filepath: &Path, fix_errors: bool) -> Result { let byte_data = PngData::read_file(filepath)?; @@ -92,23 +99,42 @@ impl PngData { // Read the data headers let mut aux_headers: HashMap> = HashMap::new(); let mut idat_headers: Vec = Vec::new(); - loop { - let header = parse_next_header(byte_data, &mut byte_offset, fix_errors); - let header = match header { - Ok(x) => x, - Err(x) => return Err(x), - }; - let header = match header { - Some(x) => x, - None => break, + let mut apng_headers: Option<(u32, u32)> = None; + let mut apng_data: Vec = Vec::new(); + while let Some(header) = parse_next_header(byte_data, &mut byte_offset, fix_errors)? { + match header.key.as_str() { + "IDAT" => { + idat_headers.extend(header.data); + } + "acTL" => { + let mut cursor = Cursor::new(&header.data); + apng_headers = Some(( + cursor + .read_u32::() + .map_err(|e| PngError::new(&e.to_string()))?, + cursor + .read_u32::() + .map_err(|e| PngError::new(&e.to_string()))?, + )) + } + "fcTL" => { + if header.data.len() != 26 { + return Err(PngError::new("Invalid length of fcTL header")); + } + apng_data.push(ApngFrame::from(header.data.as_slice())); + } + "fdAT" => match apng_data.last_mut() { + Some(ref mut frame) => { + frame.frame_data.extend_from_slice(&header.data[4..]); + } + None => { + return Err(PngError::new("fdAT with no preceding fcTL header")); + } + }, + _ => { + aux_headers.insert(header.key, header.data); + } }; - if header.0 == "IDAT" { - idat_headers.extend(header.1); - } else if header.0 == "acTL" { - return Err(PngError::new("APNG files are not (yet) supported")); - } else { - aux_headers.insert(header.0, header.1); - } } // Parse the headers into our PngData if idat_headers.is_empty() { @@ -125,6 +151,16 @@ impl PngData { Ok(x) => x, Err(x) => return Err(x), }; + for (i, frame) in apng_data.iter_mut().enumerate() { + if !frame.frame_data.is_empty() { + frame.raw_data = match deflate::inflate(idat_headers.as_ref()) { + Ok(x) => x, + Err(x) => return Err(x), + }; + } else if i > 0 { + return Err(PngError::new("APNG frame contained no data")); + } + } // Handle transparency header let mut has_transparency_pixel = false; let mut has_transparency_palette = false; @@ -151,6 +187,12 @@ impl PngData { None }, aux_headers, + apng_headers, + apng_data: if apng_headers.is_some() { + Some(apng_data) + } else { + None + }, }; png_data.raw_data = png_data.unfilter_image(); // Return the PngData @@ -213,8 +255,11 @@ impl PngData { { write_png_block(key.as_bytes(), header, &mut output); } + // acTL chunk: TODO // IDAT data write_png_block(b"IDAT", &self.idat_data, &mut output); + // fcTL chunks: TODO + // fdAT chunks: TODO // Stream end write_png_block(b"IEND", &[], &mut output); diff --git a/tests/lib.rs b/tests/lib.rs index f2619849..faf48d24 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -12,8 +12,7 @@ fn optimize_from_memory() { in_file.read_to_end(&mut in_file_buf).unwrap(); let mut opts: oxipng::Options = Default::default(); - opts.verbosity = Some(1); - + opts.pretend = true; let result = oxipng::optimize_from_memory(&in_file_buf, &opts); assert!(result.is_ok()); } @@ -25,8 +24,7 @@ fn optimize_from_memory_corrupted() { in_file.read_to_end(&mut in_file_buf).unwrap(); let mut opts: oxipng::Options = Default::default(); - opts.verbosity = Some(1); - + opts.pretend = true; let result = oxipng::optimize_from_memory(&in_file_buf, &opts); assert!(result.is_err()); } @@ -38,35 +36,40 @@ fn optimize_from_memory_apng() { in_file.read_to_end(&mut in_file_buf).unwrap(); let mut opts: oxipng::Options = Default::default(); - opts.verbosity = Some(1); - + opts.pretend = true; let result = oxipng::optimize_from_memory(&in_file_buf, &opts); - assert!(result.is_err()); + assert!(result.is_ok()); } #[test] fn optimize() { + let in_file = Path::new("tests/files/fully_optimized.png"); let mut opts: oxipng::Options = Default::default(); - opts.verbosity = Some(1); - - let result = oxipng::optimize(Path::new("tests/files/fully_optimized.png"), &opts); + opts.force = true; + opts.out_file = Some(in_file.with_extension("out.png")); + let result = oxipng::optimize(in_file, &opts); assert!(result.is_ok()); } #[test] fn optimize_corrupted() { + let in_file = Path::new("tests/files/corrupted_header.png"); let mut opts: oxipng::Options = Default::default(); - opts.verbosity = Some(1); - - let result = oxipng::optimize(Path::new("tests/files/corrupted_header.png"), &opts); + opts.force = true; + opts.out_file = Some(in_file.with_extension("out.png")); + let result = oxipng::optimize(in_file, &opts); assert!(result.is_err()); } #[test] fn optimize_apng() { + let in_file = Path::new("tests/files/apng_file.png"); let mut opts: oxipng::Options = Default::default(); - opts.verbosity = Some(1); - - let result = oxipng::optimize(Path::new("tests/files/apng_file.png"), &opts); - assert!(result.is_err()); + opts.force = true; + opts.out_file = Some(in_file.with_extension("out.png")); + let result = oxipng::optimize(in_file, &opts); + assert!(result.is_ok()); + let new_png = oxipng::png::PngData::new(&opts.out_file.unwrap(), false).unwrap(); + assert!(new_png.apng_headers.is_some()); + assert!(new_png.apng_data.is_some()); }