Immutable color reductions
This commit is contained in:
parent
52a22e9e03
commit
e8beb76192
6 changed files with 94 additions and 58 deletions
11
src/lib.rs
11
src/lib.rs
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
Loading…
Reference in a new issue