Further small reduction improvements (#504)
This commit is contained in:
parent
9a500941d8
commit
d8b7ebaf47
14 changed files with 533 additions and 342 deletions
|
|
@ -158,7 +158,7 @@ fn reductions_rgba_to_grayscale_alpha_16(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
|
||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -168,7 +168,7 @@ fn reductions_rgba_to_grayscale_alpha_8(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
|
||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -179,7 +179,7 @@ fn reductions_rgba_to_grayscale_16(b: &mut Bencher) {
|
|||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| {
|
||||
color::reduce_rgb_to_grayscale(&png.raw)
|
||||
color::reduced_rgb_to_grayscale(&png.raw)
|
||||
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
|
||||
});
|
||||
}
|
||||
|
|
@ -192,7 +192,7 @@ fn reductions_rgba_to_grayscale_8(b: &mut Bencher) {
|
|||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| {
|
||||
color::reduce_rgb_to_grayscale(&png.raw)
|
||||
color::reduced_rgb_to_grayscale(&png.raw)
|
||||
.and_then(|r| alpha::reduced_alpha_channel(&r, false))
|
||||
});
|
||||
}
|
||||
|
|
@ -204,7 +204,7 @@ fn reductions_rgb_to_grayscale_16(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| color::reduce_rgb_to_grayscale(&png.raw));
|
||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -212,7 +212,7 @@ 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(|| color::reduce_rgb_to_grayscale(&png.raw));
|
||||
b.iter(|| color::reduced_rgb_to_grayscale(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -220,7 +220,7 @@ 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(|| color::reduce_to_palette(&png.raw));
|
||||
b.iter(|| color::reduced_to_indexed(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -228,7 +228,27 @@ 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(|| color::reduce_to_palette(&png.raw));
|
||||
b.iter(|| color::reduced_to_indexed(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn reductions_grayscale_8_to_palette_8(b: &mut Bencher) {
|
||||
let input = test::black_box(PathBuf::from(
|
||||
"tests/files/grayscale_8_should_be_palette_8.png",
|
||||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| color::reduced_to_indexed(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn reductions_palette_8_to_grayscale_8(b: &mut Bencher) {
|
||||
let input = test::black_box(PathBuf::from(
|
||||
"tests/files/palette_8_should_be_grayscale_8.png",
|
||||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| color::indexed_to_channels(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -238,7 +258,7 @@ fn reductions_palette_duplicate_reduction(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| palette::optimized_palette(&png.raw, false));
|
||||
b.iter(|| palette::reduced_palette(&png.raw, false));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -248,7 +268,7 @@ fn reductions_palette_unused_reduction(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| palette::optimized_palette(&png.raw, false));
|
||||
b.iter(|| palette::reduced_palette(&png.raw, false));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
@ -258,7 +278,17 @@ fn reductions_palette_full_reduction(b: &mut Bencher) {
|
|||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| palette::optimized_palette(&png.raw, false));
|
||||
b.iter(|| palette::reduced_palette(&png.raw, false));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn reductions_palette_sort(b: &mut Bencher) {
|
||||
let input = test::black_box(PathBuf::from(
|
||||
"tests/files/palette_8_should_be_palette_8.png",
|
||||
));
|
||||
let png = PngData::new(&input, false).unwrap();
|
||||
|
||||
b.iter(|| palette::sorted_palette(&png.raw));
|
||||
}
|
||||
|
||||
#[bench]
|
||||
|
|
|
|||
|
|
@ -70,13 +70,21 @@ impl ColorType {
|
|||
matches!(self, ColorType::RGB { .. } | ColorType::RGBA)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_grayscale(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ColorType::Grayscale { .. } | ColorType::GrayscaleAlpha
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn has_alpha(&self) -> bool {
|
||||
matches!(self, ColorType::GrayscaleAlpha | ColorType::RGBA)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn has_trns(&self) -> bool {
|
||||
pub(crate) fn has_trns(&self) -> bool {
|
||||
match self {
|
||||
ColorType::Grayscale { transparent_shade } => transparent_shade.is_some(),
|
||||
ColorType::RGB { transparent_color } => transparent_color.is_some(),
|
||||
|
|
|
|||
51
src/lib.rs
51
src/lib.rs
|
|
@ -564,7 +564,12 @@ fn optimize_png(
|
|||
// Do this first so that reductions can ignore certain chunks such as bKGD
|
||||
perform_strip(png, opts);
|
||||
|
||||
if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, Some(idat_original_size)) {
|
||||
let max_size = if opts.force {
|
||||
None
|
||||
} else {
|
||||
Some(png.estimated_output_size())
|
||||
};
|
||||
if let Some(new_png) = optimize_raw(png.raw.clone(), opts, deadline, max_size) {
|
||||
png.raw = new_png.raw;
|
||||
png.idat_data = new_png.idat_data;
|
||||
}
|
||||
|
|
@ -611,7 +616,7 @@ fn optimize_raw(
|
|||
mut png: Arc<PngImage>,
|
||||
opts: &Options,
|
||||
deadline: Arc<Deadline>,
|
||||
max_idat_size: Option<usize>,
|
||||
max_size: Option<usize>,
|
||||
) -> Option<PngData> {
|
||||
// Must use normal (lazy) compression, as faster ones (greedy) are not representative
|
||||
let eval_compression = 5;
|
||||
|
|
@ -673,15 +678,10 @@ fn optimize_raw(
|
|||
};
|
||||
if trial.compression > 0 && trial.compression <= eval_compression {
|
||||
// No further compression required
|
||||
let idat_data = eval_result.image.idat_data;
|
||||
if opts.force || idat_data.len() < max_idat_size.unwrap_or(usize::MAX) {
|
||||
Some((trial, idat_data))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some((trial, eval_result.image.idat_data))
|
||||
} else {
|
||||
debug!("Trying: {}", trial.filter);
|
||||
let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size });
|
||||
let best_size = AtomicMin::new(max_size);
|
||||
perform_trial(&eval_result.image.filtered, opts, trial, &best_size)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -712,7 +712,7 @@ fn optimize_raw(
|
|||
|
||||
debug!("Trying: {} filters", results.len());
|
||||
|
||||
let best_size = AtomicMin::new(if opts.force { None } else { max_idat_size });
|
||||
let best_size = AtomicMin::new(max_size);
|
||||
let results_iter = results.into_par_iter().with_max_len(1);
|
||||
let best = results_iter.filter_map(|trial| {
|
||||
if deadline.passed() {
|
||||
|
|
@ -730,34 +730,37 @@ fn optimize_raw(
|
|||
})
|
||||
};
|
||||
|
||||
if let Some((opts, idat_data)) = best {
|
||||
debug!("Found better combination:");
|
||||
debug!(
|
||||
" zc = {} f = {:8} {} bytes",
|
||||
opts.compression,
|
||||
opts.filter,
|
||||
idat_data.len()
|
||||
);
|
||||
return Some(PngData {
|
||||
if let Some((trial, idat_data)) = best {
|
||||
let image = PngData {
|
||||
raw: png,
|
||||
// The filtered data has not been retained here, but we don't need to return it
|
||||
filtered: vec![],
|
||||
idat_data,
|
||||
});
|
||||
};
|
||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
||||
debug!("Found better combination:");
|
||||
debug!(
|
||||
" zc = {} f = {:8} {} bytes",
|
||||
trial.compression,
|
||||
trial.filter,
|
||||
image.idat_data.len()
|
||||
);
|
||||
return Some(image);
|
||||
}
|
||||
}
|
||||
} else if let Some(result) = eval_result {
|
||||
// If idat_recoding is off and reductions were attempted but ended up choosing the baseline,
|
||||
// we should still check if the evaluator compressed the baseline smaller than the original.
|
||||
let idat_data = &result.image.idat_data;
|
||||
if idat_data.len() < max_idat_size.unwrap_or(usize::MAX) {
|
||||
let image = result.image;
|
||||
if image.estimated_output_size() < max_size.unwrap_or(usize::MAX) {
|
||||
debug!("Found better combination:");
|
||||
debug!(
|
||||
" zc = {} f = {:8} {} bytes",
|
||||
eval_compression,
|
||||
result.filter,
|
||||
idat_data.len()
|
||||
image.idat_data.len()
|
||||
);
|
||||
return Some(result.image);
|
||||
return Some(image);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,28 +186,12 @@ impl PngData {
|
|||
match &self.raw.ihdr.color_type {
|
||||
ColorType::Indexed { palette } => {
|
||||
let mut palette_data = Vec::with_capacity(palette.len() * 3);
|
||||
let mut max_palette_size = 1 << (self.raw.ihdr.bit_depth as u8);
|
||||
// Ensure bKGD color doesn't get truncated from palette
|
||||
if let Some(&idx) = self.raw.aux_headers.get(b"bKGD").and_then(|b| b.first()) {
|
||||
max_palette_size = max_palette_size.max(idx as usize + 1);
|
||||
}
|
||||
for px in palette.iter().take(max_palette_size) {
|
||||
for px in palette {
|
||||
palette_data.extend_from_slice(px.rgb().as_slice());
|
||||
}
|
||||
write_png_block(b"PLTE", &palette_data, &mut output);
|
||||
let num_transparent = palette.iter().take(max_palette_size).enumerate().fold(
|
||||
0,
|
||||
|prev, (index, px)| {
|
||||
if px.a == 255 {
|
||||
prev
|
||||
} else {
|
||||
index + 1
|
||||
}
|
||||
},
|
||||
);
|
||||
if num_transparent > 0 {
|
||||
let trns_data: Vec<_> =
|
||||
palette[0..num_transparent].iter().map(|px| px.a).collect();
|
||||
if let Some(last_trns) = palette.iter().rposition(|px| px.a != 255) {
|
||||
let trns_data: Vec<_> = palette[0..=last_trns].iter().map(|px| px.a).collect();
|
||||
write_png_block(b"tRNS", &trns_data, &mut output);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,103 +1,88 @@
|
|||
use crate::colors::{BitDepth, ColorType};
|
||||
use crate::headers::IhdrData;
|
||||
use crate::png::PngImage;
|
||||
use indexmap::IndexMap;
|
||||
use rgb::{ComponentMap, FromSlice, RGBA, RGBA8};
|
||||
use indexmap::IndexSet;
|
||||
use rgb::alt::Gray;
|
||||
use rgb::{ComponentMap, ComponentSlice, FromSlice, RGB, RGBA, RGBA8};
|
||||
use rustc_hash::FxHasher;
|
||||
use std::hash::{BuildHasherDefault, Hash};
|
||||
|
||||
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
type FxIndexSet<V> = IndexSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
fn reduce_scanline_to_palette<T>(
|
||||
/// Maximum size difference between indexed and channels to consider a candidate for evaluation
|
||||
pub const INDEXED_MAX_DIFF: usize = 20000;
|
||||
|
||||
fn build_palette<T>(
|
||||
iter: impl IntoIterator<Item = T>,
|
||||
palette: &mut FxIndexMap<T, u8>,
|
||||
reduced: &mut Vec<u8>,
|
||||
) -> bool
|
||||
) -> Option<FxIndexSet<T>>
|
||||
where
|
||||
T: Eq + Hash,
|
||||
{
|
||||
let mut palette = FxIndexSet::default();
|
||||
palette.reserve(257);
|
||||
for pixel in iter {
|
||||
let idx = if let Some(&idx) = palette.get(&pixel) {
|
||||
idx
|
||||
} else {
|
||||
let len = palette.len();
|
||||
if len == 256 {
|
||||
return false;
|
||||
}
|
||||
let idx = len as u8;
|
||||
palette.insert(pixel, idx);
|
||||
idx
|
||||
};
|
||||
reduced.push(idx);
|
||||
let (idx, _) = palette.insert_full(pixel);
|
||||
if idx == 256 {
|
||||
return None;
|
||||
}
|
||||
reduced.push(idx as u8);
|
||||
}
|
||||
true
|
||||
Some(palette)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
|
||||
if png.ihdr.bit_depth != BitDepth::Eight || png.channels_per_pixel() == 1 {
|
||||
pub fn reduced_to_indexed(png: &PngImage) -> Option<PngImage> {
|
||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
||||
return None;
|
||||
}
|
||||
let mut raw_data = Vec::with_capacity(png.data.len());
|
||||
let mut palette = FxIndexMap::default();
|
||||
palette.reserve(257);
|
||||
let ok = if let ColorType::RGB { transparent_color } = png.ihdr.color_type {
|
||||
// Convert the RGB16 transparency to RGB8
|
||||
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
|
||||
reduce_scanline_to_palette(
|
||||
png.data.as_rgb().iter().cloned().map(|px| {
|
||||
px.alpha(if Some(px) != transparency_pixel {
|
||||
255
|
||||
} else {
|
||||
0
|
||||
if matches!(png.ihdr.color_type, ColorType::Indexed { .. }) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut raw_data = Vec::with_capacity(png.data.len() / png.channels_per_pixel());
|
||||
let mut palette: Vec<_> = match png.ihdr.color_type {
|
||||
ColorType::Grayscale { transparent_shade } => {
|
||||
let pmap = build_palette(png.data.as_gray().iter().cloned(), &mut raw_data)?;
|
||||
// Convert the Gray16 transparency to Gray8
|
||||
let transparency_pixel = transparent_shade.map(|t| Gray::from(t as u8));
|
||||
pmap.into_iter()
|
||||
.map(|px| {
|
||||
RGB::from(px).alpha(if Some(px) != transparency_pixel {
|
||||
255
|
||||
} else {
|
||||
0
|
||||
})
|
||||
})
|
||||
}),
|
||||
&mut palette,
|
||||
&mut raw_data,
|
||||
)
|
||||
} else if png.ihdr.color_type == ColorType::GrayscaleAlpha {
|
||||
reduce_scanline_to_palette(
|
||||
png.data.as_gray_alpha().iter().cloned().map(|px| RGBA {
|
||||
r: px.0,
|
||||
g: px.0,
|
||||
b: px.0,
|
||||
a: px.1,
|
||||
}),
|
||||
&mut palette,
|
||||
&mut raw_data,
|
||||
)
|
||||
} else {
|
||||
debug_assert_eq!(png.ihdr.color_type, ColorType::RGBA);
|
||||
reduce_scanline_to_palette(
|
||||
png.data.as_rgba().iter().cloned(),
|
||||
&mut palette,
|
||||
&mut raw_data,
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
ColorType::RGB { transparent_color } => {
|
||||
let pmap = build_palette(png.data.as_rgb().iter().cloned(), &mut raw_data)?;
|
||||
// Convert the RGB16 transparency to RGB8
|
||||
let transparency_pixel = transparent_color.map(|t| t.map(|c| c as u8));
|
||||
pmap.into_iter()
|
||||
.map(|px| {
|
||||
px.alpha(if Some(px) != transparency_pixel {
|
||||
255
|
||||
} else {
|
||||
0
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
ColorType::GrayscaleAlpha => {
|
||||
let pmap = build_palette(png.data.as_gray_alpha().iter().cloned(), &mut raw_data)?;
|
||||
pmap.into_iter().map(RGBA::from).collect()
|
||||
}
|
||||
ColorType::RGBA => {
|
||||
let pmap = build_palette(png.data.as_rgba().iter().cloned(), &mut raw_data)?;
|
||||
pmap.into_iter().collect()
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
|
||||
let num_transparent = palette
|
||||
.iter()
|
||||
.filter_map(|(px, &idx)| {
|
||||
if px.a != 255 {
|
||||
Some(idx as usize + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max();
|
||||
let trns_size = num_transparent.map_or(0, |n| n + 8);
|
||||
|
||||
let headers_size = palette.len() * 3 + 8 + trns_size;
|
||||
if raw_data.len() + headers_size > png.data.len() {
|
||||
// Reduction would result in a larger image
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut aux_headers = png.aux_headers.clone();
|
||||
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
|
||||
if let Some(bkgd_header) = aux_headers.remove(b"bKGD") {
|
||||
let bg = if png.ihdr.color_type.is_rgb() && bkgd_header.len() == 6 {
|
||||
// In bKGD 16-bit values are used even for 8-bit images
|
||||
Some(RGBA8::new(
|
||||
|
|
@ -106,7 +91,7 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
|
|||
bkgd_header[5],
|
||||
255,
|
||||
))
|
||||
} else if png.ihdr.color_type == ColorType::GrayscaleAlpha && bkgd_header.len() == 2 {
|
||||
} else if png.ihdr.color_type.is_grayscale() && bkgd_header.len() == 2 {
|
||||
Some(RGBA8::new(
|
||||
bkgd_header[1],
|
||||
bkgd_header[1],
|
||||
|
|
@ -117,16 +102,15 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
|
|||
None
|
||||
};
|
||||
if let Some(bg) = bg {
|
||||
let entry = if let Some(&entry) = palette.get(&bg) {
|
||||
entry
|
||||
} else if palette.len() < 256 {
|
||||
let entry = palette.len() as u8;
|
||||
palette.insert(bg, entry);
|
||||
entry
|
||||
} else {
|
||||
return None; // No space in palette to store the bg as an index
|
||||
};
|
||||
aux_headers.insert(*b"bKGD", vec![entry]);
|
||||
let idx = palette.iter().position(|&px| px == bg).or_else(|| {
|
||||
if palette.len() < 256 {
|
||||
palette.push(bg);
|
||||
Some(palette.len() - 1)
|
||||
} else {
|
||||
None // No space in palette to store the bg as an index
|
||||
}
|
||||
})?;
|
||||
aux_headers.insert(*b"bKGD", vec![idx as u8]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,17 +119,10 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
|
|||
aux_headers.insert(*b"sBIT", sbit_header.iter().cloned().take(3).collect());
|
||||
}
|
||||
|
||||
let mut palette_vec = vec![RGBA8::new(0, 0, 0, 0); palette.len()];
|
||||
for (color, idx) in palette {
|
||||
palette_vec[idx as usize] = color;
|
||||
}
|
||||
|
||||
Some(PngImage {
|
||||
data: raw_data,
|
||||
ihdr: IhdrData {
|
||||
color_type: ColorType::Indexed {
|
||||
palette: palette_vec,
|
||||
},
|
||||
color_type: ColorType::Indexed { palette },
|
||||
..png.ihdr
|
||||
},
|
||||
aux_headers,
|
||||
|
|
@ -153,7 +130,7 @@ pub fn reduce_to_palette(png: &PngImage) -> Option<PngImage> {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
|
||||
pub fn reduced_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
|
||||
if !png.ihdr.color_type.is_rgb() {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -204,3 +181,67 @@ pub fn reduce_rgb_to_grayscale(png: &PngImage) -> Option<PngImage> {
|
|||
aux_headers,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to convert indexed to a different color type, returning the resulting image if successful
|
||||
#[must_use]
|
||||
pub fn indexed_to_channels(png: &PngImage) -> Option<PngImage> {
|
||||
if png.ihdr.bit_depth != BitDepth::Eight {
|
||||
return None;
|
||||
}
|
||||
let palette = match &png.ihdr.color_type {
|
||||
ColorType::Indexed { palette } => palette,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
// Determine which channels are required
|
||||
let is_gray = palette.iter().all(|c| c.r == c.g && c.g == c.b);
|
||||
let has_alpha = palette.iter().any(|c| c.a != 255);
|
||||
let color_type = match (is_gray, has_alpha) {
|
||||
(false, true) => ColorType::RGBA,
|
||||
(false, false) => ColorType::RGB {
|
||||
transparent_color: None,
|
||||
},
|
||||
(true, true) => ColorType::GrayscaleAlpha,
|
||||
(true, false) => ColorType::Grayscale {
|
||||
transparent_shade: None,
|
||||
},
|
||||
};
|
||||
|
||||
// Don't proceed if output would be too much larger
|
||||
let out_size = color_type.channels_per_pixel() as usize * png.data.len();
|
||||
if out_size - png.data.len() > INDEXED_MAX_DIFF {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Construct the new data
|
||||
let black = RGBA::new(0, 0, 0, 255);
|
||||
let ch_start = if is_gray { 2 } else { 0 };
|
||||
let ch_end = if has_alpha { 3 } else { 2 };
|
||||
let mut data = Vec::with_capacity(out_size);
|
||||
for b in &png.data {
|
||||
let color = palette.get(*b as usize).unwrap_or(&black);
|
||||
data.extend_from_slice(&color.as_slice()[ch_start..=ch_end]);
|
||||
}
|
||||
|
||||
// Update bKGD if it exists
|
||||
let mut aux_headers = png.aux_headers.clone();
|
||||
if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) {
|
||||
if let Some(color) = palette.get(idx as usize) {
|
||||
let bkgd = if is_gray {
|
||||
vec![0, color.r]
|
||||
} else {
|
||||
vec![0, color.r, 0, color.g, 0, color.b]
|
||||
};
|
||||
aux_headers.insert(*b"bKGD", bkgd);
|
||||
}
|
||||
}
|
||||
|
||||
Some(PngImage {
|
||||
ihdr: IhdrData {
|
||||
color_type,
|
||||
..png.ihdr
|
||||
},
|
||||
data,
|
||||
aux_headers,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,16 @@ pub(crate) fn perform_reductions(
|
|||
// Attempt to reduce RGB to grayscale
|
||||
// This is just removal of bytes and does not need to be evaluated
|
||||
if opts.color_type_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduce_rgb_to_grayscale(&png) {
|
||||
if let Some(reduced) = reduced_rgb_to_grayscale(&png) {
|
||||
png = Arc::new(reduced);
|
||||
reduction_occurred = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to reduce the palette
|
||||
// This may change bytes but should always be beneficial
|
||||
if opts.palette_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduced_palette(&png, opts.optimize_alpha) {
|
||||
png = Arc::new(reduced);
|
||||
reduction_occurred = true;
|
||||
}
|
||||
|
|
@ -65,9 +74,9 @@ pub(crate) fn perform_reductions(
|
|||
if opts.color_type_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduced_alpha_channel(&png, opts.optimize_alpha) {
|
||||
png = Arc::new(reduced);
|
||||
// If the reduction requires a tRNS chunk, enter this into the evaluator
|
||||
// Otherwise it is just removal of bytes and should become the baseline
|
||||
if png.ihdr.color_type.has_trns() {
|
||||
// For small differences, if a tRNS chunk is required then enter this into the evaluator
|
||||
// Otherwise it is mostly just removal of bytes and should become the baseline
|
||||
if png.ihdr.color_type.has_trns() && baseline.data.len() - png.data.len() <= 1000 {
|
||||
eval.try_image(png.clone());
|
||||
evaluation_added = true;
|
||||
} else {
|
||||
|
|
@ -77,33 +86,52 @@ pub(crate) fn perform_reductions(
|
|||
}
|
||||
}
|
||||
|
||||
// Attempt to reduce the palette size
|
||||
// Attempt to sort the palette
|
||||
if opts.palette_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) {
|
||||
if let Some(reduced) = sorted_palette(&png) {
|
||||
png = Arc::new(reduced);
|
||||
eval.try_image(png.clone());
|
||||
evaluation_added = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to reduce to palette
|
||||
// Attempt to convert from indexed to channels
|
||||
// This may give a better result due to dropping the PLTE chunk
|
||||
if opts.color_type_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduce_to_palette(&png) {
|
||||
png = Arc::new(reduced);
|
||||
// Make sure the palette gets sorted (ideally, this should be done within reduce_to_palette)
|
||||
if let Some(reduced) = optimized_palette(&png, opts.optimize_alpha) {
|
||||
png = Arc::new(reduced);
|
||||
}
|
||||
eval.try_image(png.clone());
|
||||
if let Some(reduced) = indexed_to_channels(&png) {
|
||||
// This result should not be passed on to subsequent reductions
|
||||
eval.try_image(Arc::new(reduced));
|
||||
evaluation_added = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to reduce to indexed
|
||||
let mut indexed = None;
|
||||
if opts.color_type_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduced_to_indexed(&png) {
|
||||
// Make sure the palette gets sorted (but don't bother evaluating both results)
|
||||
let new = Arc::new(sorted_palette(&reduced).unwrap_or(reduced));
|
||||
// For relatively small differences, enter this into the evaluator
|
||||
// Otherwise we're confident enough for it to become the baseline
|
||||
if png.data.len() - new.data.len() <= INDEXED_MAX_DIFF {
|
||||
eval.try_image(new.clone());
|
||||
evaluation_added = true;
|
||||
} else {
|
||||
baseline = new.clone();
|
||||
reduction_occurred = true;
|
||||
}
|
||||
indexed = Some(new);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to reduce to a lower bit depth
|
||||
if opts.bit_depth_reduction && !deadline.passed() {
|
||||
if let Some(reduced) = reduced_bit_depth_8_or_less(&png, 1) {
|
||||
png = Arc::new(reduced);
|
||||
eval.try_image(png.clone());
|
||||
// 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)));
|
||||
if let Some(reduced) = reduced {
|
||||
eval.try_image(Arc::new(reduced));
|
||||
evaluation_added = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,180 +1,201 @@
|
|||
use crate::colors::{BitDepth, ColorType};
|
||||
use crate::headers::IhdrData;
|
||||
use crate::png::PngImage;
|
||||
use indexmap::map::{Entry::*, IndexMap};
|
||||
use indexmap::IndexSet;
|
||||
use rgb::RGBA8;
|
||||
|
||||
/// Attempt to shrink and sort the palette, returning the optimized image if successful
|
||||
/// Attempt to reduce the number of colors in the palette, returning the reduced image if successful
|
||||
#[must_use]
|
||||
pub fn optimized_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
|
||||
pub fn reduced_palette(png: &PngImage, optimize_alpha: bool) -> Option<PngImage> {
|
||||
let palette = match &png.ihdr.color_type {
|
||||
ColorType::Indexed { palette } => palette,
|
||||
// Can't reduce if there is no palette
|
||||
ColorType::Indexed { palette } if palette.len() > 1 => palette,
|
||||
_ => return None,
|
||||
};
|
||||
if png.ihdr.bit_depth == BitDepth::One {
|
||||
// Gains from 1-bit images will be at most 1 byte
|
||||
// Not worth the CPU time
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut palette_map = [None; 256];
|
||||
let mut used = [false; 256];
|
||||
{
|
||||
// Find palette entries that are never used
|
||||
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;
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
let used = get_used_entries(png);
|
||||
|
||||
let black = RGBA8::new(0, 0, 0, 255);
|
||||
let mut condensed = IndexSet::with_capacity(palette.len());
|
||||
let mut palette_map = [0; 256];
|
||||
let mut did_change = false;
|
||||
for (i, used) in used.iter().enumerate() {
|
||||
if !used {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut used_enumerated: Vec<(usize, &bool)> = used.iter().enumerate().collect();
|
||||
used_enumerated.sort_by(|a, b| {
|
||||
//Sort by ascending alpha and descending luma.
|
||||
let color_val = |i| {
|
||||
let color = palette
|
||||
.get(i)
|
||||
.copied()
|
||||
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
|
||||
((color.a as i32) << 18)
|
||||
// These are coefficients for standard sRGB to luma conversion
|
||||
- i32::from(color.r) * 299
|
||||
- i32::from(color.g) * 587
|
||||
- i32::from(color.b) * 114
|
||||
};
|
||||
color_val(a.0).cmp(&color_val(b.0))
|
||||
});
|
||||
|
||||
// Make sure the background is also included, but only after sorting since it may not be used in idat
|
||||
if let Some(&idx) = png.aux_headers.get(b"bKGD").and_then(|b| b.first()) {
|
||||
if !used[idx as usize] {
|
||||
used_enumerated.push((idx as usize, &true));
|
||||
}
|
||||
}
|
||||
|
||||
let mut next_index = 0_u16;
|
||||
let mut seen = IndexMap::with_capacity(palette.len());
|
||||
for (i, used) in used_enumerated.iter().cloned() {
|
||||
if !used {
|
||||
continue;
|
||||
}
|
||||
// There are invalid files that use pixel indices beyond palette size
|
||||
let mut color = palette
|
||||
.get(i)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| RGBA8::new(0, 0, 0, 255));
|
||||
// If there are multiple fully transparent entries, reduce them into one
|
||||
if optimize_alpha && color.a == 0 {
|
||||
color.r = 0;
|
||||
color.g = 0;
|
||||
color.b = 0;
|
||||
}
|
||||
match seen.entry(color) {
|
||||
Vacant(new) => {
|
||||
palette_map[i] = Some(next_index as u8);
|
||||
new.insert(next_index as u8);
|
||||
next_index += 1;
|
||||
}
|
||||
Occupied(remap_to) => palette_map[i] = Some(*remap_to.get()),
|
||||
}
|
||||
// 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 {
|
||||
did_change = true;
|
||||
}
|
||||
}
|
||||
|
||||
do_palette_reduction(png, palette, &palette_map)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn do_palette_reduction(
|
||||
png: &PngImage,
|
||||
palette: &[RGBA8],
|
||||
palette_map: &[Option<u8>; 256],
|
||||
) -> Option<PngImage> {
|
||||
let byte_map = palette_map_to_byte_map(png, palette_map)?;
|
||||
|
||||
// Reassign data bytes to new indices
|
||||
let raw_data = png.data.iter().map(|b| byte_map[*b as usize]).collect();
|
||||
|
||||
// Update bKGD if it exists, ensuring it comes last in the palette if otherwise unused
|
||||
let mut aux_headers = png.aux_headers.clone();
|
||||
if let Some(bkgd_header) = png.aux_headers.get(b"bKGD") {
|
||||
if let Some(Some(map_to)) = bkgd_header
|
||||
.first()
|
||||
.and_then(|&idx| palette_map.get(idx as usize))
|
||||
{
|
||||
aux_headers.insert(*b"bKGD", vec![*map_to]);
|
||||
if let Some(idx) = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned()) {
|
||||
if let Some(&color) = palette.get(idx as usize) {
|
||||
let idx = add_color_to_set(color, &mut condensed, optimize_alpha);
|
||||
aux_headers.insert(*b"bKGD", vec![idx]);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
png.data.clone()
|
||||
} else {
|
||||
// Nothing has changed
|
||||
return None;
|
||||
};
|
||||
|
||||
let palette: Vec<_> = condensed.into_iter().collect();
|
||||
|
||||
Some(PngImage {
|
||||
ihdr: IhdrData {
|
||||
color_type: ColorType::Indexed {
|
||||
palette: reordered_palette(palette, palette_map),
|
||||
},
|
||||
color_type: ColorType::Indexed { palette },
|
||||
..png.ihdr
|
||||
},
|
||||
data: raw_data,
|
||||
data,
|
||||
aux_headers,
|
||||
})
|
||||
}
|
||||
|
||||
fn palette_map_to_byte_map(png: &PngImage, palette_map: &[Option<u8>; 256]) -> Option<[u8; 256]> {
|
||||
if (0..256).all(|i| palette_map[i].map_or(true, |to| to == i as u8)) {
|
||||
// No reduction necessary
|
||||
return None;
|
||||
fn add_color_to_set(mut color: RGBA8, set: &mut IndexSet<RGBA8>, optimize_alpha: bool) -> u8 {
|
||||
// If there are multiple fully transparent entries, reduce them into one
|
||||
if optimize_alpha && color.a == 0 {
|
||||
color.r = 0;
|
||||
color.g = 0;
|
||||
color.b = 0;
|
||||
}
|
||||
let (idx, _) = set.insert_full(color);
|
||||
idx as u8
|
||||
}
|
||||
|
||||
let mut byte_map = [0_u8; 256];
|
||||
|
||||
// low bit-depths can be pre-computed for every byte value
|
||||
fn get_used_entries(png: &PngImage) -> [bool; 256] {
|
||||
let mut used = [false; 256];
|
||||
match png.ihdr.bit_depth {
|
||||
BitDepth::Eight => {
|
||||
for byte in 0..=255usize {
|
||||
byte_map[byte] = palette_map[byte].unwrap_or(0)
|
||||
for &byte in &png.data {
|
||||
used[byte as usize] = true;
|
||||
}
|
||||
}
|
||||
BitDepth::Four => {
|
||||
for byte in 0..=255usize {
|
||||
byte_map[byte] = palette_map[byte & 0x0F].unwrap_or(0)
|
||||
| (palette_map[byte >> 4].unwrap_or(0) << 4);
|
||||
for &byte in &png.data {
|
||||
used[(byte & 0x0F) as usize] = true;
|
||||
used[(byte >> 4) as usize] = true;
|
||||
}
|
||||
}
|
||||
BitDepth::Two => {
|
||||
for byte in 0..=255usize {
|
||||
byte_map[byte] = palette_map[byte & 0x03].unwrap_or(0)
|
||||
| (palette_map[(byte >> 2) & 0x03].unwrap_or(0) << 2)
|
||||
| (palette_map[(byte >> 4) & 0x03].unwrap_or(0) << 4)
|
||||
| (palette_map[byte >> 6].unwrap_or(0) << 6);
|
||||
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;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(byte_map)
|
||||
}
|
||||
|
||||
fn reordered_palette(palette: &[RGBA8], palette_map: &[Option<u8>; 256]) -> Vec<RGBA8> {
|
||||
let max_index = palette_map.iter().cloned().flatten().max().unwrap_or(0) as usize;
|
||||
let mut new_palette = vec![RGBA8::new(0, 0, 0, 255); max_index + 1];
|
||||
for (&color, &map_to) in palette.iter().zip(palette_map.iter()) {
|
||||
if let Some(map_to) = map_to {
|
||||
new_palette[map_to as usize] = color;
|
||||
BitDepth::One => {
|
||||
// Only two options, don't bother checking which are actually used
|
||||
used[0] = true;
|
||||
used[1] = true;
|
||||
}
|
||||
}
|
||||
new_palette
|
||||
_ => 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
|
||||
return None;
|
||||
}
|
||||
let palette = match &png.ihdr.color_type {
|
||||
ColorType::Indexed { palette } => palette,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let mut enumerated: Vec<_> = palette.iter().enumerate().collect();
|
||||
|
||||
// If the background is the last entry in the palette we should make sure it stays last
|
||||
// Otherwise an entry that's unused by the idat could prevent reduction to a lower depth
|
||||
let mut aux_headers = png.aux_headers.clone();
|
||||
let bkgd_idx = aux_headers.remove(b"bKGD").and_then(|b| b.first().cloned());
|
||||
let bkgd_last = match bkgd_idx {
|
||||
Some(idx) if idx as usize + 1 == palette.len() => enumerated.pop(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Sort the palette
|
||||
enumerated.sort_by(|a, b| {
|
||||
// Sort by ascending alpha and descending luma
|
||||
let color_val = |color: &RGBA8| {
|
||||
((color.a as i32) << 18)
|
||||
// These are coefficients for standard sRGB to luma conversion
|
||||
- i32::from(color.r) * 299
|
||||
- i32::from(color.g) * 587
|
||||
- i32::from(color.b) * 114
|
||||
};
|
||||
color_val(a.1).cmp(&color_val(b.1))
|
||||
});
|
||||
|
||||
if let Some(bkgd) = bkgd_last {
|
||||
enumerated.push(bkgd);
|
||||
}
|
||||
|
||||
// Extract the new palette and determine if anything changed
|
||||
let (old_map, palette): (Vec<_>, Vec<RGBA8>) = enumerated.into_iter().unzip();
|
||||
if old_map.iter().enumerate().all(|(a, b)| a == *b) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Construct the palette and byte maps and convert the data
|
||||
let mut new_map = [0; 256];
|
||||
for (i, &v) in old_map.iter().enumerate() {
|
||||
new_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();
|
||||
|
||||
// Update bKGD if it exists
|
||||
if let Some(idx) = bkgd_idx.map(|idx| new_map[idx as usize]) {
|
||||
aux_headers.insert(*b"bKGD", vec![idx]);
|
||||
}
|
||||
|
||||
Some(PngImage {
|
||||
ihdr: IhdrData {
|
||||
color_type: ColorType::Indexed { palette },
|
||||
..png.ihdr
|
||||
},
|
||||
data,
|
||||
aux_headers,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 66 KiB |
BIN
tests/files/grayscale_8_should_be_palette_8.png
Normal file
BIN
tests/files/grayscale_8_should_be_palette_8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
tests/files/palette_8_should_be_rgb.png
Normal file
BIN
tests/files/palette_8_should_be_rgb.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 195 B |
BIN
tests/files/palette_8_should_be_rgba.png
Normal file
BIN
tests/files/palette_8_should_be_rgba.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -427,8 +427,8 @@ fn interlacing_0_to_1_small_files() {
|
|||
};
|
||||
|
||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::Adam7);
|
||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
|
||||
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::One);
|
||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGB);
|
||||
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
|
||||
|
||||
remove_file(output).ok();
|
||||
}
|
||||
|
|
@ -461,8 +461,8 @@ fn interlacing_1_to_0_small_files() {
|
|||
};
|
||||
|
||||
assert_eq!(png.raw.ihdr.interlaced, Interlacing::None);
|
||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
|
||||
// the depth can't be asserted reliably, because on such small file different zlib implementations pick different depth as the best
|
||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), RGB);
|
||||
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
|
||||
|
||||
remove_file(output).ok();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -546,6 +546,17 @@ fn interlaced_palette_1_should_be_palette_1() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interlaced_palette_8_should_be_grayscale_8() {
|
||||
test_it_converts(
|
||||
"tests/files/interlaced_palette_8_should_be_grayscale_8.png",
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
GRAYSCALE,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interlaced_grayscale_alpha_16_should_be_grayscale_alpha_16() {
|
||||
test_it_converts(
|
||||
|
|
@ -651,8 +662,8 @@ fn interlaced_small_files() {
|
|||
"tests/files/interlaced_small_files.png",
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
INDEXED,
|
||||
BitDepth::One,
|
||||
RGB,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -556,6 +556,42 @@ fn palette_4_should_be_palette_2() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_8_should_be_grayscale_8() {
|
||||
test_it_converts(
|
||||
"tests/files/palette_8_should_be_grayscale_8.png",
|
||||
false,
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
GRAYSCALE,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_8_should_be_rgb() {
|
||||
test_it_converts(
|
||||
"tests/files/palette_8_should_be_rgb.png",
|
||||
false,
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
RGB,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_8_should_be_rgba() {
|
||||
test_it_converts(
|
||||
"tests/files/palette_8_should_be_rgba.png",
|
||||
false,
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
RGBA,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_2_should_be_palette_2() {
|
||||
test_it_converts(
|
||||
|
|
@ -808,6 +844,54 @@ fn grayscale_2_should_be_grayscale_1() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_8_should_be_palette_8() {
|
||||
test_it_converts(
|
||||
"tests/files/grayscale_8_should_be_palette_8.png",
|
||||
false,
|
||||
GRAYSCALE,
|
||||
BitDepth::Eight,
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_8_should_be_palette_4() {
|
||||
test_it_converts(
|
||||
"tests/files/grayscale_8_should_be_palette_4.png",
|
||||
false,
|
||||
GRAYSCALE,
|
||||
BitDepth::Eight,
|
||||
INDEXED,
|
||||
BitDepth::Four,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_8_should_be_palette_2() {
|
||||
test_it_converts(
|
||||
"tests/files/grayscale_8_should_be_palette_2.png",
|
||||
false,
|
||||
GRAYSCALE,
|
||||
BitDepth::Eight,
|
||||
INDEXED,
|
||||
BitDepth::Two,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_8_should_be_palette_1() {
|
||||
test_it_converts(
|
||||
"tests/files/grayscale_8_should_be_palette_1.png",
|
||||
false,
|
||||
GRAYSCALE,
|
||||
BitDepth::Eight,
|
||||
INDEXED,
|
||||
BitDepth::One,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_alpha_16_should_be_grayscale_trns_16() {
|
||||
test_it_converts(
|
||||
|
|
@ -834,33 +918,14 @@ fn grayscale_alpha_8_should_be_grayscale_trns_8() {
|
|||
|
||||
#[test]
|
||||
fn small_files() {
|
||||
let input = PathBuf::from("tests/files/small_files.png");
|
||||
let (output, opts) = get_opts(&input);
|
||||
|
||||
let png = PngData::new(&input, opts.fix_errors).unwrap();
|
||||
|
||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
|
||||
assert_eq!(png.raw.ihdr.bit_depth, BitDepth::Eight);
|
||||
|
||||
match oxipng::optimize(&InFile::Path(input), &output, &opts) {
|
||||
Ok(_) => (),
|
||||
Err(x) => panic!("{}", x),
|
||||
};
|
||||
let output = output.path().unwrap();
|
||||
assert!(output.exists());
|
||||
|
||||
let png = match PngData::new(output, opts.fix_errors) {
|
||||
Ok(x) => x,
|
||||
Err(x) => {
|
||||
remove_file(output).ok();
|
||||
panic!("{}", x)
|
||||
}
|
||||
};
|
||||
|
||||
assert_eq!(png.raw.ihdr.color_type.png_header_code(), INDEXED);
|
||||
// depth varies depending on zlib implementation used
|
||||
|
||||
remove_file(output).ok();
|
||||
test_it_converts(
|
||||
"tests/files/small_files.png",
|
||||
false,
|
||||
INDEXED,
|
||||
BitDepth::Eight,
|
||||
RGB,
|
||||
BitDepth::Eight,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue