Add scale16 option to force 16-bit reduction

This commit is contained in:
Andrew 2023-05-22 09:25:01 +12:00 committed by Josh Holmer
parent c1222368b4
commit 88b930b5b1
6 changed files with 78 additions and 3 deletions

View file

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

View file

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

View file

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

View file

@ -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<PngImage> {
pub fn reduced_bit_depth_16_to_8(png: &PngImage, force_scale: bool) -> Option<PngImage> {
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<PngImage> {
})
}
/// 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<PngImage> {
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<PngImage> {

View file

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

View file

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