From 88b930b5b196f44ce103903acf12d2bbe1eda1ec Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 22 May 2023 09:25:01 +1200 Subject: [PATCH] Add scale16 option to force 16-bit reduction --- benches/reductions.rs | 10 +++++++++- src/lib.rs | 5 +++++ src/main.rs | 9 +++++++++ src/reduction/bit_depth.rs | 38 +++++++++++++++++++++++++++++++++++++- src/reduction/mod.rs | 2 +- tests/flags.rs | 17 +++++++++++++++++ 6 files changed, 78 insertions(+), 3 deletions(-) diff --git a/benches/reductions.rs b/benches/reductions.rs index 16aae41f..45c14bee 100644 --- a/benches/reductions.rs +++ b/benches/reductions.rs @@ -13,7 +13,15 @@ fn reductions_16_to_8_bits(b: &mut Bencher) { let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_8.png")); let png = PngData::new(&input, &Options::default()).unwrap(); - b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw)); + b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, false)); +} + +#[bench] +fn reductions_16_to_8_bits_scaled(b: &mut Bencher) { + let input = test::black_box(PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png")); + let png = PngData::new(&input, &Options::default()).unwrap(); + + b.iter(|| bit_depth::reduced_bit_depth_16_to_8(&png.raw, true)); } #[bench] diff --git a/src/lib.rs b/src/lib.rs index 82856624..bbb9919e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,6 +186,10 @@ pub struct Options { /// /// Default: `true` pub idat_recoding: bool, + /// Whether to forcibly reduce 16-bit to 8-bit by scaling + /// + /// Default: `false` + pub scale_16: bool, /// Which chunks to strip from the PNG file, if any /// /// Default: `None` @@ -303,6 +307,7 @@ impl Default for Options { palette_reduction: true, grayscale_reduction: true, idat_recoding: true, + scale_16: false, strip: StripChunks::None, deflate: Deflaters::Libdeflater { compression: 11 }, fast_evaluation: true, diff --git a/src/main.rs b/src/main.rs index a1032d7e..71c66fa3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -153,6 +153,11 @@ fn main() { .possible_value("1") .possible_value("keep"), ) + .arg( + Arg::new("scale16") + .help("Forcibly reduce 16-bit images to 8-bit") + .long("scale16"), + ) .arg( Arg::new("verbose") .help("Run in verbose mode (use multiple times to increase verbosity)") @@ -446,6 +451,10 @@ fn parse_opts_into_struct( opts.optimize_alpha = true; } + if matches.is_present("scale16") { + opts.scale_16 = true; + } + if matches.is_present("fast") { opts.fast_evaluation = true; } diff --git a/src/reduction/bit_depth.rs b/src/reduction/bit_depth.rs index d6dc68ae..7a4a1ca5 100644 --- a/src/reduction/bit_depth.rs +++ b/src/reduction/bit_depth.rs @@ -4,11 +4,15 @@ use crate::png::PngImage; /// Attempt to reduce a 16-bit image to 8-bit, returning the reduced image if successful #[must_use] -pub fn reduced_bit_depth_16_to_8(png: &PngImage) -> Option { +pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option { if png.ihdr.bit_depth != BitDepth::Sixteen { return None; } + if force_scale { + return scaled_bit_depth_16_to_8(png); + } + // Reduce from 16 to 8 bits per channel per pixel if png.data.chunks(2).any(|pair| pair[0] != pair[1]) { // Can't reduce @@ -25,6 +29,38 @@ pub fn reduced_bit_depth_16_to_8(png: &PngImage) -> Option { }) } +/// Forcibly reduce a 16-bit image to 8-bit by scaling, returning the reduced image if successful +#[must_use] +pub fn scaled_bit_depth_16_to_8(png: &PngImage) -> Option { + if png.ihdr.bit_depth != BitDepth::Sixteen { + return None; + } + + // Reduce from 16 to 8 bits per channel per pixel by scaling when necessary + let data = png + .data + .chunks(2) + .map(|pair| { + if pair[0] == pair[1] { + return pair[0]; + } + // See: http://www.libpng.org/pub/png/spec/1.2/PNG-Decoders.html#D.Sample-depth-rescaling + // This allows values such as 0x00FF to be rounded to 0x01 rather than truncated to 0x00 + let val = u16::from_be_bytes([pair[0], pair[1]]) as f64; + (val * 255.0 / 65535.0).round() as u8 + }) + .collect(); + + Some(PngImage { + data, + ihdr: IhdrData { + color_type: png.ihdr.color_type.clone(), + bit_depth: BitDepth::Eight, + ..png.ihdr + }, + }) +} + /// Attempt to reduce an 8/4/2-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 { diff --git a/src/reduction/mod.rs b/src/reduction/mod.rs index f5539096..5dbd6079 100644 --- a/src/reduction/mod.rs +++ b/src/reduction/mod.rs @@ -42,7 +42,7 @@ pub(crate) fn perform_reductions( // Attempt to reduce 16-bit to 8-bit // This is just removal of bytes and does not need to be evaluated if opts.bit_depth_reduction && !deadline.passed() { - if let Some(reduced) = reduced_bit_depth_16_to_8(&png) { + if let Some(reduced) = reduced_bit_depth_16_to_8(&png, opts.scale_16) { png = Arc::new(reduced); reduction_occurred = true; } diff --git a/tests/flags.rs b/tests/flags.rs index f3fb17d2..7d146a31 100644 --- a/tests/flags.rs +++ b/tests/flags.rs @@ -604,6 +604,23 @@ fn fix_errors() { remove_file(output).ok(); } +#[test] +fn scale_16() { + let input = PathBuf::from("tests/files/rgb_16_should_be_rgb_16.png"); + let (output, mut opts) = get_opts(&input); + opts.scale_16 = true; + + test_it_converts( + input, + &output, + &opts, + RGB, + BitDepth::Sixteen, + RGB, + BitDepth::Eight, + ); +} + #[test] #[cfg(feature = "zopfli")] fn zopfli_mode() {