diff --git a/benches/interlacing.rs b/benches/interlacing.rs index 45a36cf0..26d93da7 100644 --- a/benches/interlacing.rs +++ b/benches/interlacing.rs @@ -14,7 +14,7 @@ fn interlacing_16_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(1); + safe_png.change_interlacing(1) }); } @@ -25,7 +25,7 @@ fn interlacing_8_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(1); + safe_png.change_interlacing(1) }); } @@ -38,7 +38,7 @@ fn interlacing_4_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(1); + safe_png.change_interlacing(1) }); } @@ -51,7 +51,7 @@ fn interlacing_2_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(1); + safe_png.change_interlacing(1) }); } @@ -64,7 +64,7 @@ fn interlacing_1_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(1); + safe_png.change_interlacing(1) }); } @@ -77,7 +77,7 @@ fn deinterlacing_16_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(0); + safe_png.change_interlacing(0) }); } @@ -90,7 +90,7 @@ fn deinterlacing_8_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(0); + safe_png.change_interlacing(0) }); } @@ -103,7 +103,7 @@ fn deinterlacing_4_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(0); + safe_png.change_interlacing(0) }); } @@ -116,7 +116,7 @@ fn deinterlacing_2_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(0); + safe_png.change_interlacing(0) }); } @@ -129,6 +129,6 @@ fn deinterlacing_1_bits(b: &mut Bencher) { b.iter(|| { let mut safe_png = png.clone(); - safe_png.change_interlacing(0); + safe_png.change_interlacing(0) }); } diff --git a/src/interlace.rs b/src/interlace.rs index 6d720e90..c681dc4e 100644 --- a/src/interlace.rs +++ b/src/interlace.rs @@ -1,7 +1,9 @@ +use reduction::ReducedPng; use bit_vec::BitVec; use png::PngData; -pub fn interlace_image(png: &mut PngData) { +#[must_use] +pub fn interlace_image(png: &PngData) -> ReducedPng { let mut passes: Vec = vec![BitVec::new(); 7]; let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel(); for (index, line) in png.scan_lines().enumerate() { @@ -74,14 +76,24 @@ pub fn interlace_image(png: &mut PngData) { } } } - let mut output = Vec::new(); + + let mut output = Vec::with_capacity(png.raw_data.len()); for pass in &passes { output.extend(pass.to_bytes()); } - png.raw_data = output; + + ReducedPng { + raw_data: output, + interlaced: 1, + color_type: png.ihdr_data.color_type, + bit_depth: png.ihdr_data.bit_depth, + aux_headers: Default::default(), + palette: None, + transparency_pixel: None, + } } -pub fn deinterlace_image(png: &mut PngData) { +pub fn deinterlace_image(png: &PngData) -> ReducedPng { let bits_per_pixel = png.ihdr_data.bit_depth.as_u8() * png.channels_per_pixel(); let bits_per_line = 8 + bits_per_pixel as usize * png.ihdr_data.width as usize; // Initialize each output line with a starting filter byte of 0 @@ -126,14 +138,22 @@ pub fn deinterlace_image(png: &mut PngData) { current_y = pass_constants.y_shift as usize; } } - let mut output = Vec::new(); + let mut output = Vec::with_capacity(png.raw_data.len()); for line in &mut lines { while line.len() % 8 != 0 { line.push(false); } output.extend(line.to_bytes()); } - png.raw_data = output; + ReducedPng { + raw_data: output, + interlaced: 0, + color_type: png.ihdr_data.color_type, + bit_depth: png.ihdr_data.bit_depth, + aux_headers: Default::default(), + palette: None, + transparency_pixel: None, + } } #[derive(Clone, Copy)] diff --git a/src/lib.rs b/src/lib.rs index b6119de8..3f1e8ebd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ extern crate rayon; extern crate rgb; extern crate zopfli; +use reduction::*; use atomicmin::AtomicMin; use crc::crc32; use deflate::inflate; @@ -725,10 +726,13 @@ fn optimize_png(png: &mut PngData, original_data: &[u8], opts: &Options) -> PngR fn perform_reductions(png: &mut PngData, opts: &Options, deadline: &Deadline) -> bool { let mut reduction_occurred = false; - if opts.palette_reduction && png.reduce_palette() { - reduction_occurred = true; - if opts.verbosity == Some(1) { - report_reduction(png); + if opts.palette_reduction { + if let Some(reduced) = reduced_palette(png) { + png.apply_reduction(reduced); + reduction_occurred = true; + if opts.verbosity == Some(1) { + report_reduction(png); + } } } @@ -736,10 +740,13 @@ fn perform_reductions(png: &mut PngData, opts: &Options, deadline: &Deadline) -> return reduction_occurred; } - if opts.bit_depth_reduction && png.reduce_bit_depth() { - reduction_occurred = true; - if opts.verbosity == Some(1) { - report_reduction(png); + if opts.bit_depth_reduction { + if let Some(reduced) = png.reduce_bit_depth() { + png.apply_reduction(reduced); + reduction_occurred = true; + if opts.verbosity == Some(1) { + report_reduction(png); + } } } @@ -759,8 +766,8 @@ fn perform_reductions(png: &mut PngData, opts: &Options, deadline: &Deadline) -> } if let Some(interlacing) = opts.interlace { - if png.change_interlacing(interlacing) { - png.ihdr_data.interlaced = interlacing; + if let Some(reduced) = png.change_interlacing(interlacing) { + png.apply_reduction(reduced); reduction_occurred = true; } } diff --git a/src/png/mod.rs b/src/png/mod.rs index b3b38777..25526cdc 100644 --- a/src/png/mod.rs +++ b/src/png/mod.rs @@ -4,6 +4,7 @@ use colors::{AlphaOptim, BitDepth, ColorType}; use crc::crc32; use deflate; use error::PngError; +use reduction::*; use filters::*; use headers::*; use interlace::{deinterlace_image, interlace_image}; @@ -12,9 +13,9 @@ use itertools::flatten; use rayon::prelude::*; use reduction::bit_depth::*; use reduction::color::*; +use reduction::alpha::*; use rgb::ComponentSlice; use rgb::RGBA8; -use std::collections::hash_map::Entry::*; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::{Read, Seek, SeekFrom}; @@ -327,14 +328,15 @@ impl PngData { /// Attempt to reduce the bit depth of the image /// Returns true if the bit depth was reduced, false otherwise - pub fn reduce_bit_depth(&mut self) -> bool { + #[must_use] + pub fn reduce_bit_depth(&self) -> Option { if self.ihdr_data.bit_depth != BitDepth::Sixteen { if self.ihdr_data.color_type == ColorType::Indexed || self.ihdr_data.color_type == ColorType::Grayscale { return reduce_bit_depth_8_or_less(self); } - return false; + return None; } // Reduce from 16 to 8 bits per channel per pixel @@ -354,131 +356,22 @@ impl PngData { // Low byte if high_byte != byte { // Can't reduce, exit early - return false; + return None; } reduced.push(byte); } } } - self.ihdr_data.bit_depth = BitDepth::Eight; - self.raw_data = reduced; - true - } - - /// Attempt to reduce the number of colors in the palette - /// Returns true if the palette was reduced, false otherwise - pub fn reduce_palette(&mut self) -> bool { - if self.ihdr_data.color_type != ColorType::Indexed { - // Can't reduce if there is no palette - return false; - } - if self.ihdr_data.bit_depth == BitDepth::One { - // Gains from 1-bit images will be at most 1 byte - // Not worth the CPU time - return false; - } - - let mut palette_map = [0u8; 256]; - let mut used = [false; 256]; - { - let palette = match self.palette { - Some(ref p) => p, - None => return false, - }; - - // Find palette entries that are never used - for line in self.scan_lines() { - match self.ihdr_data.bit_depth { - BitDepth::Eight => for &byte in line.data { - used[byte as usize] = true; - }, - BitDepth::Four => for &byte in line.data { - used[(byte & 0x0F) as usize] = true; - used[(byte >> 4) as usize] = true; - }, - BitDepth::Two => for &byte in line.data { - used[(byte & 0x03) as usize] = true; - used[((byte >> 2) & 0x03) as usize] = true; - used[((byte >> 4) & 0x03) as usize] = true; - used[(byte >> 6) as usize] = true; - }, - _ => unreachable!(), - } - } - - let mut next_index = 0; - let mut seen = HashMap::with_capacity(palette.len()); - for (i, (used, palette_map)) in - used.iter().cloned().zip(palette_map.iter_mut()).enumerate() - { - if !used { - continue; - } - // There are invalid files that use pixel indices beyond palette size - let color = palette - .get(i) - .cloned() - .unwrap_or_else(|| RGBA8::new(0, 0, 0, 255)); - match seen.entry(color) { - Vacant(new) => { - *palette_map = next_index; - new.insert(next_index); - next_index += 1; - } - Occupied(remap_to) => { - *palette_map = *remap_to.get(); - } - } - } - if (0..palette.len()).all(|i| palette_map[i] == i as u8) { - return false; - } - } - - self.do_palette_reduction(&palette_map, &used); - true - } - - fn do_palette_reduction(&mut self, palette_map: &[u8; 256], used: &[bool; 256]) { - let mut byte_map = *palette_map; - - // low bit-depths can be pre-computed for every byte value - match self.ihdr_data.bit_depth { - BitDepth::Four => for byte in 0..=255 { - byte_map[byte as usize] = - palette_map[(byte & 0x0F) as usize] | (palette_map[(byte >> 4) as usize] << 4); - }, - BitDepth::Two => for byte in 0..=255 { - byte_map[byte as usize] = palette_map[(byte & 0x03) as usize] - | (palette_map[((byte >> 2) & 0x03) as usize] << 2) - | (palette_map[((byte >> 4) & 0x03) as usize] << 4) - | (palette_map[(byte >> 6) as usize] << 6); - }, - _ => {} - } - - // Reassign data bytes to new indices - for line in self.scan_lines_mut() { - for byte in line.data { - *byte = byte_map[*byte as usize]; - } - } - - self.transparency_pixel = None; - if let Some(palette) = self.palette.take() { - let max_index = palette_map.iter().max().cloned().unwrap_or(0) as usize; - let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1]; - for (color, (map_to, used)) in palette - .into_iter() - .zip(palette_map.iter().cloned().zip(used.iter().cloned())) - { - if used { - new_palette[map_to as usize] = color; - } - } - self.palette = Some(new_palette); - } + Some(ReducedPng { + color_type: self.ihdr_data.color_type, + interlaced: self.ihdr_data.interlaced, + bit_depth: BitDepth::Eight, + raw_data: reduced, + palette: self.palette.clone(), + transparency_pixel: self.transparency_pixel.clone(), + aux_headers: Default::default(), + }) } /// Attempt to reduce the color type of the image @@ -490,37 +383,65 @@ impl PngData { // Go down one step at a time // Maybe not the most efficient, but it's safe if self.ihdr_data.color_type == ColorType::RGBA { - if reduce_rgba_to_grayscale_alpha(self) || reduce_rgba_to_rgb(self) { + if let Some(reduced) = reduce_rgba_to_grayscale_alpha(self).or_else(|| reduced_alpha_channel(self)) { + self.apply_reduction(reduced); changed = true; - } else if reduce_color_to_palette(self) { + } else if let Some(reduced) = reduced_color_to_palette(self) { + self.apply_reduction(reduced); changed = true; should_reduce_bit_depth = true; } } - if self.ihdr_data.color_type == ColorType::GrayscaleAlpha - && reduce_grayscale_alpha_to_grayscale(self) - { - changed = true; - should_reduce_bit_depth = true; + if self.ihdr_data.color_type == ColorType::GrayscaleAlpha { + if let Some(reduced) = reduced_alpha_channel(self) { + self.apply_reduction(reduced); + changed = true; + should_reduce_bit_depth = true; + } } - if self.ihdr_data.color_type == ColorType::RGB - && (reduce_rgb_to_grayscale(self) || reduce_color_to_palette(self)) - { - changed = true; - should_reduce_bit_depth = true; + if self.ihdr_data.color_type == ColorType::RGB { + if let Some(reduced) = reduce_rgb_to_grayscale(self).or_else(|| reduced_color_to_palette(self)) { + self.apply_reduction(reduced); + changed = true; + should_reduce_bit_depth = true; + } } if should_reduce_bit_depth { // Some conversions will allow us to perform bit depth reduction that // wasn't possible before - reduce_bit_depth_8_or_less(self); + if let Some(reduced) = reduce_bit_depth_8_or_less(self) { + self.apply_reduction(reduced); + } } changed } + pub(crate) fn apply_reduction(&mut self, ReducedPng {color_type, bit_depth, raw_data, interlaced, palette, transparency_pixel, aux_headers}: ReducedPng) { + self.ihdr_data.color_type = color_type; + self.ihdr_data.bit_depth = bit_depth; + self.ihdr_data.interlaced = interlaced; + self.raw_data = raw_data; + if palette.is_some() { + self.transparency_pixel = None; + self.palette = palette; + } + if transparency_pixel.is_some() { + self.transparency_pixel = transparency_pixel; + } + self.idat_data.clear(); // this field is out of date and needs to be replaced + + for (header, val) in aux_headers { + match val { + Some(val) => self.aux_headers.insert(header, val), + None => self.aux_headers.remove(&header), + }; + } + } + pub fn try_alpha_reduction(&mut self, alphas: &HashSet) -> bool { assert!(!alphas.is_empty()); let alphas = alphas.iter().collect::>(); @@ -733,19 +654,19 @@ impl PngData { /// The `interlace` parameter specifies the *new* interlacing mode /// Assumes that the data has already been de-filtered #[inline] - pub fn change_interlacing(&mut self, interlace: u8) -> bool { + #[must_use] + pub fn change_interlacing(&mut self, interlace: u8) -> Option { if interlace == self.ihdr_data.interlaced { - return false; + return None; } - if interlace == 1 { + Some(if interlace == 1 { // Convert progressive to interlaced data - interlace_image(self); + interlace_image(self) } else { // Convert interlaced to progressive data - deinterlace_image(self); - } - true + deinterlace_image(self) + }) } } diff --git a/src/reduction/alpha.rs b/src/reduction/alpha.rs index 0380c697..04d59821 100644 --- a/src/reduction/alpha.rs +++ b/src/reduction/alpha.rs @@ -1,7 +1,17 @@ +use reduction::ReducedPng; use png::PngData; +use colors::ColorType; +use std::collections::HashMap; -pub fn reduce_alpha_channel(png: &mut PngData, channels: u8) -> Option> { +#[must_use] +pub fn reduced_alpha_channel(png: &PngData) -> Option { + let target_color_type = match png.ihdr_data.color_type { + ColorType::GrayscaleAlpha => ColorType::Grayscale, + ColorType::RGBA => ColorType::RGB, + _ => return None, + }; let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3; + let channels = png.channels_per_pixel(); let bpp = channels * byte_depth; let bpp_mask = bpp - 1; assert_eq!(0, bpp & bpp_mask); @@ -14,27 +24,33 @@ pub fn reduce_alpha_channel(png: &mut PngData, channels: u8) -> Option> } } - let mut reduced = Vec::with_capacity(png.raw_data.len()); + let mut raw_data = Vec::with_capacity(png.raw_data.len()); for line in png.scan_lines() { - reduced.push(line.filter); + raw_data.push(line.filter); for (i, &byte) in line.data.iter().enumerate() { if i as u8 & bpp_mask >= colored_bytes { continue; } else { - reduced.push(byte); + raw_data.push(byte); } } } + let mut aux_headers = HashMap::new(); // sBIT contains information about alpha channel's original depth, // and alpha has just been removed - if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") { + if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { // Some programs save the sBIT header as RGB even if the image is RGBA. - // Only remove the alpha channel if it's actually there. - if sbit_header.len() == 4 { - sbit_header.pop(); - } + aux_headers.insert(*b"sBIT", Some(sbit_header.iter().cloned().take(3).collect())); } - Some(reduced) + Some(ReducedPng { + raw_data, + bit_depth: png.ihdr_data.bit_depth, + interlaced: png.ihdr_data.interlaced, + color_type: target_color_type, + aux_headers, + transparency_pixel: None, + palette: None, + }) } diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index ee3f0665..10afca51 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -1,3 +1,4 @@ +use reduction::ReducedPng; use bit_vec::BitVec; use colors::{BitDepth, ColorType}; use png::PngData; @@ -24,7 +25,8 @@ const FOUR_BIT_PERMUTATIONS: [u8; 11] = [ 0b1111_1111, ]; -pub fn reduce_bit_depth_8_or_less(png: &mut PngData) -> bool { +#[must_use] +pub fn reduce_bit_depth_8_or_less(png: &PngData) -> Option { let mut reduced = BitVec::with_capacity(png.raw_data.len() * 8); let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize; let mut allowed_bits = 1; @@ -37,7 +39,7 @@ pub fn reduce_bit_depth_8_or_less(png: &mut PngData) -> bool { allowed_bits = bit_index.next_power_of_two(); if allowed_bits == bit_depth { // Not reducable - return false; + return None; } } } @@ -51,7 +53,7 @@ pub fn reduce_bit_depth_8_or_less(png: &mut PngData) -> bool { } else if allowed_bits == 4 { &FOUR_BIT_PERMUTATIONS } else { - return false; + return None; }; if permutations.iter().any(|perm| *perm == byte) { break; @@ -78,7 +80,13 @@ pub fn reduce_bit_depth_8_or_less(png: &mut PngData) -> bool { } } - png.raw_data = reduced.to_bytes(); - png.ihdr_data.bit_depth = BitDepth::from_u8(allowed_bits as u8); - true + Some(ReducedPng { + color_type: png.ihdr_data.color_type, + interlaced: png.ihdr_data.interlaced, + raw_data: reduced.to_bytes(), + bit_depth: BitDepth::from_u8(allowed_bits as u8), + aux_headers: Default::default(), + palette: png.palette.clone(), + transparency_pixel: png.transparency_pixel.clone(), + }) } diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 87cec441..c773b933 100644 --- a/src/reduction/color.rs +++ b/src/reduction/color.rs @@ -1,3 +1,4 @@ +use reduction::ReducedPng; use colors::{BitDepth, ColorType}; use itertools::Itertools; use png::PngData; @@ -5,19 +6,8 @@ use rgb::{FromSlice, RGB8, RGBA8}; use std::collections::HashMap; use std::hash::Hash; -use super::alpha::reduce_alpha_channel; - -pub fn reduce_rgba_to_rgb(png: &mut PngData) -> bool { - if let Some(reduced) = reduce_alpha_channel(png, 4) { - png.raw_data = reduced; - png.ihdr_data.color_type = ColorType::RGB; - true - } else { - false - } -} - -pub fn reduce_rgba_to_grayscale_alpha(png: &mut PngData) -> bool { +#[must_use] +pub fn reduce_rgba_to_grayscale_alpha(png: &PngData) -> Option { let mut reduced = Vec::with_capacity(png.raw_data.len()); let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3; let bpp = 4 * byte_depth; @@ -42,11 +32,11 @@ pub fn reduce_rgba_to_grayscale_alpha(png: &mut PngData) -> bool { if (i as u8 & bpp_mask) == bpp - 1 { if low_bytes.iter().unique().count() > 1 { - return false; + return None; } if byte_depth == 2 { if high_bytes.iter().unique().count() > 1 { - return false; + return None; } reduced.push(high_bytes[0]); high_bytes.clear(); @@ -59,19 +49,24 @@ pub fn reduce_rgba_to_grayscale_alpha(png: &mut PngData) -> bool { } } - if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") { - assert!(sbit_header.len() >= 3); - sbit_header.remove(1); - sbit_header.remove(1); - } - if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") { - assert_eq!(bkgd_header.len(), 6); - bkgd_header.truncate(2); + let mut aux_headers = HashMap::new(); + if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { + aux_headers.insert(*b"sBIT", sbit_header.get(0).map(|&s| vec![s])); } - png.raw_data = reduced; - png.ihdr_data.color_type = ColorType::GrayscaleAlpha; - true + if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { + aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned())); + } + + Some(ReducedPng { + raw_data: reduced, + bit_depth: png.ihdr_data.bit_depth, + interlaced: png.ihdr_data.interlaced, + color_type: ColorType::GrayscaleAlpha, + palette: None, + transparency_pixel: None, + aux_headers, + }) } fn reduce_scanline_to_palette( @@ -99,18 +94,19 @@ where true } -pub fn reduce_color_to_palette(png: &mut PngData) -> bool { +#[must_use] +pub fn reduced_color_to_palette(png: &PngData) -> Option { if png.ihdr_data.bit_depth != BitDepth::Eight { - return false; + return None; } - let mut reduced = Vec::with_capacity(png.raw_data.len()); + let mut raw_data = Vec::with_capacity(png.raw_data.len()); let mut palette = HashMap::with_capacity(257); let transparency_pixel = png .transparency_pixel .as_ref() .map(|t| RGB8::new(t[1], t[3], t[5])); for line in png.scan_lines() { - reduced.push(line.filter); + raw_data.push(line.filter); let ok = if png.ihdr_data.color_type == ColorType::RGB { reduce_scanline_to_palette( line.data.as_rgb().iter().cloned().map(|px| { @@ -121,18 +117,18 @@ pub fn reduce_color_to_palette(png: &mut PngData) -> bool { }) }), &mut palette, - &mut reduced, + &mut raw_data, ) } else { debug_assert_eq!(png.ihdr_data.color_type, ColorType::RGBA); reduce_scanline_to_palette( line.data.as_rgba().iter().cloned(), &mut palette, - &mut reduced, + &mut raw_data, ) }; if !ok { - return false; + return None; } } @@ -148,12 +144,13 @@ pub fn reduce_color_to_palette(png: &mut PngData) -> bool { let trns_size = num_transparent.map(|n| n + 8).unwrap_or(0); let headers_size = palette.len() * 3 + 8 + trns_size; - if reduced.len() + headers_size > png.raw_data.len() { + if raw_data.len() + headers_size > png.raw_data.len() { // Reduction would result in a larger image - return false; + return None; } - if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") { + let mut aux_headers = HashMap::new(); + if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { assert_eq!(bkgd_header.len(), 6); // In bKGD 16-bit values are used even for 8-bit images let bg = RGBA8::new(bkgd_header[1], bkgd_header[3], bkgd_header[5], 255); @@ -164,17 +161,14 @@ pub fn reduce_color_to_palette(png: &mut PngData) -> bool { palette.insert(bg, entry); entry } else { - return false; + return None; // No space in palette to store the bg as an index }; - *bkgd_header = vec![entry]; + aux_headers.insert(*b"bKGD", Some(vec![entry])); } - if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") { + if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { // Some programs save the sBIT header as RGB even if the image is RGBA. - // Only remove the alpha channel if it's actually there. - if sbit_header.len() == 4 { - sbit_header.pop(); - } + aux_headers.insert(*b"sBIT", Some(sbit_header.iter().cloned().take(3).collect())); } let mut palette_vec = vec![RGBA8::new(0, 0, 0, 0); palette.len()]; @@ -182,14 +176,19 @@ pub fn reduce_color_to_palette(png: &mut PngData) -> bool { palette_vec[idx as usize] = color; } - png.raw_data = reduced; - png.transparency_pixel = None; - png.palette = Some(palette_vec); - png.ihdr_data.color_type = ColorType::Indexed; - true + Some(ReducedPng { + color_type: ColorType::Indexed, + bit_depth: png.ihdr_data.bit_depth, + interlaced: png.ihdr_data.interlaced, + aux_headers, + raw_data, + transparency_pixel: None, + palette: Some(palette_vec), + }) } -pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool { +#[must_use] +pub fn reduce_rgb_to_grayscale(png: &PngData) -> Option { let mut reduced = Vec::with_capacity(png.raw_data.len()); let byte_depth: u8 = png.ihdr_data.bit_depth.as_u8() >> 3; let bpp: usize = 3 * byte_depth as usize; @@ -201,7 +200,7 @@ pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool { if i % bpp == bpp - 1 { if bpp == 3 { if cur_pixel.iter().unique().count() > 1 { - return false; + return None; } reduced.push(cur_pixel[0]); } else { @@ -213,7 +212,7 @@ pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool { .unique() .collect::>(); if pixel_bytes.len() > 1 { - return false; + return None; } reduced.push(pixel_bytes[0].0); reduced.push(pixel_bytes[0].1); @@ -222,33 +221,32 @@ pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool { } } } - if let Some(ref mut trns) = png.transparency_pixel { - assert_eq!(trns.len(), 6); - if trns[0..2] != trns[2..4] || trns[2..4] != trns[4..6] { - return false; + + let transparency_pixel = if let Some(ref trns) = png.transparency_pixel { + if trns.len() != 6 || trns[0..2] != trns[2..4] || trns[2..4] != trns[4..6] { + None + } else { + Some(trns[0..2].to_owned()) } - *trns = trns[0..2].to_owned(); - } - if let Some(sbit_header) = png.aux_headers.get_mut(b"sBIT") { - assert_eq!(sbit_header.len(), 3); - sbit_header.truncate(1); - } - if let Some(bkgd_header) = png.aux_headers.get_mut(b"bKGD") { - assert_eq!(bkgd_header.len(), 6); - bkgd_header.truncate(2); - } - - png.raw_data = reduced; - png.ihdr_data.color_type = ColorType::Grayscale; - true -} - -pub fn reduce_grayscale_alpha_to_grayscale(png: &mut PngData) -> bool { - if let Some(reduced) = reduce_alpha_channel(png, 2) { - png.raw_data = reduced; - png.ihdr_data.color_type = ColorType::Grayscale; - true } else { - false + png.transparency_pixel.clone() + }; + + let mut aux_headers = HashMap::new(); + if let Some(sbit_header) = png.aux_headers.get(b"sBIT") { + aux_headers.insert(*b"sBIT", sbit_header.get(0).map(|&byte| vec![byte])); } + if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { + aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned())); + } + + Some(ReducedPng { + raw_data: reduced, + color_type: ColorType::Grayscale, + bit_depth: png.ihdr_data.bit_depth, + interlaced: png.ihdr_data.interlaced, + palette: None, + transparency_pixel, + aux_headers, + }) } diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index eb6ce754..09d32d31 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,3 +1,169 @@ -mod alpha; +use std::collections::HashMap; +use colors::{BitDepth, ColorType}; +use std::collections::hash_map::Entry::*; +use png::PngData; +use rgb::RGBA8; + +pub mod alpha; pub mod bit_depth; pub mod color; + +/// Fields to replace in PngData to apply the reduction +pub struct ReducedPng { + pub color_type: ColorType, + pub raw_data: Vec, + pub bit_depth: BitDepth, + /// replace if Some + pub palette: Option>, + /// replace if Some + pub transparency_pixel: Option>, + /// replace if Some, delete if None + pub aux_headers: HashMap<[u8; 4], Option>>, + pub interlaced: u8, +} + +/// Attempt to reduce the number of colors in the palette +/// Returns `None` if palette hasn't changed +#[must_use] +pub fn reduced_palette(png: &PngData) -> Option { + if png.ihdr_data.color_type != ColorType::Indexed { + // Can't reduce if there is no palette + return None; + } + if png.ihdr_data.bit_depth == BitDepth::One { + // Gains from 1-bit images will be at most 1 byte + // Not worth the CPU time + return None; + } + + let mut palette_map = [None; 256]; + let mut used = [false; 256]; + { + let palette = png.palette.as_ref()?; + + // Find palette entries that are never used + for line in png.scan_lines() { + match png.ihdr_data.bit_depth { + BitDepth::Eight => for &byte in line.data { + used[byte as usize] = true; + }, + BitDepth::Four => for &byte in line.data { + used[(byte & 0x0F) as usize] = true; + used[(byte >> 4) as usize] = true; + }, + BitDepth::Two => for &byte in line.data { + used[(byte & 0x03) as usize] = true; + used[((byte >> 2) & 0x03) as usize] = true; + used[((byte >> 4) & 0x03) as usize] = true; + used[(byte >> 6) as usize] = true; + }, + _ => unreachable!(), + } + } + + let mut next_index = 0u16; + let mut seen = HashMap::with_capacity(palette.len()); + for (i, (used, palette_map)) in + used.iter().cloned().zip(palette_map.iter_mut()).enumerate() + { + if !used { + continue; + } + // There are invalid files that use pixel indices beyond palette size + let color = palette.get(i).cloned().unwrap_or(RGBA8::new(0, 0, 0, 255)); + match seen.entry(color) { + Vacant(new) => { + *palette_map = Some(next_index as u8); + new.insert(next_index as u8); + next_index += 1; + } + Occupied(remap_to) => { + *palette_map = Some(*remap_to.get()); + } + } + } + } + + do_palette_reduction(png, &palette_map) +} + +#[must_use] +fn do_palette_reduction(png: &PngData, palette_map: &[Option; 256]) -> Option { + let byte_map = palette_map_to_byte_map(png, palette_map)?; + let mut raw_data = Vec::with_capacity(png.raw_data.len()); + + // Reassign data bytes to new indices + for line in png.scan_lines() { + raw_data.push(line.filter); + for byte in line.data { + raw_data.push(byte_map[*byte as usize]); + } + } + + let mut aux_headers = HashMap::new(); + if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") { + if let Some(Some(map_to)) = bkgd_header.get(0).and_then(|&idx| palette_map.get(idx as usize)) { + aux_headers.insert(*b"bKGD", Some(vec![*map_to])); + } + } + + Some(ReducedPng { + color_type: ColorType::Indexed, + bit_depth: png.ihdr_data.bit_depth, + interlaced: png.ihdr_data.interlaced, + raw_data, + transparency_pixel: None, + palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)), + aux_headers, + }) +} + +fn palette_map_to_byte_map(png: &PngData, palette_map: &[Option; 256]) -> Option<[u8; 256]> { + let len = png.palette.as_ref().map(|p| p.len()).unwrap_or(0); + if (0..len).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) { + // No reduction necessary + return None; + } + + let mut byte_map = [0u8; 256]; + + // low bit-depths can be pre-computed for every byte value + match png.ihdr_data.bit_depth { + BitDepth::Eight => { + for byte in 0..=255 { + byte_map[byte as usize] = palette_map[byte as usize].unwrap_or(0) + } + } + BitDepth::Four => { + for byte in 0..=255 { + byte_map[byte as usize] = palette_map[(byte & 0x0F) as usize].unwrap_or(0) + | (palette_map[(byte >> 4) as usize].unwrap_or(0) << 4); + } + } + BitDepth::Two => { + for byte in 0..=255 { + byte_map[byte as usize] = palette_map[(byte & 0x03) as usize].unwrap_or(0) + | (palette_map[((byte >> 2) & 0x03) as usize].unwrap_or(0) << 2) + | (palette_map[((byte >> 4) & 0x03) as usize].unwrap_or(0) << 4) + | (palette_map[(byte >> 6) as usize].unwrap_or(0) << 6); + } + } + _ => {} + } + + return Some(byte_map) +} + +fn reordered_palette(palette: &[RGBA8], palette_map: &[Option; 256]) -> Vec { + let max_index = palette_map.iter().cloned() + .filter_map(|x| x) + .max() + .unwrap_or(0) as usize; + let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1]; + for (&color, &map_to) in palette.iter().zip(palette_map.iter()) { + if let Some(map_to) = map_to { + new_palette[map_to as usize] = color; + } + } + new_palette +}