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:
Kornel 2019-01-12 10:07:57 +00:00 committed by Josh Holmer
parent d91fc890cd
commit 55d85df9fc
8 changed files with 398 additions and 262 deletions

View file

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

View file

@ -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)]

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

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

View file

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

View file

@ -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(),
})
}

View file

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

View file

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