Support interlaced files and allow conversion between interlaced and progressive files
Closes #3
This commit is contained in:
parent
2974bbbee3
commit
1fc81ef683
5 changed files with 295 additions and 8 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
220
src/png.rs
220
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<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!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
tests/files/interlacing_0_to_1_small_files.png
Normal file
BIN
tests/files/interlacing_0_to_1_small_files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
tests/files/interlacing_1_to_0_small_files.png
Normal file
BIN
tests/files/interlacing_1_to_0_small_files.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue