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,12 +740,15 @@ fn perform_reductions(png: &mut PngData, opts: &Options, deadline: &Deadline) ->
return reduction_occurred; return reduction_occurred;
} }
if opts.bit_depth_reduction && png.reduce_bit_depth() { if opts.bit_depth_reduction {
if let Some(reduced) = png.reduce_bit_depth() {
png.apply_reduction(reduced);
reduction_occurred = true; reduction_occurred = true;
if opts.verbosity == Some(1) { if opts.verbosity == Some(1) {
report_reduction(png); report_reduction(png);
} }
} }
}
if deadline.passed() { if deadline.passed() {
return reduction_occurred; return reduction_occurred;

View file

@ -328,14 +328,15 @@ impl PngData {
/// Attempt to reduce the bit depth of the image /// Attempt to reduce the bit depth of the image
/// Returns true if the bit depth was reduced, false otherwise /// 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.bit_depth != BitDepth::Sixteen {
if self.ihdr_data.color_type == ColorType::Indexed if self.ihdr_data.color_type == ColorType::Indexed
|| self.ihdr_data.color_type == ColorType::Grayscale || self.ihdr_data.color_type == ColorType::Grayscale
{ {
return reduce_bit_depth_8_or_less(self); return reduce_bit_depth_8_or_less(self);
} }
return false; return None;
} }
// Reduce from 16 to 8 bits per channel per pixel // Reduce from 16 to 8 bits per channel per pixel
@ -355,16 +356,21 @@ impl PngData {
// Low byte // Low byte
if high_byte != byte { if high_byte != byte {
// Can't reduce, exit early // Can't reduce, exit early
return false; return None;
} }
reduced.push(byte); reduced.push(byte);
} }
} }
} }
self.ihdr_data.bit_depth = BitDepth::Eight; Some(ReducedPng {
self.raw_data = reduced; color_type: self.ihdr_data.color_type,
true 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 /// Attempt to reduce the color type of the image
@ -376,9 +382,7 @@ impl PngData {
// Go down one step at a time // Go down one step at a time
// Maybe not the most efficient, but it's safe // Maybe not the most efficient, but it's safe
if self.ihdr_data.color_type == ColorType::RGBA { if self.ihdr_data.color_type == ColorType::RGBA {
if reduce_rgba_to_grayscale_alpha(self) { if let Some(reduced) = reduce_rgba_to_grayscale_alpha(self).or_else(|| reduced_alpha_channel(self)) {
changed = true;
} else if let Some(reduced) = reduced_alpha_channel(self) {
self.apply_reduction(reduced); self.apply_reduction(reduced);
changed = true; changed = true;
} else if let Some(reduced) = reduced_color_to_palette(self) { } 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 self.ihdr_data.color_type == ColorType::RGB {
if reduce_rgb_to_grayscale(self) { if let Some(reduced) = reduce_rgb_to_grayscale(self).or_else(|| reduced_color_to_palette(self)) {
changed = true;
should_reduce_bit_depth = true;
} else if let Some(reduced) = reduced_color_to_palette(self) {
self.apply_reduction(reduced); self.apply_reduction(reduced);
changed = true; changed = true;
should_reduce_bit_depth = true; should_reduce_bit_depth = true;
@ -410,14 +411,17 @@ impl PngData {
if should_reduce_bit_depth { if should_reduce_bit_depth {
// Some conversions will allow us to perform bit depth reduction that // Some conversions will allow us to perform bit depth reduction that
// wasn't possible before // 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 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.color_type = color_type;
self.ihdr_data.bit_depth = bit_depth;
self.raw_data = raw_data; self.raw_data = raw_data;
if palette.is_some() { if palette.is_some() {
self.transparency_pixel = None; self.transparency_pixel = None;

View file

@ -3,6 +3,7 @@ use png::PngData;
use colors::ColorType; use colors::ColorType;
use std::collections::HashMap; use std::collections::HashMap;
#[must_use]
pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> { pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> {
let target_color_type = match png.ihdr_data.color_type { let target_color_type = match png.ihdr_data.color_type {
ColorType::GrayscaleAlpha => ColorType::Grayscale, ColorType::GrayscaleAlpha => ColorType::Grayscale,
@ -45,6 +46,7 @@ pub fn reduced_alpha_channel(png: &PngData) -> Option<ReducedPng> {
Some(ReducedPng { Some(ReducedPng {
raw_data, raw_data,
bit_depth: png.ihdr_data.bit_depth,
color_type: target_color_type, color_type: target_color_type,
aux_headers, aux_headers,
transparency_pixel: None, transparency_pixel: None,

View file

@ -1,3 +1,4 @@
use reduction::ReducedPng;
use bit_vec::BitVec; use bit_vec::BitVec;
use colors::{BitDepth, ColorType}; use colors::{BitDepth, ColorType};
use png::PngData; use png::PngData;
@ -24,7 +25,8 @@ const FOUR_BIT_PERMUTATIONS: [u8; 11] = [
0b1111_1111, 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 mut reduced = BitVec::with_capacity(png.raw_data.len() * 8);
let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize; let bit_depth: usize = png.ihdr_data.bit_depth.as_u8() as usize;
let mut allowed_bits = 1; 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(); allowed_bits = bit_index.next_power_of_two();
if allowed_bits == bit_depth { if allowed_bits == bit_depth {
// Not reducable // 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 { } else if allowed_bits == 4 {
&FOUR_BIT_PERMUTATIONS &FOUR_BIT_PERMUTATIONS
} else { } else {
return false; return None;
}; };
if permutations.iter().any(|perm| *perm == byte) { if permutations.iter().any(|perm| *perm == byte) {
break; break;
@ -78,7 +80,12 @@ pub fn reduce_bit_depth_8_or_less(png: &mut PngData) -> bool {
} }
} }
png.raw_data = reduced.to_bytes(); Some(ReducedPng {
png.ihdr_data.bit_depth = BitDepth::from_u8(allowed_bits as u8); color_type: png.ihdr_data.color_type,
true 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::collections::HashMap;
use std::hash::Hash; 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 mut reduced = Vec::with_capacity(png.raw_data.len());
let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3; let byte_depth = png.ihdr_data.bit_depth.as_u8() >> 3;
let bpp = 4 * byte_depth; 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 (i as u8 & bpp_mask) == bpp - 1 {
if low_bytes.iter().unique().count() > 1 { if low_bytes.iter().unique().count() > 1 {
return false; return None;
} }
if byte_depth == 2 { if byte_depth == 2 {
if high_bytes.iter().unique().count() > 1 { if high_bytes.iter().unique().count() > 1 {
return false; return None;
} }
reduced.push(high_bytes[0]); reduced.push(high_bytes[0]);
high_bytes.clear(); 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") { let mut aux_headers = HashMap::new();
assert!(sbit_header.len() >= 3); if let Some(sbit_header) = png.aux_headers.get(b"sBIT") {
sbit_header.remove(1); aux_headers.insert(*b"sBIT", sbit_header.get(0).map(|&s| vec![s]));
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);
} }
png.raw_data = reduced; if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
png.ihdr_data.color_type = ColorType::GrayscaleAlpha; aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned()));
true }
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>( fn reduce_scanline_to_palette<T>(
@ -88,7 +93,8 @@ where
true 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 { if png.ihdr_data.bit_depth != BitDepth::Eight {
return None; return None;
} }
@ -171,6 +177,7 @@ pub fn reduced_color_to_palette(png: &mut PngData) -> Option<ReducedPng> {
Some(ReducedPng { Some(ReducedPng {
color_type: ColorType::Indexed, color_type: ColorType::Indexed,
bit_depth: png.ihdr_data.bit_depth,
aux_headers, aux_headers,
raw_data, raw_data,
transparency_pixel: None, 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 mut reduced = Vec::with_capacity(png.raw_data.len());
let byte_depth: u8 = png.ihdr_data.bit_depth.as_u8() >> 3; let byte_depth: u8 = png.ihdr_data.bit_depth.as_u8() >> 3;
let bpp: usize = 3 * byte_depth as usize; 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 i % bpp == bpp - 1 {
if bpp == 3 { if bpp == 3 {
if cur_pixel.iter().unique().count() > 1 { if cur_pixel.iter().unique().count() > 1 {
return false; return None;
} }
reduced.push(cur_pixel[0]); reduced.push(cur_pixel[0]);
} else { } else {
@ -202,7 +210,7 @@ pub fn reduce_rgb_to_grayscale(png: &mut PngData) -> bool {
.unique() .unique()
.collect::<Vec<(u8, u8)>>(); .collect::<Vec<(u8, u8)>>();
if pixel_bytes.len() > 1 { if pixel_bytes.len() > 1 {
return false; return None;
} }
reduced.push(pixel_bytes[0].0); reduced.push(pixel_bytes[0].0);
reduced.push(pixel_bytes[0].1); 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); let transparency_pixel = if let Some(ref trns) = png.transparency_pixel {
if trns[0..2] != trns[2..4] || trns[2..4] != trns[4..6] { if trns.len() != 6 || trns[0..2] != trns[2..4] || trns[2..4] != trns[4..6] {
return false; 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") { if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
assert_eq!(sbit_header.len(), 3); aux_headers.insert(*b"bKGD", bkgd_header.get(0..2).map(|b| b.to_owned()));
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; Some(ReducedPng {
png.ihdr_data.color_type = ColorType::Grayscale; raw_data: reduced,
true 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 struct ReducedPng {
pub color_type: ColorType, pub color_type: ColorType,
pub raw_data: Vec<u8>, pub raw_data: Vec<u8>,
pub bit_depth: BitDepth,
/// replace if Some /// replace if Some
pub palette: Option<Vec<RGBA8>>, pub palette: Option<Vec<RGBA8>>,
/// replace if Some /// replace if Some
@ -22,6 +23,7 @@ pub struct ReducedPng {
/// Attempt to reduce the number of colors in the palette /// Attempt to reduce the number of colors in the palette
/// Returns `None` if palette hasn't changed /// Returns `None` if palette hasn't changed
#[must_use]
pub fn reduced_palette(png: &PngData) -> Option<ReducedPng> { pub fn reduced_palette(png: &PngData) -> Option<ReducedPng> {
if png.ihdr_data.color_type != ColorType::Indexed { if png.ihdr_data.color_type != ColorType::Indexed {
// Can't reduce if there is no palette // 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) do_palette_reduction(png, &palette_map)
} }
#[must_use]
fn do_palette_reduction(png: &PngData, palette_map: &[Option<u8>; 256]) -> Option<ReducedPng> { 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 byte_map = palette_map_to_byte_map(png, palette_map)?;
let mut raw_data = Vec::with_capacity(png.raw_data.len()); 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 { Some(ReducedPng {
color_type: ColorType::Indexed, color_type: ColorType::Indexed,
bit_depth: png.ihdr_data.bit_depth,
raw_data, raw_data,
transparency_pixel: None, transparency_pixel: None,
palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)), palette: Some(reordered_palette(png.palette.as_ref()?, palette_map)),