Code always tends to get messy over time. I've found the `optimize_raw`
function increasingly harder to read, particularly after the addition of
fast mode, so I've taken some time to refactor and simplify it.
One change of note here is the main compression trials now use the
Evaluator. This means verbose output is a little different which is
shown below.
There is no change to performance or output size.
`-vvo2`: master
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 4-bit Indexed (5 colors) None 1837 bytes
Eval: 8-bit Indexed (5 colors) None 1988 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1837 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1837 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Evaluating: 2 filters
Eval: 4-bit Indexed (5 colors) Sub >1810 bytes
Eval: 4-bit Indexed (5 colors) Entropy >1810 bytes
Trying: None
zc = 11 f = None 1583 bytes
Found better combination:
zc = 11 f = None 1583 bytes
IDAT size = 1583 bytes (1174 bytes decrease)
file size = 16962 bytes (1147 bytes = 6.33% decrease)
16962 bytes (6.33% smaller): Running in pretend mode, no output
```
`-vvo2`: PR
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 4-bit Indexed (5 colors) None 1837 bytes
Eval: 8-bit Indexed (5 colors) None 1988 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1837 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1837 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Evaluating 2 filters
Eval: 4-bit Indexed (5 colors) Sub >1810 bytes
Eval: 4-bit Indexed (5 colors) Entropy >1810 bytes
Trying filter None with zc = 11
1610 bytes
Found better result:
zc = 11, f = None
IDAT size = 1583 bytes (1174 bytes decrease)
file size = 16962 bytes (1147 bytes = 6.33% decrease)
16962 bytes (6.33% smaller): Running in pretend mode, no output
```
`-vvZo5`: master
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 8-bit Indexed (battiato sort) None 1821 bytes
Eval: 4-bit Indexed (5 colors) None 1657 bytes
Eval: 8-bit Indexed (mzeng sort) None 1821 bytes
Eval: 8-bit Indexed (5 colors) None 1821 bytes
Eval: 8-bit Indexed (battiato sort) Bigrams >1821 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1657 bytes
Eval: 8-bit Indexed (mzeng sort) Bigrams >1657 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1657 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Trying: 8 filters
zc = zopfli f = Brute 1562 bytes
zc = zopfli f = Sub >1562 bytes
zc = zopfli f = Bigrams >1562 bytes
zc = zopfli f = None 1407 bytes
zc = zopfli f = Up >1407 bytes
zc = zopfli f = MinSum >1407 bytes
zc = zopfli f = BigEnt >1407 bytes
zc = zopfli f = Entropy >1407 bytes
Found better combination:
zc = zopfli f = None 1407 bytes
IDAT size = 1407 bytes (1350 bytes decrease)
file size = 16786 bytes (1323 bytes = 7.31% decrease)
16786 bytes (7.31% smaller): Running in pretend mode, no output
```
`-vvZo5`: PR
```
Processing: tests/files/rgba_8_should_be_palette_4.png
500x400 pixels, PNG format
8-bit RGB + Alpha, non-interlaced
IDAT size = 2757 bytes
File size = 18109 bytes
Eval: 8-bit Indexed (battiato sort) None 1821 bytes
Eval: 4-bit Indexed (5 colors) None 1657 bytes
Eval: 8-bit Indexed (mzeng sort) None 1821 bytes
Eval: 8-bit Indexed (5 colors) None 1821 bytes
Eval: 8-bit Indexed (battiato sort) Bigrams >1657 bytes
Eval: 4-bit Indexed (5 colors) Bigrams >1657 bytes
Eval: 8-bit Indexed (mzeng sort) Bigrams >1657 bytes
Eval: 8-bit Indexed (5 colors) Bigrams >1657 bytes
Transformed image to 4-bit Indexed (5 colors), non-interlaced
Trying 8 filters with zopfli, zi = 15
Eval: 4-bit Indexed (5 colors) Brute 1589 bytes
Eval: 4-bit Indexed (5 colors) Bigrams 1641 bytes
Eval: 4-bit Indexed (5 colors) Sub 1711 bytes
Eval: 4-bit Indexed (5 colors) None 1434 bytes
Eval: 4-bit Indexed (5 colors) Up 1764 bytes
Eval: 4-bit Indexed (5 colors) MinSum 1760 bytes
Eval: 4-bit Indexed (5 colors) BigEnt 1742 bytes
Eval: 4-bit Indexed (5 colors) Entropy 1748 bytes
Found better result:
zopfli, zi = 15, f = None
IDAT size = 1407 bytes (1350 bytes decrease)
file size = 16786 bytes (1323 bytes = 7.31% decrease)
16786 bytes (7.31% smaller): Running in pretend mode, no output
```
129 lines
3.6 KiB
Rust
129 lines
3.6 KiB
Rust
use std::{fmt, fmt::Display};
|
|
|
|
use rgb::{RGB16, RGBA8};
|
|
|
|
use crate::PngError;
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
/// The color type used to represent this image
|
|
pub enum ColorType {
|
|
/// Grayscale, with one color channel
|
|
Grayscale {
|
|
/// Optional shade of gray that should be rendered as transparent
|
|
transparent_shade: Option<u16>,
|
|
},
|
|
/// RGB, with three color channels
|
|
RGB {
|
|
/// Optional color value that should be rendered as transparent
|
|
transparent_color: Option<RGB16>,
|
|
},
|
|
/// Indexed, with one byte per pixel representing a color from the palette
|
|
Indexed {
|
|
/// The palette containing the colors used, up to 256 entries
|
|
palette: Vec<RGBA8>,
|
|
},
|
|
/// Grayscale + Alpha, with two color channels
|
|
GrayscaleAlpha,
|
|
/// RGBA, with four color channels
|
|
RGBA,
|
|
}
|
|
|
|
impl Display for ColorType {
|
|
#[inline]
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Grayscale { .. } => write!(f, "Grayscale"),
|
|
Self::RGB { .. } => write!(f, "RGB"),
|
|
Self::Indexed { palette } => write!(f, "Indexed ({} colors)", palette.len()),
|
|
Self::GrayscaleAlpha => write!(f, "Grayscale + Alpha"),
|
|
Self::RGBA => write!(f, "RGB + Alpha"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ColorType {
|
|
/// Get the code used by the PNG specification to denote this color type
|
|
#[inline]
|
|
#[must_use]
|
|
pub const fn png_header_code(&self) -> u8 {
|
|
match self {
|
|
Self::Grayscale { .. } => 0,
|
|
Self::RGB { .. } => 2,
|
|
Self::Indexed { .. } => 3,
|
|
Self::GrayscaleAlpha => 4,
|
|
Self::RGBA => 6,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) const fn channels_per_pixel(&self) -> u8 {
|
|
match self {
|
|
Self::Grayscale { .. } | Self::Indexed { .. } => 1,
|
|
Self::GrayscaleAlpha => 2,
|
|
Self::RGB { .. } => 3,
|
|
Self::RGBA => 4,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) const fn is_rgb(&self) -> bool {
|
|
matches!(self, Self::RGB { .. } | Self::RGBA)
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) const fn is_gray(&self) -> bool {
|
|
matches!(self, Self::Grayscale { .. } | Self::GrayscaleAlpha)
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) const fn has_alpha(&self) -> bool {
|
|
matches!(self, Self::GrayscaleAlpha | Self::RGBA)
|
|
}
|
|
|
|
#[inline]
|
|
pub(crate) const fn has_trns(&self) -> bool {
|
|
match self {
|
|
Self::Grayscale { transparent_shade } => transparent_shade.is_some(),
|
|
Self::RGB { transparent_color } => transparent_color.is_some(),
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(u8)]
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
|
/// The number of bits to be used per channel per pixel
|
|
pub enum BitDepth {
|
|
/// One bit per channel per pixel
|
|
One = 1,
|
|
/// Two bits per channel per pixel
|
|
Two = 2,
|
|
/// Four bits per channel per pixel
|
|
Four = 4,
|
|
/// Eight bits per channel per pixel
|
|
Eight = 8,
|
|
/// Sixteen bits per channel per pixel
|
|
Sixteen = 16,
|
|
}
|
|
|
|
impl TryFrom<u8> for BitDepth {
|
|
type Error = PngError;
|
|
|
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
match value {
|
|
1 => Ok(Self::One),
|
|
2 => Ok(Self::Two),
|
|
4 => Ok(Self::Four),
|
|
8 => Ok(Self::Eight),
|
|
16 => Ok(Self::Sixteen),
|
|
_ => Err(PngError::new("Unexpected bit depth")),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for BitDepth {
|
|
#[inline]
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
Display::fmt(&(*self as u8).to_string(), f)
|
|
}
|
|
}
|