#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Quality {
ApproxJpegli(f32),
ApproxMozjpeg(u8),
ApproxSsim2(f32),
ApproxButteraugli(f32),
}
impl Default for Quality {
fn default() -> Self {
Quality::ApproxJpegli(90.0)
}
}
impl From<f32> for Quality {
fn from(q: f32) -> Self {
Quality::ApproxJpegli(q)
}
}
impl From<u8> for Quality {
fn from(q: u8) -> Self {
Quality::ApproxJpegli(q as f32)
}
}
impl From<i32> for Quality {
fn from(q: i32) -> Self {
Quality::ApproxJpegli(q as f32)
}
}
impl Quality {
#[must_use]
pub fn to_internal(&self) -> f32 {
match self {
Quality::ApproxJpegli(q) => *q,
Quality::ApproxMozjpeg(q) => mozjpeg_to_internal(*q),
Quality::ApproxSsim2(score) => ssim2_to_internal(*score),
Quality::ApproxButteraugli(dist) => butteraugli_to_internal(*dist),
}
}
#[must_use]
pub fn to_distance(&self) -> f32 {
if let Quality::ApproxButteraugli(d) = self {
return *d;
}
let q = self.to_internal();
if q >= 100.0 {
0.01
} else if q >= 30.0 {
0.1 + (100.0 - q) * 0.09
} else {
53.0 / 3000.0 * q * q - 23.0 / 20.0 * q + 25.0
}
}
}
const MOZJPEG_TO_JPEGLI: [(u8, u8); 10] = [
(30, 28),
(40, 37),
(50, 47),
(60, 55),
(70, 65),
(75, 71),
(80, 77),
(85, 83),
(90, 89),
(95, 94),
];
fn mozjpeg_to_internal(q: u8) -> f32 {
if q >= 100 {
return 100.0;
}
if q <= 30 {
return (q as f32 / 30.0) * 28.0;
}
let mut lower = (30u8, 28u8);
let mut upper = (95u8, 94u8);
for &(moz_q, jpegli_q) in &MOZJPEG_TO_JPEGLI {
if moz_q <= q && moz_q > lower.0 {
lower = (moz_q, jpegli_q);
}
if moz_q >= q && moz_q < upper.0 {
upper = (moz_q, jpegli_q);
}
}
if lower.0 == upper.0 {
return lower.1 as f32;
}
let t = (q - lower.0) as f32 / (upper.0 - lower.0) as f32;
lower.1 as f32 + t * (upper.1 as f32 - lower.1 as f32)
}
const SSIM2_TO_JPEGLI: [(u8, u8); 8] = [
(70, 55), (75, 65),
(80, 73),
(85, 80),
(88, 85),
(90, 88),
(93, 92),
(95, 95),
];
fn ssim2_to_internal(score: f32) -> f32 {
if score >= 100.0 {
return 100.0;
}
if score <= 70.0 {
return (score / 70.0) * 55.0;
}
let q = score as u8;
let mut lower = (70u8, 55u8);
let mut upper = (95u8, 95u8);
for &(ssim_score, jpegli_q) in &SSIM2_TO_JPEGLI {
if ssim_score <= q && ssim_score > lower.0 {
lower = (ssim_score, jpegli_q);
}
if ssim_score >= q && ssim_score < upper.0 {
upper = (ssim_score, jpegli_q);
}
}
if lower.0 == upper.0 {
return lower.1 as f32;
}
let t = (score - lower.0 as f32) / (upper.0 - lower.0) as f32;
lower.1 as f32 + t * (upper.1 as f32 - lower.1 as f32)
}
const BUTTERAUGLI_TO_JPEGLI: [(f32, f32); 7] = [
(0.3, 96.0),
(0.5, 93.0),
(1.0, 88.0),
(1.5, 82.0),
(2.0, 76.0),
(3.0, 68.0),
(5.0, 55.0),
];
fn butteraugli_to_internal(dist: f32) -> f32 {
if dist <= 0.0 {
return 100.0;
}
if dist <= 0.3 {
return 96.0 + (0.3 - dist) / 0.3 * 4.0;
}
if dist >= 5.0 {
return 55.0 - (dist - 5.0) * 3.0;
}
let mut lower = (0.3f32, 96.0f32);
let mut upper = (5.0f32, 55.0f32);
for &(ba_dist, jpegli_q) in &BUTTERAUGLI_TO_JPEGLI {
if ba_dist <= dist && ba_dist > lower.0 {
lower = (ba_dist, jpegli_q);
}
if ba_dist >= dist && ba_dist < upper.0 {
upper = (ba_dist, jpegli_q);
}
}
if (lower.0 - upper.0).abs() < 0.001 {
return lower.1;
}
let t = (dist - lower.0) / (upper.0 - lower.0);
lower.1 + t * (upper.1 - lower.1)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ColorMode {
YCbCr { subsampling: ChromaSubsampling },
Xyb { subsampling: XybSubsampling },
Grayscale,
}
impl Default for ColorMode {
fn default() -> Self {
ColorMode::YCbCr {
subsampling: ChromaSubsampling::None, }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ChromaSubsampling {
None,
HalfHorizontal,
Quarter,
HalfVertical,
}
impl ChromaSubsampling {
#[must_use]
pub const fn h_factor(&self) -> u8 {
match self {
ChromaSubsampling::None | ChromaSubsampling::HalfVertical => 1,
ChromaSubsampling::HalfHorizontal | ChromaSubsampling::Quarter => 2,
}
}
#[must_use]
pub const fn v_factor(&self) -> u8 {
match self {
ChromaSubsampling::None | ChromaSubsampling::HalfHorizontal => 1,
ChromaSubsampling::HalfVertical | ChromaSubsampling::Quarter => 2,
}
}
#[must_use]
pub const fn h_samp_factor_luma(self) -> u8 {
self.h_factor()
}
#[must_use]
pub const fn v_samp_factor_luma(self) -> u8 {
self.v_factor()
}
#[must_use]
pub const fn mcu_size(self) -> usize {
match self {
ChromaSubsampling::None => 8,
ChromaSubsampling::Quarter
| ChromaSubsampling::HalfHorizontal
| ChromaSubsampling::HalfVertical => 16,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum XybSubsampling {
Full,
#[default]
BQuarter,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DownsamplingMethod {
#[default]
Box,
GammaAware,
GammaAwareIterative,
}
impl DownsamplingMethod {
#[must_use]
pub const fn uses_gamma_aware(self) -> bool {
matches!(self, Self::GammaAware | Self::GammaAwareIterative)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PixelLayout {
Rgb8Srgb,
Bgr8Srgb,
Rgbx8Srgb,
Bgrx8Srgb,
Gray8Srgb,
Rgb16Linear,
Rgbx16Linear,
Gray16Linear,
RgbF32Linear,
RgbxF32Linear,
GrayF32Linear,
YCbCr8,
YCbCrF32,
}
impl PixelLayout {
#[must_use]
pub const fn bytes_per_pixel(&self) -> usize {
match self {
Self::Gray8Srgb => 1,
Self::Gray16Linear => 2,
Self::Rgb8Srgb | Self::Bgr8Srgb | Self::YCbCr8 => 3,
Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::GrayF32Linear => 4,
Self::Rgb16Linear => 6,
Self::Rgbx16Linear => 8,
Self::RgbF32Linear | Self::YCbCrF32 => 12,
Self::RgbxF32Linear => 16,
}
}
#[must_use]
pub const fn channels(&self) -> usize {
match self {
Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear => 1,
Self::Rgb8Srgb
| Self::Bgr8Srgb
| Self::Rgb16Linear
| Self::RgbF32Linear
| Self::YCbCr8
| Self::YCbCrF32 => 3,
Self::Rgbx8Srgb | Self::Bgrx8Srgb | Self::Rgbx16Linear | Self::RgbxF32Linear => 4,
}
}
#[must_use]
pub const fn is_grayscale(&self) -> bool {
matches!(
self,
Self::Gray8Srgb | Self::Gray16Linear | Self::GrayF32Linear
)
}
#[must_use]
pub const fn is_ycbcr(&self) -> bool {
matches!(self, Self::YCbCr8 | Self::YCbCrF32)
}
#[must_use]
pub const fn is_bgr(&self) -> bool {
matches!(self, Self::Bgr8Srgb | Self::Bgrx8Srgb)
}
#[must_use]
pub const fn is_float(&self) -> bool {
matches!(
self,
Self::RgbF32Linear | Self::RgbxF32Linear | Self::GrayF32Linear | Self::YCbCrF32
)
}
#[must_use]
pub const fn is_16bit(&self) -> bool {
matches!(
self,
Self::Rgb16Linear | Self::Rgbx16Linear | Self::Gray16Linear
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct YCbCrPlanes<'a> {
pub y: &'a [f32],
pub y_stride: usize,
pub cb: &'a [f32],
pub cb_stride: usize,
pub cr: &'a [f32],
pub cr_stride: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quality_default() {
let q = Quality::default();
assert!(matches!(q, Quality::ApproxJpegli(90.0)));
}
#[test]
fn test_quality_from() {
let q: Quality = 85.0.into();
assert!(matches!(q, Quality::ApproxJpegli(85.0)));
let q: Quality = 75u8.into();
assert!(matches!(q, Quality::ApproxJpegli(75.0)));
}
#[test]
fn test_pixel_layout_bytes() {
assert_eq!(PixelLayout::Rgb8Srgb.bytes_per_pixel(), 3);
assert_eq!(PixelLayout::Rgbx8Srgb.bytes_per_pixel(), 4);
assert_eq!(PixelLayout::RgbF32Linear.bytes_per_pixel(), 12);
assert_eq!(PixelLayout::Gray8Srgb.bytes_per_pixel(), 1);
}
#[test]
fn test_chroma_subsampling_factors() {
assert_eq!(ChromaSubsampling::None.h_factor(), 1);
assert_eq!(ChromaSubsampling::None.v_factor(), 1);
assert_eq!(ChromaSubsampling::Quarter.h_factor(), 2);
assert_eq!(ChromaSubsampling::Quarter.v_factor(), 2);
assert_eq!(ChromaSubsampling::HalfHorizontal.h_factor(), 2);
assert_eq!(ChromaSubsampling::HalfHorizontal.v_factor(), 1);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
#[cfg(feature = "parallel")]
pub enum ParallelEncoding {
#[default]
Auto,
}