From 1fc81ef683d7981620bd665496ee9df63e009bfa Mon Sep 17 00:00:00 2001 From: Joshua Holmer Date: Mon, 4 Apr 2016 16:07:46 -0400 Subject: [PATCH] Support interlaced files and allow conversion between interlaced and progressive files Closes #3 --- src/lib.rs | 1 + src/png.rs | 220 +++++++++++++++++- .../files/interlacing_0_to_1_small_files.png | Bin 0 -> 3586 bytes .../files/interlacing_1_to_0_small_files.png | Bin 0 -> 3600 bytes tests/flags.rs | 82 +++++++ 5 files changed, 295 insertions(+), 8 deletions(-) create mode 100644 tests/files/interlacing_0_to_1_small_files.png create mode 100644 tests/files/interlacing_1_to_0_small_files.png diff --git a/src/lib.rs b/src/lib.rs index c248f702..c95000e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; } } diff --git a/src/png.rs b/src/png.rs index 37607615..33a4b58d 100644 --- a/src/png.rs +++ b/src/png.rs @@ -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 = 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 = 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!(), } } diff --git a/tests/files/interlacing_0_to_1_small_files.png b/tests/files/interlacing_0_to_1_small_files.png new file mode 100644 index 0000000000000000000000000000000000000000..576b88c9af735c88ab6203999c8cab06f0261363 GIT binary patch literal 3586 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 JFEjJJ@Gm!KOL_nR literal 0 HcmV?d00001 diff --git a/tests/flags.rs b/tests/flags.rs index 782e9d0d..3666809d 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -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::>>() == + new_png.pixels().map(|x| x.2.channels().to_owned()).collect::>>()); + + 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::>>() == + new_png.pixels().map(|x| x.2.channels().to_owned()).collect::>>()); + + remove_file(output).ok(); +}