Immutable color reductions

This commit is contained in:
Kornel Lesiński 2019-01-10 14:58:18 +00:00 committed by Kornel Lesiński
parent 52a22e9e03
commit e8beb76192
6 changed files with 94 additions and 58 deletions

View file

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

View file

@ -328,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
@ -355,16 +356,21 @@ 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
Some(ReducedPng {
color_type: self.ihdr_data.color_type,
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
@ -376,9 +382,7 @@ 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) {
changed = true;
} else if let Some(reduced) = reduced_alpha_channel(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 let Some(reduced) = reduced_color_to_palette(self) {
@ -397,10 +401,7 @@ impl PngData {
}
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) {
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;
@ -410,14 +411,17 @@ impl PngData {
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, raw_data, palette, transparency_pixel, aux_headers}: ReducedPng) {
pub(crate) fn apply_reduction(&mut self, ReducedPng {color_type, bit_depth, raw_data, palette, transparency_pixel, aux_headers}: ReducedPng) {
self.ihdr_data.color_type = color_type;
self.ihdr_data.bit_depth = bit_depth;
self.raw_data = raw_data;
if palette.is_some() {
self.transparency_pixel = None;

View file

@ -3,6 +3,7 @@ use png::PngData;
use colors::ColorType;
use std::collections::HashMap;
#[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,
@ -45,6 +46,7 @@ pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> {
Some(ReducedPng {
raw_data,
bit_depth: png.ihdr_data.bit_depth,
color_type: target_color_type,
aux_headers,
transparency_pixel: 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,12 @@ 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,
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

@ -6,7 +6,8 @@ use rgb::{FromSlice, RGB8, RGBA8};
use std::collections::HashMap;
use std::hash::Hash;
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;
@ -31,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();
@ -48,19 +49,23 @@ 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,
color_type: ColorType::GrayscaleAlpha,
palette: None,
transparency_pixel: None,
aux_headers,
})
}
fn reduce_scanline_to_palette<T>(
@ -88,7 +93,8 @@ where
true
}
pub fn reduced_color_to_palette(png: &mut PngData) -> Option<ReducedPng> {
#[must_use]
pub fn reduced_color_to_palette(png: &PngData) -> Option<ReducedPng> {
if png.ihdr_data.bit_depth != BitDepth::Eight {
return None;
}
@ -171,6 +177,7 @@ pub fn reduced_color_to_palette(png: &mut PngData) -> Option<ReducedPng> {
Some(ReducedPng {
color_type: ColorType::Indexed,
bit_depth: png.ihdr_data.bit_depth,
aux_headers,
raw_data,
transparency_pixel: None,
@ -178,7 +185,8 @@ pub fn reduced_color_to_palette(png: &mut PngData) -> Option<ReducedPng> {
})
}
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;
@ -190,7 +198,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 {
@ -202,7 +210,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);
@ -211,23 +219,31 @@ 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();
} else {
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(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);
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()));
}
png.raw_data = reduced;
png.ihdr_data.color_type = ColorType::Grayscale;
true
Some(ReducedPng {
raw_data: reduced,
color_type: ColorType::Grayscale,
bit_depth: png.ihdr_data.bit_depth,
palette: None,
transparency_pixel,
aux_headers,
})
}

View file

@ -12,6 +12,7 @@ pub mod color;
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
@ -22,6 +23,7 @@ pub struct ReducedPng {
/// 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
@ -84,6 +86,7 @@ pub fn reduced_palette(png: &PngData) -> Option<ReducedPng> {
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());
@ -105,6 +108,7 @@ fn do_palette_reduction(png: &PngData, palette_map: &[Option<u8>; 256]) -> Optio
Some(ReducedPng {
color_type: ColorType::Indexed,
bit_depth: png.ihdr_data.bit_depth,
raw_data,
transparency_pixel: None,
palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)),