Filtering, mostly

This commit is contained in:
Joshua Holmer 2016-01-10 23:24:02 -05:00
parent e24fa4ae27
commit 4765c2133a
2 changed files with 233 additions and 15 deletions

View file

@ -65,7 +65,7 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
palette.len() / 3);
} else {
println!(" {}x{} bits/pixel, {:?}",
png.bits_per_pixel(),
png.bits_per_pixel_raw(),
png.ihdr_data.bit_depth,
png.ihdr_data.color_type);
}
@ -86,11 +86,11 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
// TODO: Force reencoding if interlacing was changed
// TODO: Multithreading
for f in &opts.filter {
// TODO: Apply filter
let filtered = png.filter_image(*f);
for zc in &opts.compression {
for zm in &opts.memory {
for zs in &opts.strategies {
let new_idat = match deflate::deflate::deflate(png.raw_data.as_ref(),
let new_idat = match deflate::deflate::deflate(&filtered,
*zc,
*zm,
*zs,
@ -98,7 +98,6 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
Ok(x) => x,
Err(x) => return Err(x),
};
// TODO: Apply filtering
if new_idat.len() < png.idat_data.len() {
best = Some((*f, *zc, *zm, *zs));
png.idat_data = new_idat.clone();
@ -124,7 +123,6 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
better.3,
better.0,
png.idat_data.len());
png.ihdr_data.filter = better.0;
}
}

View file

@ -5,6 +5,7 @@ use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::prelude::*;
use std::iter::Iterator;
use std::path::Path;
#[derive(Debug)]
@ -77,6 +78,40 @@ impl BitDepth {
}
}
struct ScanLines<'a> {
png: &'a PngData,
start: usize,
end: usize,
len: usize,
}
impl<'a> Iterator for ScanLines<'a> {
type Item = ScanLine;
fn next(&mut self) -> Option<Self::Item> {
if self.end == self.len {
None
} else {
let bits_per_line = self.png.ihdr_data.width as usize *
self.png.ihdr_data.bit_depth.as_u8() as usize *
self.png.bits_per_pixel_raw() as usize;
// This avoids casting to and from floats, which is expensive
let bytes_per_line = ((bits_per_line + bits_per_line % 8) >> 3) + 1;
self.start = self.end;
self.end = self.start + bytes_per_line;
Some(ScanLine {
filter: self.png.raw_data[self.start].clone(),
data: self.png.raw_data[self.start + 1..self.end].to_owned(),
})
}
}
}
#[derive(Debug)]
struct ScanLine {
filter: u8,
data: Vec<u8>,
}
#[derive(Debug)]
pub struct PngData {
pub idat_data: Vec<u8>,
@ -142,25 +177,26 @@ impl PngData {
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());
let ihdr_header = match parse_ihdr_header(aux_headers.get("IHDR").unwrap().as_ref()) {
Ok(x) => x,
Err(x) => return Err(x),
};
let raw_data = match super::deflate::deflate::inflate(idat_headers.as_ref()) {
Ok(x) => x,
Err(x) => return Err(x),
};
// TODO: Reverse filtering?
// Return the PngData
Ok(PngData {
let mut png_data = PngData {
idat_data: idat_headers.clone(),
ihdr_data: match ihdr_header {
Ok(x) => x,
Err(x) => return Err(x),
},
ihdr_data: ihdr_header,
raw_data: raw_data,
palette: aux_headers.remove("PLTE"),
aux_headers: aux_headers,
})
};
png_data.raw_data = png_data.unfilter_image();
// Return the PngData
Ok(png_data)
}
pub fn bits_per_pixel(&self) -> u8 {
pub fn bits_per_pixel_raw(&self) -> u8 {
match self.ihdr_data.color_type {
ColorType::Grayscale => 1,
ColorType::RGB => 3,
@ -231,6 +267,190 @@ impl PngData {
output
}
fn scan_lines(&self) -> ScanLines {
ScanLines {
png: &self,
start: 0,
end: 0,
len: self.raw_data.len(),
}
}
pub fn unfilter_image(&self) -> Vec<u8> {
let mut unfiltered = Vec::with_capacity(self.raw_data.len());
// This avoids casting to and from floats, which is expensive
let tmp = self.ihdr_data.bit_depth.as_u8() * self.bits_per_pixel_raw();
let bpp = (tmp + tmp % 8) >> 3;
let mut last_line: Option<Vec<u8>> = None;
for line in self.scan_lines() {
unfiltered.push(line.filter);
match line.filter {
0 => {
let mut data = line.data.clone();
last_line = Some(data.clone());
unfiltered.append(&mut data);
}
1 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
match i.checked_sub(bpp as usize) {
Some(x) => data.push(byte.wrapping_add(line.data[x])),
None => data.push(*byte),
}
}
last_line = Some(data.clone());
unfiltered.append(&mut data);
}
2 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
if let Some(last) = last_line.clone() {
data.push(byte.wrapping_add(last[i]));
} else {
data.push(*byte);
};
}
last_line = Some(data.clone());
unfiltered.append(&mut data);
}
3 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
if let Some(last) = last_line.clone() {
data.push(match i.checked_sub(bpp as usize) {
Some(x) => byte.wrapping_add((line.data[x] + last[i]) >> 1),
None => byte.wrapping_add(last[i] >> 1),
});
} else {
data.push(match i.checked_sub(bpp as usize) {
Some(x) => byte.wrapping_add(line.data[x] >> 1),
None => *byte,
});
};
}
last_line = Some(data.clone());
unfiltered.append(&mut data);
}
4 => {
let mut data = Vec::with_capacity(line.data.len());
for (i, byte) in line.data.iter().enumerate() {
if let Some(last) = last_line.clone() {
data.push(match i.checked_sub(bpp as usize) {
Some(x) => {
byte.wrapping_add(paeth_predictor(line.data[x],
last[i],
last[x]))
}
None => byte.wrapping_add(last[i]),
});
} else {
data.push(match i.checked_sub(bpp as usize) {
Some(x) => byte.wrapping_add(line.data[x]),
None => *byte,
});
};
}
last_line = Some(data.clone());
unfiltered.append(&mut data);
}
_ => panic!("Unreachable"),
}
}
unfiltered
}
pub fn filter_image(&self, filter: u8) -> Vec<u8> {
let mut filtered = Vec::with_capacity(self.raw_data.len());
// This avoids casting to and from floats, which is expensive
let tmp = self.ihdr_data.bit_depth.as_u8() * self.bits_per_pixel_raw();
let bpp = (tmp + tmp % 8) >> 3;
let mut last_line: Option<Vec<u8>> = None;
// We could try a different filter method for each line
// But that would be prohibitively slow and probably not provide much benefit
// So we just use one filter method for the whole image
for line in self.scan_lines() {
let internal_filter = match filter {
5 => {
// TODO: Heuristically guess best filter per line
0
}
x => x,
};
filtered.push(internal_filter);
match internal_filter {
0 => {
let mut data = line.data.clone();
filtered.append(&mut data);
},
1 => {
for (i, byte) in line.data.iter().enumerate() {
filtered.push(match i.checked_sub(bpp as usize) {
Some(x) => byte.wrapping_sub(line.data[x]),
None => *byte,
});
}
}
2 => {
for (i, byte) in line.data.iter().enumerate() {
if let Some(last) = last_line.clone() {
filtered.push(byte.wrapping_sub(last[i]));
} else {
filtered.push(*byte);
};
}
}
3 => {
for (i, byte) in line.data.iter().enumerate() {
if let Some(last) = last_line.clone() {
filtered.push(match i.checked_sub(bpp as usize) {
Some(x) => byte.wrapping_sub((line.data[x] + last[i]) >> 1),
None => byte.wrapping_sub(last[i] >> 1),
});
} else {
filtered.push(match i.checked_sub(bpp as usize) {
Some(x) => byte.wrapping_sub(line.data[x] >> 1),
None => *byte,
});
};
}
}
4 => {
for (i, byte) in line.data.iter().enumerate() {
if let Some(last) = last_line.clone() {
filtered.push(match i.checked_sub(bpp as usize) {
Some(x) => {
byte.wrapping_sub(paeth_predictor(line.data[x],
last[i],
last[x]))
}
None => byte.wrapping_sub(last[i]),
});
} else {
filtered.push(match i.checked_sub(bpp as usize) {
Some(x) => byte.wrapping_sub(line.data[x]),
None => *byte,
});
};
}
}
_ => panic!("Unreachable"),
}
last_line = Some(line.data.clone());
}
filtered
}
}
fn paeth_predictor(a: u8, b: u8, c: u8) -> u8 {
let p = a as i32 + b as i32 - c as i32;
let pa = (p - a as i32).abs();
let pb = (p - b as i32).abs();
let pc = (p - c as i32).abs();
if pa <= pb && pa <= pc {
a
} else if pb <= pc {
b
} else {
c
}
}
fn file_header_is_valid(bytes: &[u8]) -> bool {