Move reductions to a module. Make copy instead of changing in-place.
This commit is contained in:
parent
d91fc890cd
commit
e5e51cdaa8
4 changed files with 216 additions and 149 deletions
12
src/lib.rs
12
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
155
src/png/mod.rs
155
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<AlphaOptim>) -> bool {
|
||||
assert!(!alphas.is_empty());
|
||||
let alphas = alphas.iter().collect::<Vec<_>>();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue