From 9e1cf7881d594a7bf1c3890e456cee632106ad1a Mon Sep 17 00:00:00 2001 From: Joshua Holmer Date: Fri, 18 Dec 2015 22:19:05 -0500 Subject: [PATCH] Use our own libpng --- Cargo.toml | 4 +- src/compress.rs | 5 -- src/lib.rs | 87 ++++++++++---------- src/main.rs | 18 ++-- src/png.rs | 213 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 58 deletions(-) delete mode 100644 src/compress.rs create mode 100644 src/png.rs diff --git a/Cargo.toml b/Cargo.toml index 5a71d187..e98effd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,5 @@ version = "2.0.0" authors = ["Joshua Holmer "] [dependencies] -flate2 = "0.2" -png = "0.4" +byteorder = "0.4." +crc = "^1.0.0" diff --git a/src/compress.rs b/src/compress.rs deleted file mode 100644 index eb2321b4..00000000 --- a/src/compress.rs +++ /dev/null @@ -1,5 +0,0 @@ -extern crate png; - -pub fn compress(idat: Vec, f: u8, i: u8, zc: u8, zm: u8, zs: u8, zw: u32) -> Vec { - -} diff --git a/src/lib.rs b/src/lib.rs index 8733eb63..a2e8eaed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,10 @@ -extern crate png; +extern crate byteorder; +extern crate crc; use std::path::Path; -use std::fs::File; -use std::io; use std::collections::HashSet; -mod compress; +mod png; pub struct Options<'a> { pub backup: bool, @@ -16,9 +15,9 @@ pub struct Options<'a> { pub create: bool, pub preserve_attrs: bool, pub verbosity: Option, - pub f: HashSet, - pub i: u8, - pub zc: HashSet, + pub filter: HashSet, + pub interlaced: u8, + pub compression: HashSet, pub zm: HashSet, pub zs: HashSet, pub zw: u32, @@ -38,50 +37,52 @@ pub struct Options<'a> { // Output: // IDAT size = 45711 bytes (no change) // file size = 45821 bytes (60 bytes = 0.13% decrease) -pub fn optimize(filepath: &Path, opts: Options) -> Result<(), io::Error> { +pub fn optimize(filepath: &Path, opts: Options) -> Result<(), String> { // Decode PNG from file println!("Processing: {}", filepath.to_str().unwrap()); - let in_file = try!(File::open(filepath)); - let decoder = png::Decoder::new(in_file); - let (info, mut reader) = try!(decoder.read_info()); - let mut img_buf = vec![0; info.buffer_size()]; - try!(reader.next_frame(&mut img_buf)); + let in_file = Path::new(filepath); + let png = match png::PngData::new(&in_file) { + Ok(x) => x, + Err(x) => return Err(x) + }; - // Read and print - let info = reader.info(); - let (width, height) = info.size(); - let depth = (info.bytes_per_pixel(), info.bits_per_pixel() / info.bytes_per_pixel()); - // FIXME - let idat_current_size = img_buf.len(); + // Print png info + let idat_current_size = png.idat_data.len(); let file_current_size = filepath.metadata().unwrap().len(); if opts.verbosity.is_some() { - println!("{}x{} pixels, PNG format", width, height); - if let Some(palette) = info.palette.clone() { - println!("{} bits/pixel, {} colors in palette", depth.1, palette.len() / 3); + println!(" {}x{} pixels, PNG format", png.ihdr_data.width, png.ihdr_data.height); + if let Some(palette) = png.palette.clone() { + println!(" {} bits/pixel, {} colors in palette", png.ihdr_data.bit_depth, palette.len() / 3); } else { - println!("{}x{} bits/pixel, {:?}", depth.0, depth.1, info.color_type); - } - println!("IDAT size = {} bytes", idat_current_size); - println!("File size = {} bytes", file_current_size); - } - - // TODO: Bit depth/palette reduction - - // Go through selected permutations and determine the best - if opts.idat_recoding { - let combinations = opts.f.len() * opts.zc.len() * opts.zm.len() * opts.zs.len(); - println!("Trying: {} combinations", combinations); - // TODO: Multithreading - for f in &opts.f { - for zc in &opts.zc { - for zm in &opts.zm { - for zs in &opts.zs { - // TODO: Test compressions - } - } - } + println!(" {}x{} bits/pixel, {:?}", png.bits_per_pixel(), png.ihdr_data.bit_depth, png.ihdr_data.color_type); } + println!(" IDAT size = {} bytes", idat_current_size); + println!(" File size = {} bytes", file_current_size); } + // + // // TODO: Bit depth/palette reduction + // + // // Go through selected permutations and determine the best + // let mut best: (Option<(u8, u8, u8, u8)>, usize) = (None, idat_current_size.clone()); + // if opts.idat_recoding { + // let combinations = opts.f.len() * opts.zc.len() * opts.zm.len() * opts.zs.len(); + // println!("Trying: {} combinations", combinations); + // // TODO: Multithreading + // for f in &opts.f { + // for zc in &opts.zc { + // for zm in &opts.zm { + // for zs in &opts.zs { + // // TODO: Test compressions + // let new_idat = compress::compress(img_buf.clone(), *f, opts.i, *zc, *zm, *zs, opts.zw); + // // TODO: Force reencoding if interlacing was changed + // if new_idat.len() < best.1 { + // best = (Some((*f, *zc, *zm, *zs)), new_idat.len()); + // } + // } + // } + // } + // } + // } // TODO: Backup before writing? diff --git a/src/main.rs b/src/main.rs index 57db6a2c..f90d0848 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,11 @@ fn main() { // TODO: Handle wildcards let filename = env::args().skip(1).next().unwrap(); let infile = Path::new(&filename); - let mut f = HashSet::new(); - f.insert(0); - f.insert(5); - let mut zc = HashSet::new(); - zc.insert(9); + let mut filter = HashSet::new(); + filter.insert(0); + filter.insert(5); + let mut compression = HashSet::new(); + compression.insert(9); let mut zm = HashSet::new(); zm.insert(9); let mut zs = HashSet::new(); @@ -29,9 +29,9 @@ fn main() { create: true, preserve_attrs: false, verbosity: Some(0), - f: f, - i: 0, - zc: zc, + filter: filter, + interlaced: 0, + compression: compression, zm: zm, zs: zs, zw: 4096, @@ -41,5 +41,7 @@ fn main() { idat_recoding: true, idat_paranoia: false, }; + // TODO: Handle command line args + // TODO: Handle optimization presets optipng::optimize(infile, default_opts); } diff --git a/src/png.rs b/src/png.rs new file mode 100644 index 00000000..b69b1b91 --- /dev/null +++ b/src/png.rs @@ -0,0 +1,213 @@ +use std::io::Cursor; +use byteorder::{BigEndian, ReadBytesExt}; +use crc::crc32; +use std::collections::HashMap; +use std::fmt; +use std::fs::File; +use std::io::prelude::*; +use std::path::Path; + +#[derive(Debug)] +pub enum ColorType { + Grayscale, + RGB, + Indexed, + GrayscaleAlpha, + RGBA, +} + +impl fmt::Display for ColorType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", match *self { + ColorType::Grayscale => "Grayscale", + ColorType::RGB => "RGB", + ColorType::Indexed => "Indexed", + ColorType::GrayscaleAlpha => "Grayscale + Alpha", + ColorType::RGBA => "RGB + Alpha", + }) + } +} + +#[derive(Debug)] +pub enum BitDepth { + One, + Two, + Four, + Eight, + Sixteen, +} + +impl fmt::Display for BitDepth { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", match *self { + BitDepth::One => "1", + BitDepth::Two => "2", + BitDepth::Four => "4", + BitDepth::Eight => "8", + BitDepth::Sixteen => "16", + }) + } +} + +#[derive(Debug)] +pub struct PngData { + pub idat_data: Vec, + pub ihdr_data: IhdrData, + pub palette: Option>, +} + +#[derive(Debug)] +pub struct IhdrData { + pub width: u32, + pub height: u32, + pub color_type: ColorType, + pub bit_depth: BitDepth, + pub compression: u8, + pub filter: u8, + pub interlaced: u8, +} + +impl PngData { + pub fn new(filepath: &Path) -> Result { + let mut file = match File::open(filepath) { + Ok(f) => f, + Err(_) => return Err("Failed to open file for reading".to_owned()) + }; + let mut byte_data: Vec = Vec::new(); + // Read raw png data into memory + match file.read_to_end(&mut byte_data) { + Ok(_) => (), + Err(_) => return Err("Failed to read from file".to_owned()) + } + let mut byte_offset: usize = 0; + // Test that png header is valid + let header: Vec = byte_data.iter().take(8).cloned().collect(); + if !file_header_is_valid(header.as_ref()) { + return Err("Invalid PNG header detected".to_owned()); + } + byte_offset += 8; + // 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.as_ref(), &mut byte_offset); + let header = match header { + Ok(x) => x, + Err(x) => return Err(x) + }; + let header = match header { + Some(x) => x, + None => break + }; + if header.0 == "IDAT" { + idat_headers.extend(header.1); + } else { + aux_headers.insert(header.0, header.1); + } + } + // Parse the headers into our PngData + if idat_headers.len() == 0 { + return Err("Image data was empty, skipping".to_owned()); + } + if aux_headers.get("IHDR").is_none() { + return Err("Image header data was missing, skipping".to_owned()); + } + let ihdr_header = parse_ihdr_header(aux_headers.get("IHDR").unwrap().as_ref()); + // Return the PngData + Ok(PngData { + idat_data: idat_headers, + ihdr_data: match ihdr_header { + Ok(x) => x, + Err(x) => return Err(x) + }, + palette: aux_headers.get("PLTE").cloned(), + }) + } + pub fn buffer_size(&self) -> usize { + // Return the size needed to hold a decoded frame + self.ihdr_data.width * self.ihdr_data.height * match self.ihdr_data.bit_depth { + BitDepth::One => 1, + BitDepth::Two => 2, + BitDepth::Four => 4, + BitDepth::Eight => 8, + BitDepth::Sixteen => 16 + } as usize + } + pub fn bits_per_pixel(&self) -> u8 { + match self.ihdr_data.color_type { + ColorType::Grayscale => 1, + ColorType::RGB => 3, + ColorType::Indexed => 1, + ColorType::GrayscaleAlpha => 2, + ColorType::RGBA => 4, + } + } +} + +fn file_header_is_valid(bytes: &[u8]) -> bool { + let expected_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + + bytes.iter().zip(expected_header.iter()).all(|x| x.0 == x.1) +} + +fn parse_next_header(byte_data: &[u8], byte_offset: &mut usize) -> Result)>, String> { + let mut rdr = Cursor::new(byte_data.iter().skip(*byte_offset).take(4).cloned().collect::>()); + let length: u32 = match rdr.read_u32::() { + Ok(x) => x, + Err(_) => return Err("Invalid data found--unable to read PNG file".to_owned()) + }; + *byte_offset += 4; + + let mut header_bytes: Vec = byte_data.iter().skip(*byte_offset).take(4).cloned().collect(); + let header = match String::from_utf8(header_bytes.clone()) { + Ok(x) => x, + Err(_) => return Err("Invalid data found--unable to read PNG file".to_owned()) + }; + if header == "IEND".to_owned() { + // End of data + return Ok(None); + } + *byte_offset += 4; + + let data: Vec = byte_data.iter().skip(*byte_offset).take(length as usize).cloned().collect(); + *byte_offset += length as usize; + let mut rdr = Cursor::new(byte_data.iter().skip(*byte_offset).take(4).cloned().collect::>()); + let crc: u32 = match rdr.read_u32::() { + Ok(x) => x, + Err(_) => return Err("Invalid data found--unable to read PNG file".to_owned()) + }; + *byte_offset += 4; + header_bytes.extend(data.clone()); + if crc32::checksum_ieee(header_bytes.as_ref()) != crc { + return Err(format!("Corrupt data chunk found--CRC Mismatch in {}", header)); + } + + Ok(Some((header, data))) +} + +fn parse_ihdr_header(byte_data: &[u8]) -> Result { + let mut rdr = Cursor::new(&byte_data[0..8]); + Ok(IhdrData { + color_type: match byte_data[9] { + 0 => ColorType::Grayscale, + 2 => ColorType::RGB, + 3 => ColorType::Indexed, + 4 => ColorType::GrayscaleAlpha, + 6 => ColorType::RGBA, + _ => return Err("Unexpected color type in header".to_owned()) + }, + bit_depth: match byte_data[8] { + 1 => BitDepth::One, + 2 => BitDepth::Two, + 4 => BitDepth::Four, + 8 => BitDepth::Eight, + 16 => BitDepth::Sixteen, + _ => return Err("Unexpected bit depth in header".to_owned()) + }, + width: rdr.read_u32::().unwrap(), + height: rdr.read_u32::().unwrap(), + compression: byte_data[10], + filter: byte_data[11], + interlaced: byte_data[12], + }) +}