Move reductions to a module. Make copy instead of changing in-place.

This commit is contained in:
Kornel Lesiński 2018-11-26 16:18:35 +00:00 committed by Kornel Lesiński
parent d91fc890cd
commit e5e51cdaa8
4 changed files with 216 additions and 149 deletions

View file

@ -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);
}
}
}

View file

@ -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<AlphaOptim>) -> bool {
assert!(!alphas.is_empty());
let alphas = alphas.iter().collect::<Vec<_>>();

View file

@ -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<ReducedPng> {
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 {

View file

@ -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<u8>,
pub palette: Option<Vec<RGBA8>>,
pub aux_headers: HashMap<[u8; 4], Option<Vec<u8>>>,
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<ReducedPng> {
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<u8>; 256]) -> Option<ReducedPng> {
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<u8>; 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<u8>; 256]) -> Vec<RGBA8> {
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
}