Compare commits

...

1 commit

Author SHA1 Message Date
Josh Holmer
85b0e8ec0b Initial work on APNG support 2018-10-12 13:44:17 -04:00
5 changed files with 163 additions and 15 deletions

1
.gitignore vendored
View file

@ -3,4 +3,5 @@ target
.DS_Store .DS_Store
*.out.png *.out.png
/.idea /.idea
/.vscode
/node_modules /node_modules

View file

@ -40,6 +40,18 @@ pub enum Headers {
All, All,
} }
#[derive(Debug, Clone)]
pub struct Header {
pub key: String,
pub data: Vec<u8>,
}
impl Header {
pub fn new(key: String, data: Vec<u8>) -> Self {
Header { key, data }
}
}
#[inline] #[inline]
pub fn file_header_is_valid(bytes: &[u8]) -> bool { pub fn file_header_is_valid(bytes: &[u8]) -> bool {
let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];

81
src/png/apng.rs Normal file
View file

@ -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<u8> 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<u8> 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<u8>,
/// The uncompressed, optionally filtered data from the fdAT chunks
pub raw_data: Vec<u8>,
}
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::<BigEndian>().unwrap(),
width: cursor.read_u32::<BigEndian>().unwrap(),
height: cursor.read_u32::<BigEndian>().unwrap(),
x_offset: cursor.read_u32::<BigEndian>().unwrap(),
y_offset: cursor.read_u32::<BigEndian>().unwrap(),
delay_num: cursor.read_u16::<BigEndian>().unwrap(),
delay_den: cursor.read_u16::<BigEndian>().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,
}

View file

@ -1,6 +1,6 @@
use atomicmin::AtomicMin; use atomicmin::AtomicMin;
use bit_vec::BitVec; use bit_vec::BitVec;
use byteorder::{BigEndian, WriteBytesExt}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use colors::{AlphaOptim, BitDepth, ColorType}; use colors::{AlphaOptim, BitDepth, ColorType};
use crc::crc32; use crc::crc32;
use deflate; use deflate;
@ -15,7 +15,7 @@ use reduction::bit_depth::*;
use reduction::color::*; use reduction::color::*;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs::File; use std::fs::File;
use std::io::{Read, Seek, SeekFrom}; use std::io::{Cursor, Read, Seek, SeekFrom};
use std::iter::Iterator; use std::iter::Iterator;
use std::path::Path; use std::path::Path;
@ -24,17 +24,19 @@ const STD_STRATEGY: u8 = 2; // Huffman only
const STD_WINDOW: u8 = 15; const STD_WINDOW: u8 = 15;
const STD_FILTERS: [u8; 2] = [0, 5]; const STD_FILTERS: [u8; 2] = [0, 5];
mod apng;
mod scan_lines; mod scan_lines;
use self::apng::{ApngFrame, ApngHeaders};
use self::scan_lines::{ScanLine, ScanLines}; use self::scan_lines::{ScanLine, ScanLines};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
/// Contains all data relevant to a PNG image /// Contains all data relevant to a PNG image
pub struct PngData { pub struct PngData {
/// The filtered and compressed data of the IDAT chunk
pub idat_data: Vec<u8>,
/// The headers stored in the IHDR chunk /// The headers stored in the IHDR chunk
pub ihdr_data: IhdrData, pub ihdr_data: IhdrData,
/// The filtered and compressed data of the IDAT chunk
pub idat_data: Vec<u8>,
/// The uncompressed, optionally filtered data from the IDAT chunk /// The uncompressed, optionally filtered data from the IDAT chunk
pub raw_data: Vec<u8>, pub raw_data: Vec<u8>,
/// The palette containing colors used in an Indexed image /// The palette containing colors used in an Indexed image
@ -46,11 +48,16 @@ pub struct PngData {
pub transparency_palette: Option<Vec<u8>>, pub transparency_palette: Option<Vec<u8>>,
/// All non-critical headers from the PNG are stored here /// All non-critical headers from the PNG are stored here
pub aux_headers: HashMap<[u8; 4], Vec<u8>>, pub aux_headers: HashMap<[u8; 4], Vec<u8>>,
/// Header data for an animated PNG: Number of frames and number of plays
pub apng_headers: Option<ApngHeaders>,
/// Frame data for an animated PNG
pub apng_data: Option<Vec<ApngFrame>>,
} }
impl PngData { impl PngData {
/// Create a new `PngData` struct by opening a file /// Create a new `PngData` struct by opening a file
#[inline] #[inline]
#[allow(dead_code)]
pub fn new(filepath: &Path, fix_errors: bool) -> Result<PngData, PngError> { pub fn new(filepath: &Path, fix_errors: bool) -> Result<PngData, PngError> {
let byte_data = PngData::read_file(filepath)?; let byte_data = PngData::read_file(filepath)?;
@ -95,14 +102,42 @@ impl PngData {
// Read the data headers // Read the data headers
let mut aux_headers: HashMap<[u8; 4], Vec<u8>> = HashMap::new(); let mut aux_headers: HashMap<[u8; 4], Vec<u8>> = HashMap::new();
let mut idat_headers: Vec<u8> = Vec::new(); let mut idat_headers: Vec<u8> = Vec::new();
let mut apng_headers = None;
let mut apng_data: Vec<ApngFrame> = Vec::new();
while let Some(header) = parse_next_header(byte_data, &mut byte_offset, fix_errors)? { while let Some(header) = parse_next_header(byte_data, &mut byte_offset, fix_errors)? {
match &header.name { match &header.name {
b"IDAT" => idat_headers.extend(header.data), b"IDAT" => {
b"acTL" => return Err(PngError::APNGNotSupported), idat_headers.extend(header.data);
}
b"acTL" => {
let mut cursor = Cursor::new(&header.data);
apng_headers = Some(ApngHeaders {
frames: cursor
.read_u32::<BigEndian>()
.map_err(|e| PngError::new(&e.to_string()))?,
plays: cursor
.read_u32::<BigEndian>()
.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()); aux_headers.insert(header.name, header.data.to_owned());
} }
} };
} }
// Parse the headers into our PngData // Parse the headers into our PngData
if idat_headers.is_empty() { if idat_headers.is_empty() {
@ -114,6 +149,16 @@ impl PngData {
}; };
let ihdr_header = parse_ihdr_header(&ihdr)?; let ihdr_header = parse_ihdr_header(&ihdr)?;
let raw_data = deflate::inflate(idat_headers.as_ref())?; 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 // Handle transparency header
let mut has_transparency_pixel = false; let mut has_transparency_pixel = false;
let mut has_transparency_palette = false; let mut has_transparency_palette = false;
@ -140,6 +185,12 @@ impl PngData {
None None
}, },
aux_headers, aux_headers,
apng_headers,
apng_data: if apng_headers.is_some() {
Some(apng_data)
} else {
None
},
}; };
png_data.raw_data = png_data.unfilter_image(); png_data.raw_data = png_data.unfilter_image();
// Return the PngData // Return the PngData
@ -203,8 +254,11 @@ impl PngData {
{ {
write_png_block(key, header, &mut output); write_png_block(key, header, &mut output);
} }
// acTL chunk: TODO
// IDAT data // IDAT data
write_png_block(b"IDAT", &self.idat_data, &mut output); write_png_block(b"IDAT", &self.idat_data, &mut output);
// fcTL chunks: TODO
// fdAT chunks: TODO
// Stream end // Stream end
write_png_block(b"IEND", &[], &mut output); write_png_block(b"IEND", &[], &mut output);

View file

@ -12,8 +12,7 @@ fn optimize_from_memory() {
in_file.read_to_end(&mut in_file_buf).unwrap(); in_file.read_to_end(&mut in_file_buf).unwrap();
let mut opts: oxipng::Options = Default::default(); let mut opts: oxipng::Options = Default::default();
opts.verbosity = Some(1); opts.pretend = true;
let result = oxipng::optimize_from_memory(&in_file_buf, &opts); let result = oxipng::optimize_from_memory(&in_file_buf, &opts);
assert!(result.is_ok()); assert!(result.is_ok());
} }
@ -25,8 +24,7 @@ fn optimize_from_memory_corrupted() {
in_file.read_to_end(&mut in_file_buf).unwrap(); in_file.read_to_end(&mut in_file_buf).unwrap();
let mut opts: oxipng::Options = Default::default(); let mut opts: oxipng::Options = Default::default();
opts.verbosity = Some(1); opts.pretend = true;
let result = oxipng::optimize_from_memory(&in_file_buf, &opts); let result = oxipng::optimize_from_memory(&in_file_buf, &opts);
assert!(result.is_err()); assert!(result.is_err());
} }
@ -38,10 +36,9 @@ fn optimize_from_memory_apng() {
in_file.read_to_end(&mut in_file_buf).unwrap(); in_file.read_to_end(&mut in_file_buf).unwrap();
let mut opts: oxipng::Options = Default::default(); let mut opts: oxipng::Options = Default::default();
opts.verbosity = Some(1); opts.pretend = true;
let result = oxipng::optimize_from_memory(&in_file_buf, &opts); let result = oxipng::optimize_from_memory(&in_file_buf, &opts);
assert!(result.is_err()); assert!(result.is_ok());
} }
#[test] #[test]
@ -80,5 +77,8 @@ fn optimize_apng() {
&OutFile::Path(None), &OutFile::Path(None),
&opts, &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());
} }