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:
andrews05 2023-06-18 17:32:47 +12:00 committed by GitHub
parent c71fd44454
commit 96122fa45a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 164 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -262,7 +262,7 @@ fn issue_82() {
INDEXED,
BitDepth::Four,
INDEXED,
BitDepth::Four,
BitDepth::Eight,
);
}