Expand low depths to 8-bit (#516)
* Keep track of number of pixels in each scanline * Expand depth to 8-bit * Attempt expand bit depth * Simplify tracking of reduction_occurred * Fix test * Simplify depth handling in reductions * Add tests for disabled reductions
This commit is contained in:
parent
c71fd44454
commit
96122fa45a
9 changed files with 202 additions and 164 deletions
|
|
@ -31,7 +31,7 @@ fn reductions_8_to_4_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -41,7 +41,7 @@ fn reductions_8_to_2_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -51,7 +51,7 @@ fn reductions_8_to_1_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -61,7 +61,7 @@ fn reductions_4_to_2_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -71,7 +71,7 @@ fn reductions_4_to_1_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -81,7 +81,7 @@ fn reductions_2_to_1_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -91,7 +91,7 @@ fn reductions_grayscale_8_to_4_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -101,7 +101,7 @@ fn reductions_grayscale_8_to_2_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -111,7 +111,7 @@ fn reductions_grayscale_8_to_1_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -121,7 +121,7 @@ fn reductions_grayscale_4_to_2_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -131,7 +131,7 @@ fn reductions_grayscale_4_to_1_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -141,7 +141,7 @@ fn reductions_grayscale_2_to_1_bits(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, &Options::default()).unwrap();
|
||||
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw, 1));
|
||||
b.iter(|| bit_depth::reduced_bit_depth_8_or_less(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ pub struct Candidate {
|
|||
pub idat_data: Vec<u8>,
|
||||
pub filtered: Vec<u8>,
|
||||
pub filter: RowFilter,
|
||||
pub is_reduction: bool,
|
||||
// first wins tie-breaker
|
||||
nth: usize,
|
||||
}
|
||||
|
|
@ -95,11 +94,6 @@ impl Evaluator {
|
|||
self.eval_best_candidate.into_inner()
|
||||
}
|
||||
|
||||
/// Set baseline image. It will be used only to measure minimum compression level required
|
||||
pub fn set_baseline(&self, image: Arc<PngImage>) {
|
||||
self.try_image_inner(image, false)
|
||||
}
|
||||
|
||||
/// Set best size, if known in advance
|
||||
pub fn set_best_size(&self, size: usize) {
|
||||
self.best_candidate_size.set_min(size);
|
||||
|
|
@ -107,10 +101,6 @@ impl Evaluator {
|
|||
|
||||
/// Check if the image is smaller than others
|
||||
pub fn try_image(&self, image: Arc<PngImage>) {
|
||||
self.try_image_inner(image, true)
|
||||
}
|
||||
|
||||
fn try_image_inner(&self, image: Arc<PngImage>, is_reduction: bool) {
|
||||
let nth = self.nth.fetch_add(1, SeqCst);
|
||||
// These clones are only cheap refcounts
|
||||
let deadline = self.deadline.clone();
|
||||
|
|
@ -150,7 +140,6 @@ impl Evaluator {
|
|||
idat_data,
|
||||
filtered,
|
||||
filter,
|
||||
is_reduction,
|
||||
nth,
|
||||
};
|
||||
|
||||
|
|
|
|||
14
src/lib.rs
14
src/lib.rs
|
|
@ -615,7 +615,7 @@ fn optimize_png(
|
|||
|
||||
/// Perform optimization on the input image data using the options provided
|
||||
fn optimize_raw(
|
||||
mut png: Arc<PngImage>,
|
||||
image: Arc<PngImage>,
|
||||
opts: &Options,
|
||||
deadline: Arc<Deadline>,
|
||||
max_size: Option<usize>,
|
||||
|
|
@ -631,16 +631,14 @@ fn optimize_raw(
|
|||
eval_compression,
|
||||
false,
|
||||
);
|
||||
let (baseline, mut reduction_occurred) =
|
||||
perform_reductions(png.clone(), opts, &deadline, &eval);
|
||||
png = baseline;
|
||||
let mut png = perform_reductions(image.clone(), opts, &deadline, &eval);
|
||||
let mut eval_result = eval.get_best_candidate();
|
||||
if let Some(ref result) = eval_result {
|
||||
if result.is_reduction {
|
||||
png = result.image.clone();
|
||||
reduction_occurred = true;
|
||||
}
|
||||
png = result.image.clone();
|
||||
}
|
||||
let reduction_occurred = png.ihdr.color_type != image.ihdr.color_type
|
||||
|| png.ihdr.bit_depth != image.ihdr.bit_depth
|
||||
|| png.ihdr.interlaced != image.ihdr.interlaced;
|
||||
|
||||
if reduction_occurred {
|
||||
report_format("Reducing image to ", &png);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ impl<'a> Iterator for ScanLines<'a> {
|
|||
type Item = ScanLine<'a>;
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|(len, pass)| {
|
||||
self.iter.next().map(|(len, pass, num_pixels)| {
|
||||
let (data, rest) = self.raw_data.split_at(len);
|
||||
self.raw_data = rest;
|
||||
let (&filter, data) = if self.has_filter {
|
||||
|
|
@ -33,7 +33,12 @@ impl<'a> Iterator for ScanLines<'a> {
|
|||
} else {
|
||||
(&0, data)
|
||||
};
|
||||
ScanLine { filter, data, pass }
|
||||
ScanLine {
|
||||
filter,
|
||||
data,
|
||||
pass,
|
||||
num_pixels,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +73,7 @@ impl ScanLineRanges {
|
|||
}
|
||||
|
||||
impl Iterator for ScanLineRanges {
|
||||
type Item = (usize, Option<u8>);
|
||||
type Item = (usize, Option<u8>, usize);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.left == 0 {
|
||||
return None;
|
||||
|
|
@ -149,7 +154,7 @@ impl Iterator for ScanLineRanges {
|
|||
len += 1;
|
||||
}
|
||||
self.left = self.left.checked_sub(len)?;
|
||||
Some((len, current_pass))
|
||||
Some((len, current_pass, pixels_per_line as usize))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,4 +167,6 @@ pub struct ScanLine<'a> {
|
|||
pub data: &'a [u8],
|
||||
/// The current pass if the image is interlaced
|
||||
pub pass: Option<u8>,
|
||||
/// The number of pixels in the current scan line
|
||||
pub num_pixels: usize,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,60 +61,48 @@ pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option<PngImage> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Attempt to reduce an 8/4/2-bit image to a lower bit depth, returning the reduced image if successful
|
||||
/// Attempt to reduce an 8-bit image to a lower bit depth, returning the reduced image if successful
|
||||
#[must_use]
|
||||
pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> Option<PngImage> {
|
||||
assert!((1..8).contains(&minimum_bits));
|
||||
let bit_depth = png.ihdr.bit_depth as usize;
|
||||
if minimum_bits >= bit_depth || bit_depth > 8 || png.channels_per_pixel() != 1 {
|
||||
pub fn reduced_bit_depth_8_or_less(png: &PngImage) -> Option<PngImage> {
|
||||
if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() != 1 {
|
||||
return None;
|
||||
}
|
||||
// Calculate the current number of pixels per byte
|
||||
let ppb = 8 / bit_depth;
|
||||
|
||||
let mut minimum_bits = 1;
|
||||
|
||||
if let ColorType::Indexed { palette } = &png.ihdr.color_type {
|
||||
// We can easily determine minimum depth by the palette size
|
||||
let required_bits = match palette.len() {
|
||||
minimum_bits = match palette.len() {
|
||||
0..=2 => 1,
|
||||
3..=4 => 2,
|
||||
5..=16 => 4,
|
||||
_ => 8,
|
||||
_ => return None,
|
||||
};
|
||||
if required_bits >= bit_depth {
|
||||
// Not reducable
|
||||
return None;
|
||||
} else if required_bits > minimum_bits {
|
||||
minimum_bits = required_bits;
|
||||
}
|
||||
} else {
|
||||
// Finding minimum depth for grayscale is much more complicated
|
||||
let mut mask = (1 << minimum_bits) - 1;
|
||||
let mut divisions = 1..(bit_depth / minimum_bits);
|
||||
let mut mask = 1;
|
||||
let mut divisions = 1..8;
|
||||
for &b in &png.data {
|
||||
if b == 0 || b == 255 {
|
||||
continue;
|
||||
}
|
||||
'try_depth: loop {
|
||||
let mut byte = b;
|
||||
// Loop over each pixel in the byte
|
||||
for _ in 0..ppb {
|
||||
// Align the first pixel division with the mask
|
||||
// Align the first pixel division with the mask
|
||||
let mut byte = b.rotate_left(minimum_bits as u32);
|
||||
// Each potential division of this pixel must be identical to successfully reduce
|
||||
let compare = byte & mask;
|
||||
for _ in divisions.clone() {
|
||||
// Align the next division with the mask
|
||||
byte = byte.rotate_left(minimum_bits as u32);
|
||||
// Each potential division of this pixel must be identical to successfully reduce
|
||||
let compare = byte & mask;
|
||||
for _ in divisions.clone() {
|
||||
// Align the next division with the mask
|
||||
byte = byte.rotate_left(minimum_bits as u32);
|
||||
if byte & mask != compare {
|
||||
// This depth is not possible, try the next one up
|
||||
minimum_bits <<= 1;
|
||||
if minimum_bits == bit_depth {
|
||||
return None;
|
||||
}
|
||||
mask = (1 << minimum_bits) - 1;
|
||||
divisions = 1..(bit_depth / minimum_bits);
|
||||
continue 'try_depth;
|
||||
if byte & mask != compare {
|
||||
// This depth is not possible, try the next one up
|
||||
minimum_bits <<= 1;
|
||||
if minimum_bits == 8 {
|
||||
return None;
|
||||
}
|
||||
mask = (1 << minimum_bits) - 1;
|
||||
divisions = 1..(8 / minimum_bits);
|
||||
continue 'try_depth;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -126,18 +114,13 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O
|
|||
let mask = (1 << minimum_bits) - 1;
|
||||
for line in png.scan_lines(false) {
|
||||
// Loop over the data in chunks that will produce 1 byte of output
|
||||
for chunk in line.data.chunks(bit_depth / minimum_bits) {
|
||||
for chunk in line.data.chunks(8 / minimum_bits) {
|
||||
let mut new_byte = 0;
|
||||
let mut shift = 8;
|
||||
for &(mut byte) in chunk {
|
||||
// Loop over each pixel in the byte
|
||||
for _ in 0..ppb {
|
||||
// Align the current pixel with the mask
|
||||
byte = byte.rotate_left(bit_depth as u32);
|
||||
shift -= minimum_bits;
|
||||
// Take the low bits of the pixel and shift them into the output byte
|
||||
new_byte |= (byte & mask) << shift;
|
||||
}
|
||||
for byte in chunk {
|
||||
shift -= minimum_bits;
|
||||
// Take the low bits of the pixel and shift them into the output byte
|
||||
new_byte |= (byte & mask) << shift;
|
||||
}
|
||||
reduced.push(new_byte);
|
||||
}
|
||||
|
|
@ -148,11 +131,11 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O
|
|||
transparent_shade: Some(trans),
|
||||
} = png.ihdr.color_type
|
||||
{
|
||||
let reduced_trans = (trans & 0xFF) >> (bit_depth - minimum_bits);
|
||||
let reduced_trans = (trans & 0xFF) >> (8 - minimum_bits);
|
||||
// Verify the reduction is valid by restoring back to original bit depth
|
||||
let mut check = reduced_trans;
|
||||
let mut bits = minimum_bits;
|
||||
while bits < bit_depth {
|
||||
while bits < 8 {
|
||||
check = check << bits | check;
|
||||
bits <<= 1;
|
||||
}
|
||||
|
|
@ -177,3 +160,67 @@ pub fn reduced_bit_depth_8_or_less(png: &PngImage, mut minimum_bits: usize) -> O
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Expand a 1/2/4-bit image to 8-bit, returning the expanded image if successful
|
||||
#[must_use]
|
||||
pub fn expanded_bit_depth_to_8(png: &PngImage) -> Option<PngImage> {
|
||||
let bit_depth = png.ihdr.bit_depth as u32;
|
||||
if bit_depth >= 8 {
|
||||
return None;
|
||||
}
|
||||
// Calculate the current number of pixels per byte
|
||||
let ppb = 8 / bit_depth;
|
||||
let is_gray = matches!(png.ihdr.color_type, ColorType::Grayscale { .. });
|
||||
|
||||
let mut reduced = Vec::with_capacity((png.ihdr.width * png.ihdr.height) as usize);
|
||||
let mut length = 0;
|
||||
let mask = (1 << bit_depth) - 1;
|
||||
for line in png.scan_lines(false) {
|
||||
for &(mut byte) in line.data {
|
||||
// Loop over each pixel in the byte
|
||||
for _ in 0..ppb {
|
||||
// Align the current pixel with the mask
|
||||
byte = byte.rotate_left(bit_depth);
|
||||
let mut val = byte & mask;
|
||||
if is_gray {
|
||||
// Expand gray by repeating the bits
|
||||
let mut bits = bit_depth;
|
||||
while bits < 8 {
|
||||
val = val << bits | val;
|
||||
bits <<= 1;
|
||||
}
|
||||
}
|
||||
reduced.push(val);
|
||||
}
|
||||
}
|
||||
// Trim any overflow
|
||||
length += line.num_pixels;
|
||||
reduced.truncate(length);
|
||||
}
|
||||
|
||||
// If the image is grayscale we also need to expand the transparency pixel
|
||||
let color_type = if let ColorType::Grayscale {
|
||||
transparent_shade: Some(mut trans),
|
||||
} = png.ihdr.color_type
|
||||
{
|
||||
let mut bits = bit_depth;
|
||||
while bits < 8 {
|
||||
trans = trans << bits | trans;
|
||||
bits <<= 1;
|
||||
}
|
||||
ColorType::Grayscale {
|
||||
transparent_shade: Some(trans),
|
||||
}
|
||||
} else {
|
||||
png.ihdr.color_type.clone()
|
||||
};
|
||||
|
||||
Some(PngImage {
|
||||
data: reduced,
|
||||
ihdr: IhdrData {
|
||||
color_type,
|
||||
bit_depth: BitDepth::Eight,
|
||||
..png.ihdr
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,15 +18,13 @@ pub(crate) fn perform_reductions(
|
|||
opts: &Options,
|
||||
deadline: &Deadline,
|
||||
eval: &Evaluator,
|
||||
) -> (Arc<PngImage>, bool) {
|
||||
let mut reduction_occurred = false;
|
||||
) -> Arc<PngImage> {
|
||||
let mut evaluation_added = false;
|
||||
|
||||
// Interlacing must be processed first in order to evaluate the rest correctly
|
||||
if let Some(interlacing) = opts.interlace {
|
||||
if let Some(reduced) = png.change_interlacing(interlacing) {
|
||||
png = Arc::new(reduced);
|
||||
reduction_occurred = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,7 +33,6 @@ pub(crate) fn perform_reductions(
|
|||
if opts.optimize_alpha && !deadline.passed() {
|
||||
if let Some(reduced) = cleaned_alpha_channel(&png) {
|
||||
png = Arc::new(reduced);
|
||||
// This does not count as a reduction
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +41,6 @@ pub(crate) fn perform_reductions(
|
|||
if opts.bit_depth_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) {
|
||||
png = Arc::new(reduced);
|
||||
reduction_occurred = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +49,14 @@ pub(crate) fn perform_reductions(
|
|||
if opts.color_type_reduction && opts.grayscale_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduced_rgb_to_grayscale(&png) {
|
||||
png = Arc::new(reduced);
|
||||
reduction_occurred = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to expand the bit depth to 8
|
||||
// This does need to be evaluated but will be done so later when it gets reduced again
|
||||
if opts.bit_depth_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = expanded_bit_depth_to_8(&png) {
|
||||
png = Arc::new(reduced);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +65,6 @@ pub(crate) fn perform_reductions(
|
|||
if opts.palette_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) {
|
||||
png = Arc::new(reduced);
|
||||
reduction_occurred = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +83,6 @@ pub(crate) fn perform_reductions(
|
|||
evaluation_added = true;
|
||||
} else {
|
||||
baseline = png.clone();
|
||||
reduction_occurred = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +119,6 @@ pub(crate) fn perform_reductions(
|
|||
evaluation_added = true;
|
||||
} else {
|
||||
baseline = new.clone();
|
||||
reduction_occurred = true;
|
||||
}
|
||||
indexed = Some(new);
|
||||
}
|
||||
|
|
@ -128,8 +128,8 @@ pub(crate) fn perform_reductions(
|
|||
if opts.bit_depth_reduction && !deadline.passed() {
|
||||
// Try reducing the previous png, falling back to the indexed one if it exists
|
||||
// This allows a grayscale depth reduction to be preferred over an indexed depth reduction
|
||||
let reduced = reduced_bit_depth_8_or_less(&png, 1)
|
||||
.or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png, 1)));
|
||||
let reduced = reduced_bit_depth_8_or_less(&png)
|
||||
.or_else(|| indexed.and_then(|png| reduced_bit_depth_8_or_less(&png)));
|
||||
if let Some(reduced) = reduced {
|
||||
eval.try_image(Arc::new(reduced));
|
||||
evaluation_added = true;
|
||||
|
|
@ -137,7 +137,7 @@ pub(crate) fn perform_reductions(
|
|||
}
|
||||
|
||||
if evaluation_added {
|
||||
eval.set_baseline(baseline.clone());
|
||||
eval.try_image(baseline.clone());
|
||||
}
|
||||
(baseline, reduction_occurred)
|
||||
baseline
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,16 +7,22 @@ use rgb::RGBA8;
|
|||
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
|
||||
#[must_use]
|
||||
pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
|
||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
||||
return None;
|
||||
}
|
||||
let palette = match &png.ihdr.color_type {
|
||||
ColorType::Indexed { palette } if palette.len() > 1 => palette,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let used = get_used_entries(png);
|
||||
let mut used = [false; 256];
|
||||
for &byte in &png.data {
|
||||
used[byte as usize] = true;
|
||||
}
|
||||
|
||||
let black = RGBA8::new(0, 0, 0, 255);
|
||||
let mut condensed = IndexSet::with_capacity(palette.len());
|
||||
let mut palette_map = [0; 256];
|
||||
let mut byte_map = [0; 256];
|
||||
let mut did_change = false;
|
||||
for (i, used) in used.iter().enumerate() {
|
||||
if !used {
|
||||
|
|
@ -24,15 +30,14 @@ pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage>
|
|||
}
|
||||
// There are invalid files that use pixel indices beyond palette size
|
||||
let color = *palette.get(i).unwrap_or(&black);
|
||||
palette_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha);
|
||||
if palette_map[i] as usize != i {
|
||||
byte_map[i] = add_color_to_set(color, &mut condensed, optimize_alpha);
|
||||
if byte_map[i] as usize != i {
|
||||
did_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
let data = if did_change {
|
||||
// Reassign data bytes to new indices
|
||||
let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &palette_map);
|
||||
png.data.iter().map(|b| byte_map[*b as usize]).collect()
|
||||
} else if condensed.len() < palette.len() {
|
||||
// Data is unchanged but palette will be truncated
|
||||
|
|
@ -64,68 +69,10 @@ fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet<RGBA8>, optimize_alpha:
|
|||
idx as u8
|
||||
}
|
||||
|
||||
fn get_used_entries(png: &PngImage) -> [bool; 256] {
|
||||
let mut used = [false; 256];
|
||||
match png.ihdr.bit_depth {
|
||||
BitDepth::Eight => {
|
||||
for &byte in &png.data {
|
||||
used[byte as usize] = true;
|
||||
}
|
||||
}
|
||||
BitDepth::Four => {
|
||||
for &byte in &png.data {
|
||||
used[(byte & 0x0F) as usize] = true;
|
||||
used[(byte >> 4) as usize] = true;
|
||||
}
|
||||
}
|
||||
BitDepth::Two => {
|
||||
for &byte in &png.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;
|
||||
}
|
||||
}
|
||||
BitDepth::One => {
|
||||
// Only two options, don't bother checking which are actually used
|
||||
used[0] = true;
|
||||
used[1] = true;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
used
|
||||
}
|
||||
|
||||
fn palette_map_to_byte_map(bit_depth: BitDepth, palette_map: &[u8; 256]) -> [u8; 256] {
|
||||
// Low bit-depths can be pre-computed for every byte value
|
||||
match bit_depth {
|
||||
BitDepth::Eight => *palette_map,
|
||||
BitDepth::Four => {
|
||||
let mut byte_map = [0_u8; 256];
|
||||
for byte in 0..256 {
|
||||
byte_map[byte] = palette_map[byte & 0x0F] | (palette_map[byte >> 4] << 4);
|
||||
}
|
||||
byte_map
|
||||
}
|
||||
BitDepth::Two => {
|
||||
let mut byte_map = [0_u8; 256];
|
||||
for byte in 0..256 {
|
||||
byte_map[byte] = palette_map[byte & 0x03]
|
||||
| (palette_map[(byte >> 2) & 0x03] << 2)
|
||||
| (palette_map[(byte >> 4) & 0x03] << 4)
|
||||
| (palette_map[byte >> 6] << 6);
|
||||
}
|
||||
byte_map
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to sort the colors in the palette, returning the sorted image if successful
|
||||
#[must_use]
|
||||
pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
|
||||
if png.ihdr.bit_depth == BitDepth::One {
|
||||
// Don't bother trying to sort a 1-bit image
|
||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
||||
return None;
|
||||
}
|
||||
let palette = match &png.ihdr.color_type {
|
||||
|
|
@ -154,12 +101,11 @@ pub fn sorted_palette(png: &PngImage) -> Option<PngImage> {
|
|||
return None;
|
||||
}
|
||||
|
||||
// Construct the palette and byte maps and convert the data
|
||||
let mut new_map = [0; 256];
|
||||
// Construct the new mapping and convert the data
|
||||
let mut byte_map = [0; 256];
|
||||
for (i, &v) in old_map.iter().enumerate() {
|
||||
new_map[v] = i as u8;
|
||||
byte_map[v] = i as u8;
|
||||
}
|
||||
let byte_map = palette_map_to_byte_map(png.ihdr.bit_depth, &new_map);
|
||||
let data = png.data.iter().map(|&b| byte_map[b as usize]).collect();
|
||||
|
||||
Some(PngImage {
|
||||
|
|
|
|||
|
|
@ -604,6 +604,57 @@ fn fix_errors() {
|
|||
remove_file(output).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_color_type_change() {
|
||||
let input = PathBuf::from("tests/files/palette_8_should_be_rgb.png");
|
||||
let (output, mut opts) = get_opts(&input);
|
||||
opts.color_type_reduction = false;
|
||||
|
||||
test_it_converts(
|
||||
input,
|
||||
&output,
|
||||
&opts,
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
INDEXED,
|
||||
BitDepth::One,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_grayscale_change() {
|
||||
let input = PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png");
|
||||
let (output, mut opts) = get_opts(&input);
|
||||
opts.grayscale_reduction = false;
|
||||
|
||||
test_it_converts(
|
||||
input,
|
||||
&output,
|
||||
&opts,
|
||||
RGB,
|
||||
BitDepth::Eight,
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_bit_depth_change() {
|
||||
let input = PathBuf::from("tests/files/palette_4_should_be_palette_2.png");
|
||||
let (output, mut opts) = get_opts(&input);
|
||||
opts.bit_depth_reduction = false;
|
||||
|
||||
test_it_converts(
|
||||
input,
|
||||
&output,
|
||||
&opts,
|
||||
INDEXED,
|
||||
BitDepth::Four,
|
||||
INDEXED,
|
||||
BitDepth::Four,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_16() {
|
||||
let input = PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png");
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ fn issue_82() {
|
|||
INDEXED,
|
||||
BitDepth::Four,
|
||||
INDEXED,
|
||||
BitDepth::Four,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue