Support interlaced files and allow conversion between interlaced and progressive files

Closes #3
This commit is contained in:
Joshua Holmer 2016-04-04 16:07:46 -04:00
parent 2974bbbee3
commit 1fc81ef683
5 changed files with 295 additions and 8 deletions

View file

@ -186,6 +186,7 @@ pub fn optimize(filepath: &Path, opts: &Options) -> Result<(), String> {
if let Some(interlacing) = opts.interlace {
if png.change_interlacing(interlacing) {
png.ihdr_data.interlaced = interlacing;
something_changed = true;
}
}

View file

@ -156,11 +156,7 @@ impl<'a> Iterator for ScanLines<'a> {
let mut bits_per_line = self.png.ihdr_data.width as usize * bits_per_pixel;
let y_steps;
match self.pass {
Some((1, _)) => {
bits_per_line = (bits_per_line as f32 / 8f32).ceil() as usize;
y_steps = 8;
}
Some((2, _)) => {
Some((1, _)) | Some((2, _)) => {
bits_per_line = (bits_per_line as f32 / 8f32).ceil() as usize;
y_steps = 8;
}
@ -634,17 +630,225 @@ impl PngData {
}
/// Convert the image to the specified interlacing type
/// Returns true if the interlacing was changed, false otherwise
/// The `interlace` parameter specifies the *new* interlacing mode
/// Assumes that the data has already been de-filtered
pub fn change_interlacing(&mut self, interlace: u8) -> bool {
if interlace == self.ihdr_data.interlaced {
return false;
}
if interlace == 1 {
// TODO: Interlace
// Convert progressive to interlaced data
interlace_image(self);
} else {
// TODO: Deinterlace
// Convert interlaced to progressive data
deinterlace_image(self);
}
false
true
}
}
fn interlace_image(png: &mut PngData) {
let mut passes: Vec<BitVec> = Vec::with_capacity(7);
for _ in 0..7 {
passes.push(BitVec::new());
}
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
for (index, line) in png.scan_lines().enumerate() {
match index % 8 {
// Add filter bytes to appropriate lines
0 => {
passes[0].extend(BitVec::from_elem(8, false));
passes[3].extend(BitVec::from_elem(8, false));
passes[5].extend(BitVec::from_elem(8, false));
if png.ihdr_data.width > 4 {
passes[1].extend(BitVec::from_elem(8, false));
}
}
4 => {
passes[3].extend(BitVec::from_elem(8, false));
passes[5].extend(BitVec::from_elem(8, false));
passes[2].extend(BitVec::from_elem(8, false));
}
2 | 6 => {
passes[4].extend(BitVec::from_elem(8, false));
passes[5].extend(BitVec::from_elem(8, false));
}
_ => {
passes[6].extend(BitVec::from_elem(8, false));
}
}
let bit_vec = BitVec::from_bytes(&line.data);
for (i, bit) in bit_vec.iter().enumerate() {
// Avoid moving padded 0's into new image
if i >= (png.ihdr_data.width * bits_per_pixel as u32) as usize {
break;
}
// Copy pixels into interlaced passes
let pix_modulo = (((i / bits_per_pixel as usize) as f32).floor() as usize) % 8;
match index % 8 {
0 => {
match pix_modulo {
0 => passes[0].push(bit),
4 => passes[1].push(bit),
2 | 6 => passes[3].push(bit),
_ => passes[5].push(bit),
}
}
4 => {
match pix_modulo {
0 | 4 => passes[2].push(bit),
2 | 6 => passes[3].push(bit),
_ => passes[5].push(bit),
}
}
2 | 6 => {
match pix_modulo % 2 {
0 => passes[4].push(bit),
_ => passes[5].push(bit),
}
}
_ => {
passes[6].push(bit);
}
}
}
// Pad end of line on each pass to get 8 bits per byte
for pass in &mut passes {
while pass.len() % 8 != 0 {
pass.push(false);
}
}
}
let mut output = Vec::new();
for pass in &passes {
output.extend(pass.to_bytes());
}
png.raw_data = output;
}
fn deinterlace_image(png: &mut PngData) {
let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel();
let mut lines: Vec<BitVec> = Vec::with_capacity(png.ihdr_data.height as usize);
for _ in 0..png.ihdr_data.height {
// Initialize each output line with a starting filter byte of 0
// as well as some blank data
lines.push(BitVec::from_elem(8 + bits_per_pixel as usize * png.ihdr_data.width as usize,
false));
}
let mut current_pass = 1;
let mut pass_constants = interlaced_constants(current_pass);
let mut current_y: usize = pass_constants.y_shift as usize;
for line in png.scan_lines() {
let bit_vec = BitVec::from_bytes(&line.data);
let bits_in_line = ((png.ihdr_data.width - pass_constants.x_shift as u32) as f32 /
pass_constants.x_step as f32)
.ceil() as usize *
bits_per_pixel as usize;
for (i, bit) in bit_vec.iter().enumerate() {
// Avoid moving padded 0's into new image
if i >= bits_in_line {
break;
}
let current_x: usize = pass_constants.x_shift as usize +
(i / bits_per_pixel as usize) * pass_constants.x_step as usize;
// Copy this bit into the output line, offset by 8 because of filter byte
let index = 8 + (i % bits_per_pixel as usize) + current_x * bits_per_pixel as usize;
lines[current_y].set(index, bit);
}
// Calculate the next line and move to next pass if necessary
current_y += pass_constants.y_step as usize;
if current_y >= png.ihdr_data.height as usize {
if current_pass == 7 {
break;
}
current_pass += 1;
if current_pass == 2 && png.ihdr_data.width <= 4 {
current_pass += 1;
}
if current_pass == 3 && png.ihdr_data.height <= 4 {
current_pass += 1;
}
pass_constants = interlaced_constants(current_pass);
current_y = pass_constants.y_shift as usize;
}
}
let mut output = Vec::new();
for line in &mut lines {
while line.len() % 8 != 0 {
line.push(false);
}
output.extend(line.to_bytes());
}
png.raw_data = output;
}
struct InterlacedConstants {
x_shift: u8,
y_shift: u8,
x_step: u8,
y_step: u8,
}
fn interlaced_constants(pass: u8) -> InterlacedConstants {
match pass {
1 => {
InterlacedConstants {
x_shift: 0,
y_shift: 0,
x_step: 8,
y_step: 8,
}
}
2 => {
InterlacedConstants {
x_shift: 4,
y_shift: 0,
x_step: 8,
y_step: 8,
}
}
3 => {
InterlacedConstants {
x_shift: 0,
y_shift: 4,
x_step: 4,
y_step: 8,
}
}
4 => {
InterlacedConstants {
x_shift: 2,
y_shift: 0,
x_step: 4,
y_step: 4,
}
}
5 => {
InterlacedConstants {
x_shift: 0,
y_shift: 2,
x_step: 2,
y_step: 4,
}
}
6 => {
InterlacedConstants {
x_shift: 1,
y_shift: 0,
x_step: 2,
y_step: 2,
}
}
7 => {
InterlacedConstants {
x_shift: 0,
y_shift: 1,
x_step: 1,
y_step: 2,
}
}
_ => unreachable!(),
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -341,3 +341,85 @@ fn interlacing_1_to_0() {
remove_file(output).ok();
}
#[test]
fn interlacing_0_to_1_small_files() {
let input = PathBuf::from("tests/files/interlacing_0_to_1_small_files.png");
let mut opts = get_opts(&input);
opts.interlace = Some(1);
let output = opts.out_file.clone();
let png = png::PngData::new(&input).unwrap();
assert!(png.ihdr_data.interlaced == 0);
assert!(png.ihdr_data.color_type == png::ColorType::Indexed);
assert!(png.ihdr_data.bit_depth == png::BitDepth::Eight);
match oxipng::optimize(&input, &opts) {
Ok(_) => (),
Err(x) => panic!(x.to_owned()),
};
assert!(output.exists());
let png = match png::PngData::new(&output) {
Ok(x) => x,
Err(x) => {
remove_file(output).ok();
panic!(x.to_owned())
}
};
assert!(png.ihdr_data.interlaced == 1);
assert!(png.ihdr_data.color_type == png::ColorType::Indexed);
assert!(png.ihdr_data.bit_depth == png::BitDepth::One);
let old_png = image::open(&input).unwrap();
let new_png = image::open(&output).unwrap();
// Conversion should be lossless
assert!(old_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>() ==
new_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>());
remove_file(output).ok();
}
#[test]
fn interlacing_1_to_0_small_files() {
let input = PathBuf::from("tests/files/interlacing_1_to_0_small_files.png");
let mut opts = get_opts(&input);
opts.interlace = Some(0);
let output = opts.out_file.clone();
let png = png::PngData::new(&input).unwrap();
assert!(png.ihdr_data.interlaced == 1);
assert!(png.ihdr_data.color_type == png::ColorType::Indexed);
assert!(png.ihdr_data.bit_depth == png::BitDepth::Eight);
match oxipng::optimize(&input, &opts) {
Ok(_) => (),
Err(x) => panic!(x.to_owned()),
};
assert!(output.exists());
let png = match png::PngData::new(&output) {
Ok(x) => x,
Err(x) => {
remove_file(output).ok();
panic!(x.to_owned())
}
};
assert!(png.ihdr_data.interlaced == 0);
assert!(png.ihdr_data.color_type == png::ColorType::Indexed);
assert!(png.ihdr_data.bit_depth == png::BitDepth::One);
let old_png = image::open(&input).unwrap();
let new_png = image::open(&output).unwrap();
// Conversion should be lossless
assert!(old_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>() ==
new_png.pixels().map(|x| x.2.channels().to_owned()).collect::<Vec<Vec<u8>>>());
remove_file(output).ok();
}