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>"]
|
authors = ["Joshua Holmer <jholmer.in@gmail.com>"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
flate2 = "0.2"
|
byteorder = "0.4."
|
||||||
png = "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::path::Path;
|
||||||
use std::fs::File;
|
|
||||||
use std::io;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
mod compress;
|
mod png;
|
||||||
|
|
||||||
pub struct Options<'a> {
|
pub struct Options<'a> {
|
||||||
pub backup: bool,
|
pub backup: bool,
|
||||||
|
|
@ -16,9 +15,9 @@ pub struct Options<'a> {
|
||||||
pub create: bool,
|
pub create: bool,
|
||||||
pub preserve_attrs: bool,
|
pub preserve_attrs: bool,
|
||||||
pub verbosity: Option<u8>,
|
pub verbosity: Option<u8>,
|
||||||
pub f: HashSet<u8>,
|
pub filter: HashSet<u8>,
|
||||||
pub i: u8,
|
pub interlaced: u8,
|
||||||
pub zc: HashSet<u8>,
|
pub compression: HashSet<u8>,
|
||||||
pub zm: HashSet<u8>,
|
pub zm: HashSet<u8>,
|
||||||
pub zs: HashSet<u8>,
|
pub zs: HashSet<u8>,
|
||||||
pub zw: u32,
|
pub zw: u32,
|
||||||
|
|
@ -38,50 +37,52 @@ pub struct Options<'a> {
|
||||||
// Output:
|
// Output:
|
||||||
// IDAT size = 45711 bytes (no change)
|
// IDAT size = 45711 bytes (no change)
|
||||||
// file size = 45821 bytes (60 bytes = 0.13% decrease)
|
// 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
|
// Decode PNG from file
|
||||||
println!("Processing: {}", filepath.to_str().unwrap());
|
println!("Processing: {}", filepath.to_str().unwrap());
|
||||||
let in_file = try!(File::open(filepath));
|
let in_file = Path::new(filepath);
|
||||||
let decoder = png::Decoder::new(in_file);
|
let png = match png::PngData::new(&in_file) {
|
||||||
let (info, mut reader) = try!(decoder.read_info());
|
Ok(x) => x,
|
||||||
let mut img_buf = vec![0; info.buffer_size()];
|
Err(x) => return Err(x)
|
||||||
try!(reader.next_frame(&mut img_buf));
|
};
|
||||||
|
|
||||||
// Read and print
|
// Print png info
|
||||||
let info = reader.info();
|
let idat_current_size = png.idat_data.len();
|
||||||
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();
|
|
||||||
let file_current_size = filepath.metadata().unwrap().len();
|
let file_current_size = filepath.metadata().unwrap().len();
|
||||||
if opts.verbosity.is_some() {
|
if opts.verbosity.is_some() {
|
||||||
println!("{}x{} pixels, PNG format", width, height);
|
println!(" {}x{} pixels, PNG format", png.ihdr_data.width, png.ihdr_data.height);
|
||||||
if let Some(palette) = info.palette.clone() {
|
if let Some(palette) = png.palette.clone() {
|
||||||
println!("{} bits/pixel, {} colors in palette", depth.1, palette.len() / 3);
|
println!(" {} bits/pixel, {} colors in palette", png.ihdr_data.bit_depth, palette.len() / 3);
|
||||||
} else {
|
} else {
|
||||||
println!("{}x{} bits/pixel, {:?}", depth.0, depth.1, info.color_type);
|
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
|
|
||||||
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!(" 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?
|
// TODO: Backup before writing?
|
||||||
|
|
||||||
|
|
|
||||||
18
src/main.rs
18
src/main.rs
|
|
@ -8,11 +8,11 @@ fn main() {
|
||||||
// TODO: Handle wildcards
|
// TODO: Handle wildcards
|
||||||
let filename = env::args().skip(1).next().unwrap();
|
let filename = env::args().skip(1).next().unwrap();
|
||||||
let infile = Path::new(&filename);
|
let infile = Path::new(&filename);
|
||||||
let mut f = HashSet::new();
|
let mut filter = HashSet::new();
|
||||||
f.insert(0);
|
filter.insert(0);
|
||||||
f.insert(5);
|
filter.insert(5);
|
||||||
let mut zc = HashSet::new();
|
let mut compression = HashSet::new();
|
||||||
zc.insert(9);
|
compression.insert(9);
|
||||||
let mut zm = HashSet::new();
|
let mut zm = HashSet::new();
|
||||||
zm.insert(9);
|
zm.insert(9);
|
||||||
let mut zs = HashSet::new();
|
let mut zs = HashSet::new();
|
||||||
|
|
@ -29,9 +29,9 @@ fn main() {
|
||||||
create: true,
|
create: true,
|
||||||
preserve_attrs: false,
|
preserve_attrs: false,
|
||||||
verbosity: Some(0),
|
verbosity: Some(0),
|
||||||
f: f,
|
filter: filter,
|
||||||
i: 0,
|
interlaced: 0,
|
||||||
zc: zc,
|
compression: compression,
|
||||||
zm: zm,
|
zm: zm,
|
||||||
zs: zs,
|
zs: zs,
|
||||||
zw: 4096,
|
zw: 4096,
|
||||||
|
|
@ -41,5 +41,7 @@ fn main() {
|
||||||
idat_recoding: true,
|
idat_recoding: true,
|
||||||
idat_paranoia: false,
|
idat_paranoia: false,
|
||||||
};
|
};
|
||||||
|
// TODO: Handle command line args
|
||||||
|
// TODO: Handle optimization presets
|
||||||
optipng::optimize(infile, default_opts);
|
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