Add scale16 option to force 16-bit reduction
This commit is contained in:
parent
c1222368b4
commit
88b930b5b1
6 changed files with 78 additions and 3 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue