Compare commits
1 commit
master
...
79-APNG-Su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b0e8ec0b |
5 changed files with 163 additions and 15 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,4 +3,5 @@ target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.out.png
|
*.out.png
|
||||||
/.idea
|
/.idea
|
||||||
|
/.vscode
|
||||||
/node_modules
|
/node_modules
|
||||||
|
|
|
||||||
|
|
@ -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
81
src/png/apng.rs
Normal 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,
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
16
tests/lib.rs
16
tests/lib.rs
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue