For RGB(A) images that contain gray colors, this reduction can achieve significant space savings. However, in the absence of gamma correction data, some PNG decoders assume more exotic color spaces for grayscale images instead of the ubiquitous sRGB. This results in gamma miscorrection, and for the end user this means that colors will look wrong, like "washed-out". Java's ImageIO class, which is popular in the JVM world to read PNG files, uses rather unconventional defaults, as explained in this StackOverflow question: https://stackoverflow.com/questions/31312645/java-imageio-grayscale-png-issue Gamma miscorrection problems aside, OxiPNG currently tries hard to reduce RGB(A) images to grayscale, because it expects that reduction to be quite effective. However, in some cases, OxiPNG generates smaller PNG files when reducing grasycale RGB(A) images to paletted color than actual grayscale color. For example, let's say that "~/gray.png" is a 256x256 RGBA image entirely filled with (119, 119, 119, 255) pixels. OxiPNG, by default, reduces this image to grayscale and achieves a 68.23% decrease: $ cargo build --release && target/release/oxipng -omax --out ~/out.png ~/gray.png Processing: /home/user/gray.png 256x256 pixels, PNG format 4x8 bits/pixel, RGBA IDAT size = 604 bytes File size = 661 bytes Reducing image to 1x4 bits/pixel, Grayscale Trying: 144 combinations Found better combination: zc = 6 zs = 0 f = 0 153 bytes IDAT size = 153 bytes (451 bytes decrease) file size = 210 bytes (451 bytes = 68.23% decrease) Output: /home/user/out.png However, if the --ng option that this commit adds is used to skip the grayscale reduction step, OxiPNG reduces to a single color palette instead, which is much more efficient, achieving a 84.42% decrease: $ cargo build --release && target/release/oxipng -omax --ng --out ~/out.png ~/gray.png Processing: /home/alejandro/gray.png 256x256 pixels, PNG format 4x8 bits/pixel, RGBA IDAT size = 604 bytes File size = 661 bytes Reducing image to 1 bits/pixel, 1 colors in palette Trying: 144 combinations Found better combination: zc = 3 zs = 3 f = 0 31 bytes IDAT size = 31 bytes (573 bytes decrease) file size = 103 bytes (558 bytes = 84.42% decrease) Output: /home/alejandro/out.png While OxiPNG should arguably be made smarter to better handle these cases, in the meantime, adding an option to manually skip that grayscale reduction can't hurt. In fact, it may even help users achieving the most out of current versions of OxiPNG, and developers reasoning about what makes a grayscale-like RGB(A) image compress better with a color palette. Due to the reasons stated above, this adds a simple "grayscale_reduction" option to the Options struct, and a "no-grayscale-reduction" command line switch, that makes OxiPNG not try this problematic grayscale reduction on RGB(A) images.
244 lines
7.1 KiB
Rust
244 lines
7.1 KiB
Rust
#![feature(test)]
|
|
|
|
extern crate oxipng;
|
|
extern crate test;
|
|
|
|
use oxipng::internal_tests::*;
|
|
use std::path::PathBuf;
|
|
use test::Bencher;
|
|
|
|
#[bench]
|
|
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, false).unwrap();
|
|
|
|
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_8_to_4_bits(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_8_should_be_palette_4.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_8_to_2_bits(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_8_should_be_palette_2.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_8_to_1_bits(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_8_should_be_palette_1.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_4_to_2_bits(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_4_should_be_palette_2.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_4_to_1_bits(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_4_should_be_palette_1.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_2_to_1_bits(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_2_should_be_palette_1.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| bit_depth::reduce_bit_depth(&png.raw, 1));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgba_to_rgb_16(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_16_should_be_rgb_16.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgba_to_rgb_8(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_rgb_8.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/rgba_16_should_be_grayscale_alpha_16.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/rgba_8_should_be_grayscale_alpha_8.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/rgba_16_should_be_grayscale_16.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/rgba_8_should_be_grayscale_8.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/rgb_16_should_be_grayscale_16.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgb_to_grayscale_8(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_grayscale_8.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgba_to_palette_8(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_should_be_palette_8.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_rgb_to_palette_8(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgb_8_should_be_palette_8.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduce_color_type(&png.raw, true));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_palette_duplicate_reduction(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_should_be_reduced_with_dupes.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduced_palette(&png.raw));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_palette_unused_reduction(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_should_be_reduced_with_unused.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduced_palette(&png.raw));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_palette_full_reduction(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from(
|
|
"tests/files/palette_should_be_reduced_with_both.png",
|
|
));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| reduced_palette(&png.raw));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_alpha_black(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_black.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| alpha::filtered_alpha_channel(&png.raw, AlphaOptim::Black));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_alpha_white(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_white.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| alpha::filtered_alpha_channel(&png.raw, AlphaOptim::White));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_alpha_left(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_left.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| alpha::filtered_alpha_channel(&png.raw, AlphaOptim::Left));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_alpha_right(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_right.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| alpha::filtered_alpha_channel(&png.raw, AlphaOptim::Right));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_alpha_up(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_up.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| alpha::filtered_alpha_channel(&png.raw, AlphaOptim::Up));
|
|
}
|
|
|
|
#[bench]
|
|
fn reductions_alpha_down(b: &mut Bencher) {
|
|
let input = test::black_box(PathBuf::from("tests/files/rgba_8_reduce_alpha_down.png"));
|
|
let png = PngData::new(&input, false).unwrap();
|
|
|
|
b.iter(|| alpha::filtered_alpha_channel(&png.raw, AlphaOptim::Down));
|
|
}
|