Use our own libpng
This commit is contained in:
parent
7d3a601686
commit
9e1cf7881d
5 changed files with 269 additions and 58 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
||||
}
|
||||
87
src/lib.rs
87
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<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?
|
||||
|
||||
|
|
|
|||
18
src/main.rs
18
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);
|
||||
}
|
||||
|
|
|
|||
213
src/png.rs
Normal file
213
src/png.rs
Normal 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],
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue