Non-self-mutable reductions (#154)
* Move reductions to a module. Make copy instead of changing in-place. * Alpha reductions * Immutable color reductions * Immutable interlace reductions
This commit is contained in:
parent
d91fc890cd
commit
55d85df9fc
8 changed files with 398 additions and 262 deletions
|
|
@ -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)
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BitVec> = 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)]
|
||||
|
|
|
|||
27
src/lib.rs
27
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
205
src/png/mod.rs
205
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<ReducedPng> {
|
||||
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<AlphaOptim>) -> bool {
|
||||
assert!(!alphas.is_empty());
|
||||
let alphas = alphas.iter().collect::<Vec<_>>();
|
||||
|
|
@ -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<ReducedPng> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Vec<u8>> {
|
||||
#[must_use]
|
||||
pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> {
|
||||
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<Vec<u8>>
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ReducedPng> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ReducedPng> {
|
||||
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<T>(
|
||||
|
|
@ -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<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 +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<ReducedPng> {
|
||||
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::<Vec<(u8, u8)>>();
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
pub bit_depth: BitDepth,
|
||||
/// replace if Some
|
||||
pub palette: Option<Vec<RGBA8>>,
|
||||
/// replace if Some
|
||||
pub transparency_pixel: Option<Vec<u8>>,
|
||||
/// replace if Some, delete if None
|
||||
pub aux_headers: HashMap<[u8; 4], Option<Vec<u8>>>,
|
||||
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<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)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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,
|
||||
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<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