From 85b0e8ec0b0cb2e7c227422534e5b8de080b755c 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 | 12 ++++++++ src/png/apng.rs | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ src/png/mod.rs | 68 ++++++++++++++++++++++++++++++++++++----- tests/lib.rs | 16 +++++----- 5 files changed, 163 insertions(+), 15 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 4c0c340b..ca8717df 100644 --- a/src/headers.rs +++ b/src/headers.rs @@ -40,6 +40,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]; diff --git a/src/png/apng.rs b/src/png/apng.rs new file mode 100644 index 00000000..f7e365ec --- /dev/null +++ b/src/png/apng.rs @@ -0,0 +1,81 @@ +use byteorder::{BigEndian, ReadBytesExt}; +use std::io::Cursor; + +#[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(), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ApngHeaders { + pub frames: u32, + pub plays: u32, +} diff --git a/src/png/mod.rs b/src/png/mod.rs index 56c682e1..b442ec2d 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -1,6 +1,6 @@ use atomicmin::AtomicMin; use bit_vec::BitVec; -use byteorder::{BigEndian, WriteBytesExt}; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use colors::{AlphaOptim, BitDepth, ColorType}; use crc::crc32; use deflate; @@ -15,7 +15,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; @@ -24,17 +24,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, ApngHeaders}; 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 @@ -46,11 +48,16 @@ pub struct PngData { pub transparency_palette: Option>, /// All non-critical headers from the PNG are stored here pub aux_headers: HashMap<[u8; 4], Vec>, + /// Header data for an animated PNG: Number of frames and number of plays + pub apng_headers: Option, + /// 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)?; @@ -95,14 +102,42 @@ impl PngData { // Read the data headers let mut aux_headers: HashMap<[u8; 4], Vec> = HashMap::new(); let mut idat_headers: Vec = Vec::new(); + let mut apng_headers = None; + let mut apng_data: Vec = Vec::new(); while let Some(header) = parse_next_header(byte_data, &mut byte_offset, fix_errors)? { match &header.name { - b"IDAT" => idat_headers.extend(header.data), - b"acTL" => return Err(PngError::APNGNotSupported), + b"IDAT" => { + idat_headers.extend(header.data); + } + b"acTL" => { + let mut cursor = Cursor::new(&header.data); + apng_headers = Some(ApngHeaders { + frames: cursor + .read_u32::() + .map_err(|e| PngError::new(&e.to_string()))?, + plays: cursor + .read_u32::() + .map_err(|e| PngError::new(&e.to_string()))?, + }) + } + b"fcTL" => { + if header.data.len() != 26 { + return Err(PngError::new("Invalid length of fcTL header")); + } + apng_data.push(ApngFrame::from(header.data)); + } + b"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.name, header.data.to_owned()); } - } + }; } // Parse the headers into our PngData if idat_headers.is_empty() { @@ -114,6 +149,16 @@ impl PngData { }; let ihdr_header = parse_ihdr_header(&ihdr)?; let raw_data = deflate::inflate(idat_headers.as_ref())?; + 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; @@ -140,6 +185,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 @@ -203,8 +254,11 @@ impl PngData { { write_png_block(key, 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 448fb41d..92562c48 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,10 +36,9 @@ 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] @@ -80,5 +77,8 @@ fn optimize_apng() { &OutFile::Path(None), &opts, ); - assert!(result.is_err()); + assert!(result.is_ok()); + let new_png = oxipng::PngData::new(&opts.out_file.unwrap(), false).unwrap(); + assert!(new_png.apng_headers.is_some()); + assert!(new_png.apng_data.is_some()); }