From e5e51cdaa86554f52547c8694f8504ae0a3b734c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kornel=20Lesin=CC=81ski?= Date: Mon, 26 Nov 2018 16:18:35 +0000 Subject: [PATCH] Move reductions to a module. Make copy instead of changing in-place. --- src/lib.rs | 12 ++-- src/png/mod.rs | 155 +++++++++-------------------------------- src/reduction/color.rs | 44 ++++++------ src/reduction/mod.rs | 154 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 149 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b6119de8..ee7f03dd 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); + } } } diff --git a/src/png/mod.rs b/src/png/mod.rs index b3b38777..6ae59eae 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}; @@ -14,7 +15,6 @@ use reduction::bit_depth::*; use reduction::color::*; 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}; @@ -366,121 +366,6 @@ impl PngData { 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); - } - } - /// Attempt to reduce the color type of the image /// Returns true if the color type was reduced, false otherwise pub fn reduce_color_type(&mut self) -> bool { @@ -490,9 +375,12 @@ 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 reduce_rgba_to_grayscale_alpha(self) { changed = true; - } else if reduce_color_to_palette(self) { + } else if reduce_rgba_to_rgb(self) { + changed = true; + } else if let Some(reduced) = reduced_color_to_palette(self) { + self.apply_reduction(reduced); changed = true; should_reduce_bit_depth = true; } @@ -505,11 +393,15 @@ impl PngData { 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 reduce_rgb_to_grayscale(self) { + changed = true; + should_reduce_bit_depth = true; + } else if let Some(reduced) = reduced_color_to_palette(self) { + self.apply_reduction(reduced); + changed = true; + should_reduce_bit_depth = true; + } } if should_reduce_bit_depth { @@ -521,6 +413,23 @@ impl PngData { changed } + pub(crate) fn apply_reduction(&mut self, ReducedPng {color_type, raw_data, palette, aux_headers}: ReducedPng) { + self.ihdr_data.color_type = color_type; + self.raw_data = raw_data; + if let Some(palette) = palette { + self.transparency_pixel = None; + self.palette = Some(palette); + } + 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::>(); diff --git a/src/reduction/color.rs b/src/reduction/color.rs index 87cec441..4fc2d082 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; @@ -99,18 +100,18 @@ where true } -pub fn reduce_color_to_palette(png: &mut PngData) -> bool { +pub fn reduced_color_to_palette(png: &mut 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 +122,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 +149,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 +166,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,11 +181,12 @@ 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, + aux_headers, + raw_data, + palette: Some(palette_vec), + }) } pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool { diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index eb6ce754..9064a810 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -1,3 +1,157 @@ +use std::collections::HashMap; +use colors::{BitDepth, ColorType}; +use std::collections::hash_map::Entry::*; +use png::PngData; +use rgb::RGBA8; + mod alpha; pub mod bit_depth; pub mod color; + +pub struct ReducedPng { + pub raw_data: Vec, + pub palette: Option>, + pub aux_headers: HashMap<[u8; 4], Option>>, + pub color_type: ColorType, +} + +/// Attempt to reduce the number of colors in the palette +/// Returns true if the palette was reduced, false otherwise +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) +} + +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, + raw_data, + 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 +}