Skip to content

Commit

Permalink
Initial work on APNG support
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh Holmer authored and shssoichiro committed Apr 9, 2018
1 parent 8a2bdcd commit db0131c
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ target
.DS_Store
*.out.png
/.idea
/.vscode
/node_modules
16 changes: 14 additions & 2 deletions src/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ pub enum Headers {
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]
pub fn file_header_is_valid(bytes: &[u8]) -> bool {
let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
Expand All @@ -47,7 +59,7 @@ pub fn parse_next_header(
byte_data: &[u8],
byte_offset: &mut usize,
fix_errors: bool,
) -> Result<Option<(String, Vec<u8>)>, PngError> {
) -> Result<Option<Header>, PngError> {
let mut rdr = Cursor::new(
byte_data
.iter()
Expand Down Expand Up @@ -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<IhdrData, PngError> {
Expand Down
75 changes: 75 additions & 0 deletions src/png/apng.rs
Original file line number Diff line number Diff line change
@@ -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<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(),
}
}
}
85 changes: 65 additions & 20 deletions src/png/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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<u8>,
/// The headers stored in the IHDR chunk
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
pub raw_data: Vec<u8>,
/// The palette containing colors used in an Indexed image
Expand All @@ -43,11 +45,16 @@ pub struct PngData {
pub transparency_palette: Option<Vec<u8>>,
/// All non-critical headers from the PNG are stored here
pub aux_headers: HashMap<String, Vec<u8>>,
/// 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<Vec<ApngFrame>>,
}

impl PngData {
/// Create a new `PngData` struct by opening a file
#[inline]
#[allow(dead_code)]
pub fn new(filepath: &Path, fix_errors: bool) -> Result<PngData, PngError> {
let byte_data = PngData::read_file(filepath)?;

Expand Down Expand Up @@ -92,23 +99,42 @@ impl PngData {
// Read the data headers
let mut aux_headers: HashMap<String, Vec<u8>> = HashMap::new();
let mut idat_headers: Vec<u8> = 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<ApngFrame> = 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::<BigEndian>()
.map_err(|e| PngError::new(&e.to_string()))?,
cursor
.read_u32::<BigEndian>()
.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() {
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);

Expand Down
37 changes: 20 additions & 17 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand All @@ -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());
}
Expand All @@ -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());
}

3 comments on commit db0131c

@TPS
Copy link

@TPS TPS commented on db0131c Apr 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Started #79.…

@javiergutierrezchamorro

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any available builds with this change?

@shssoichiro
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still incomplete at this point. It can read in an APNG but will write out a non animated PNG.

Please sign in to comment.