Use our own libpng

This commit is contained in:
Joshua Holmer 2015-12-18 22:19:05 -05:00
parent 7d3a601686
commit 9e1cf7881d
5 changed files with 269 additions and 58 deletions

View file

@ -4,5 +4,5 @@ version = "2.0.0"
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
[dependencies]
flate2 = "0.2"
png = "0.4"
byteorder = "0.4."
crc = "^1.0.0"

View file

@ -1,5 +0,0 @@
extern crate png;
pub fn compress(idat: Vec<u8>, f: u8, i: u8, zc: u8, zm: u8, zs: u8, zw: u32) -> Vec<u8> {
}

View file

@ -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<u8>,
pub f: HashSet<u8>,
pub i: u8,
pub zc: HashSet<u8>,
pub filter: HashSet<u8>,
pub interlaced: u8,
pub compression: HashSet<u8>,
pub zm: HashSet<u8>,
pub zs: HashSet<u8>,
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?

View file

@ -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);
}

213
src/png.rs Normal file
View file

@ -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<u8>,
pub ihdr_data: IhdrData,
pub palette: Option<Vec<u8>>,
}
#[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<PngData, String> {
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<u8> = 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<u8> = 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<String, Vec<u8>> = HashMap::new();
let mut idat_headers: Vec<u8> = 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<Option<(String, Vec<u8>)>, String> {
let mut rdr = Cursor::new(byte_data.iter().skip(*byte_offset).take(4).cloned().collect::<Vec<u8>>());
let length: u32 = match rdr.read_u32::<BigEndian>() {
Ok(x) => x,
Err(_) => return Err("Invalid data found--unable to read PNG file".to_owned())
};
*byte_offset += 4;
let mut header_bytes: Vec<u8> = 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<u8> = 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::<Vec<u8>>());
let crc: u32 = match rdr.read_u32::<BigEndian>() {
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<IhdrData, String> {
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::<BigEndian>().unwrap(),
height: rdr.read_u32::<BigEndian>().unwrap(),
compression: byte_data[10],
filter: byte_data[11],
interlaced: byte_data[12],
})
}