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 00000000..576b88c9 Binary files /dev/null and b/tests/files/interlacing_0_to_1_small_files.png differ diff --git a/tests/files/interlacing_1_to_0_small_files.png b/tests/files/interlacing_1_to_0_small_files.png new file mode 100644 index 00000000..91af6176 Binary files /dev/null and b/tests/files/interlacing_1_to_0_small_files.png differ 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(); +}