pub use crate::entropy_coding::Lz77Method;
pub use enough::{Stop, Unstoppable};
pub use whereat::{At, ResultAtExt, at};
#[derive(Debug)]
#[non_exhaustive]
pub enum EncodeError {
InvalidInput { message: String },
InvalidConfig { message: String },
UnsupportedPixelLayout(PixelLayout),
LimitExceeded { message: String },
Cancelled,
Oom(std::collections::TryReserveError),
#[cfg(feature = "std")]
Io(std::io::Error),
Internal { message: String },
}
impl core::fmt::Display for EncodeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidInput { message } => write!(f, "invalid input: {message}"),
Self::InvalidConfig { message } => write!(f, "invalid config: {message}"),
Self::UnsupportedPixelLayout(layout) => {
write!(f, "unsupported pixel layout: {layout:?}")
}
Self::LimitExceeded { message } => write!(f, "limit exceeded: {message}"),
Self::Cancelled => write!(f, "encoding cancelled"),
Self::Oom(e) => write!(f, "out of memory: {e}"),
#[cfg(feature = "std")]
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::Internal { message } => write!(f, "internal error: {message}"),
}
}
}
impl core::error::Error for EncodeError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Oom(e) => Some(e),
#[cfg(feature = "std")]
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<crate::error::Error> for EncodeError {
fn from(e: crate::error::Error) -> Self {
match e {
crate::error::Error::InvalidImageDimensions(w, h) => Self::InvalidInput {
message: format!("invalid dimensions: {w}x{h}"),
},
crate::error::Error::ImageTooLarge(w, h, mw, mh) => Self::LimitExceeded {
message: format!("image {w}x{h} exceeds max {mw}x{mh}"),
},
crate::error::Error::DimensionOverflow {
width,
height,
channels,
} => Self::InvalidInput {
message: format!("dimension overflow: {width}x{height}x{channels} exceeds usize"),
},
crate::error::Error::InvalidInput(msg) => Self::InvalidInput { message: msg },
crate::error::Error::OutOfMemory(e) => Self::Oom(e),
#[cfg(feature = "std")]
crate::error::Error::IoError(e) => Self::Io(e),
crate::error::Error::Cancelled => Self::Cancelled,
other => Self::Internal {
message: format!("{other}"),
},
}
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for EncodeError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<enough::StopReason> for EncodeError {
fn from(_: enough::StopReason) -> Self {
Self::Cancelled
}
}
pub type Result<T> = core::result::Result<T, At<EncodeError>>;
#[derive(Clone, Debug)]
pub struct EncodeResult {
data: Option<Vec<u8>>,
stats: EncodeStats,
}
impl EncodeResult {
pub fn data(&self) -> Option<&[u8]> {
self.data.as_deref()
}
pub fn take_data(&mut self) -> Option<Vec<u8>> {
self.data.take()
}
pub fn stats(&self) -> &EncodeStats {
&self.stats
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct EncodeStats {
codestream_size: usize,
output_size: usize,
mode: EncodeMode,
strategy_counts: [u32; 19],
gaborish: bool,
ans: bool,
butteraugli_iters: u32,
pixel_domain_loss: bool,
}
impl EncodeStats {
pub fn codestream_size(&self) -> usize {
self.codestream_size
}
pub fn output_size(&self) -> usize {
self.output_size
}
pub fn mode(&self) -> EncodeMode {
self.mode
}
pub fn strategy_counts(&self) -> &[u32; 19] {
&self.strategy_counts
}
pub fn gaborish(&self) -> bool {
self.gaborish
}
pub fn ans(&self) -> bool {
self.ans
}
pub fn butteraugli_iters(&self) -> u32 {
self.butteraugli_iters
}
pub fn pixel_domain_loss(&self) -> bool {
self.pixel_domain_loss
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EncodeMode {
#[default]
Lossy,
Lossless,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PixelLayout {
Rgb8,
Rgba8,
Bgr8,
Bgra8,
Gray8,
GrayAlpha8,
Rgb16,
Rgba16,
Gray16,
GrayAlpha16,
RgbLinearF32,
RgbaLinearF32,
GrayLinearF32,
GrayAlphaLinearF32,
}
impl PixelLayout {
pub const fn bytes_per_pixel(self) -> usize {
match self {
Self::Rgb8 | Self::Bgr8 => 3,
Self::Rgba8 | Self::Bgra8 => 4,
Self::Gray8 => 1,
Self::GrayAlpha8 => 2,
Self::Rgb16 => 6,
Self::Rgba16 => 8,
Self::Gray16 => 2,
Self::GrayAlpha16 => 4,
Self::RgbLinearF32 => 12,
Self::RgbaLinearF32 => 16,
Self::GrayLinearF32 => 4,
Self::GrayAlphaLinearF32 => 8,
}
}
pub const fn is_linear(self) -> bool {
matches!(
self,
Self::RgbLinearF32
| Self::RgbaLinearF32
| Self::GrayLinearF32
| Self::GrayAlphaLinearF32
)
}
pub const fn is_16bit(self) -> bool {
matches!(
self,
Self::Rgb16 | Self::Rgba16 | Self::Gray16 | Self::GrayAlpha16
)
}
pub const fn is_f32(self) -> bool {
matches!(
self,
Self::RgbLinearF32
| Self::RgbaLinearF32
| Self::GrayLinearF32
| Self::GrayAlphaLinearF32
)
}
pub const fn has_alpha(self) -> bool {
matches!(
self,
Self::Rgba8
| Self::Bgra8
| Self::GrayAlpha8
| Self::Rgba16
| Self::GrayAlpha16
| Self::RgbaLinearF32
| Self::GrayAlphaLinearF32
)
}
pub const fn is_grayscale(self) -> bool {
matches!(
self,
Self::Gray8
| Self::GrayAlpha8
| Self::Gray16
| Self::GrayAlpha16
| Self::GrayLinearF32
| Self::GrayAlphaLinearF32
)
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Quality {
Distance(f32),
Percent(u32),
}
impl Quality {
fn to_distance(self) -> core::result::Result<f32, EncodeError> {
match self {
Self::Distance(d) => {
if d <= 0.0 {
return Err(EncodeError::InvalidConfig {
message: format!("lossy distance must be > 0.0, got {d}"),
});
}
Ok(d)
}
Self::Percent(q) => {
if q >= 100 {
return Err(EncodeError::InvalidConfig {
message: "quality 100 is lossless; use LosslessConfig instead".into(),
});
}
Ok(percent_to_distance(q))
}
}
}
}
fn percent_to_distance(quality: u32) -> f32 {
if quality >= 100 {
0.0
} else if quality >= 90 {
(100 - quality) as f32 / 10.0
} else if quality >= 70 {
1.0 + (90 - quality) as f32 / 20.0
} else {
2.0 + (70 - quality) as f32 / 10.0
}
}
#[must_use]
pub fn quality_to_distance(quality: f32) -> f32 {
let q = quality.clamp(0.0, 100.0);
if q >= 100.0 {
0.0
} else if q >= 90.0 {
(100.0 - q) / 10.0
} else if q >= 70.0 {
1.0 + (90.0 - q) / 20.0
} else {
2.0 + (70.0 - q) / 10.0
}
}
#[must_use]
pub fn calibrated_jxl_quality(generic_q: f32) -> f32 {
let clamped = generic_q.clamp(0.0, 100.0);
const TABLE: &[(f32, f32)] = &[
(5.0, 5.0),
(10.0, 5.0),
(15.0, 5.0),
(20.0, 5.0),
(25.0, 9.3),
(30.0, 22.7),
(35.0, 33.0),
(40.0, 38.8),
(45.0, 43.8),
(50.0, 48.5),
(55.0, 51.9),
(60.0, 55.1),
(65.0, 58.0),
(70.0, 61.3),
(72.0, 63.2),
(75.0, 65.5),
(78.0, 67.9),
(80.0, 69.1),
(82.0, 71.8),
(85.0, 76.1),
(87.0, 79.3),
(90.0, 84.2),
(92.0, 86.9),
(95.0, 91.2),
(97.0, 92.8),
(99.0, 93.8),
];
interp_quality(TABLE, clamped)
}
fn interp_quality(table: &[(f32, f32)], x: f32) -> f32 {
if x <= table[0].0 {
return table[0].1;
}
if x >= table[table.len() - 1].0 {
return table[table.len() - 1].1;
}
for i in 1..table.len() {
if x <= table[i].0 {
let (x0, y0) = table[i - 1];
let (x1, y1) = table[i];
let t = (x - x0) / (x1 - x0);
return y0 + t * (y1 - y0);
}
}
table[table.len() - 1].1
}
#[derive(Clone, Debug, Default)]
pub struct ImageMetadata<'a> {
icc_profile: Option<&'a [u8]>,
exif: Option<&'a [u8]>,
xmp: Option<&'a [u8]>,
intensity_target: Option<f32>,
min_nits: Option<f32>,
intrinsic_size: Option<(u32, u32)>,
}
impl<'a> ImageMetadata<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_icc_profile(mut self, data: &'a [u8]) -> Self {
self.icc_profile = Some(data);
self
}
pub fn with_exif(mut self, data: &'a [u8]) -> Self {
self.exif = Some(data);
self
}
pub fn with_xmp(mut self, data: &'a [u8]) -> Self {
self.xmp = Some(data);
self
}
pub fn icc_profile(&self) -> Option<&[u8]> {
self.icc_profile
}
pub fn exif(&self) -> Option<&[u8]> {
self.exif
}
pub fn xmp(&self) -> Option<&[u8]> {
self.xmp
}
pub fn with_intensity_target(mut self, nits: f32) -> Self {
self.intensity_target = Some(nits);
self
}
pub fn with_min_nits(mut self, nits: f32) -> Self {
self.min_nits = Some(nits);
self
}
pub fn intensity_target(&self) -> Option<f32> {
self.intensity_target
}
pub fn min_nits(&self) -> Option<f32> {
self.min_nits
}
pub fn with_intrinsic_size(mut self, width: u32, height: u32) -> Self {
self.intrinsic_size = Some((width, height));
self
}
pub fn intrinsic_size(&self) -> Option<(u32, u32)> {
self.intrinsic_size
}
}
#[derive(Clone, Debug, Default)]
pub struct Limits {
max_width: Option<u64>,
max_height: Option<u64>,
max_pixels: Option<u64>,
max_memory_bytes: Option<u64>,
}
impl Limits {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_width(mut self, w: u64) -> Self {
self.max_width = Some(w);
self
}
pub fn with_max_height(mut self, h: u64) -> Self {
self.max_height = Some(h);
self
}
pub fn with_max_pixels(mut self, p: u64) -> Self {
self.max_pixels = Some(p);
self
}
pub fn with_max_memory_bytes(mut self, bytes: u64) -> Self {
self.max_memory_bytes = Some(bytes);
self
}
pub fn max_width(&self) -> Option<u64> {
self.max_width
}
pub fn max_height(&self) -> Option<u64> {
self.max_height
}
pub fn max_pixels(&self) -> Option<u64> {
self.max_pixels
}
pub fn max_memory_bytes(&self) -> Option<u64> {
self.max_memory_bytes
}
}
#[derive(Clone, Debug)]
pub struct AnimationParams {
pub tps_numerator: u32,
pub tps_denominator: u32,
pub num_loops: u32,
}
impl Default for AnimationParams {
fn default() -> Self {
Self {
tps_numerator: 100,
tps_denominator: 1,
num_loops: 0,
}
}
}
pub struct AnimationFrame<'a> {
pub pixels: &'a [u8],
pub duration: u32,
}
#[derive(Clone, Debug)]
pub struct LosslessConfig {
effort: u8,
mode: EncoderMode,
use_ans: bool,
squeeze: bool,
tree_learning: bool,
lz77: bool,
lz77_method: Lz77Method,
patches: bool,
lossy_palette: bool,
threads: usize,
}
impl Default for LosslessConfig {
fn default() -> Self {
Self::with_effort_level(7)
}
}
impl LosslessConfig {
fn with_effort_level(effort: u8) -> Self {
let profile = crate::effort::EffortProfile::lossless(effort, EncoderMode::Reference);
Self {
effort: profile.effort,
mode: EncoderMode::Reference,
use_ans: profile.use_ans,
tree_learning: profile.tree_learning,
squeeze: false, lz77: profile.lz77,
lz77_method: profile.lz77_method,
patches: profile.patches,
lossy_palette: false,
threads: 0,
}
}
pub fn new() -> Self {
Self::default()
}
pub fn with_effort(self, effort: u8) -> Self {
let mut new = Self::with_effort_level(effort);
new.mode = self.mode;
new.squeeze = self.squeeze;
new
}
pub fn with_mode(mut self, mode: EncoderMode) -> Self {
self.mode = mode;
self
}
pub fn mode(&self) -> EncoderMode {
self.mode
}
pub fn with_patches(mut self, enable: bool) -> Self {
self.patches = enable;
self
}
pub fn with_ans(mut self, enable: bool) -> Self {
self.use_ans = enable;
self
}
pub fn with_squeeze(mut self, enable: bool) -> Self {
self.squeeze = enable;
self
}
pub fn with_tree_learning(mut self, enable: bool) -> Self {
self.tree_learning = enable;
self
}
pub fn with_lz77(mut self, enable: bool) -> Self {
self.lz77 = enable;
self
}
pub fn with_lz77_method(mut self, method: Lz77Method) -> Self {
self.lz77_method = method;
self
}
pub fn with_lossy_palette(mut self, enable: bool) -> Self {
self.lossy_palette = enable;
self
}
pub fn with_threads(mut self, threads: usize) -> Self {
self.threads = threads;
self
}
pub fn effort(&self) -> u8 {
self.effort
}
pub fn ans(&self) -> bool {
self.use_ans
}
pub fn squeeze(&self) -> bool {
self.squeeze
}
pub fn tree_learning(&self) -> bool {
self.tree_learning
}
pub fn lz77(&self) -> bool {
self.lz77
}
pub fn lz77_method(&self) -> Lz77Method {
self.lz77_method
}
pub fn patches(&self) -> bool {
self.patches
}
pub fn lossy_palette(&self) -> bool {
self.lossy_palette
}
pub fn threads(&self) -> usize {
self.threads
}
pub fn encode_request(
&self,
width: u32,
height: u32,
layout: PixelLayout,
) -> EncodeRequest<'_> {
EncodeRequest {
config: ConfigRef::Lossless(self),
width,
height,
layout,
metadata: None,
limits: None,
stop: None,
source_gamma: None,
color_encoding: None,
}
}
#[track_caller]
pub fn encode(
&self,
pixels: &[u8],
width: u32,
height: u32,
layout: PixelLayout,
) -> Result<Vec<u8>> {
self.encode_request(width, height, layout).encode(pixels)
}
#[track_caller]
pub fn encode_into(
&self,
pixels: &[u8],
width: u32,
height: u32,
layout: PixelLayout,
out: &mut Vec<u8>,
) -> Result<()> {
self.encode_request(width, height, layout)
.encode_into(pixels, out)
.map(|_| ())
}
#[track_caller]
pub fn encode_animation(
&self,
width: u32,
height: u32,
layout: PixelLayout,
animation: &AnimationParams,
frames: &[AnimationFrame<'_>],
) -> Result<Vec<u8>> {
encode_animation_lossless(self, width, height, layout, animation, frames).map_err(at)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum EncoderMode {
#[default]
Reference,
Experimental,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ProgressiveMode {
#[default]
Single,
QuantizedAcFullAc,
DcVlfLfAc,
}
#[derive(Clone, Debug)]
pub struct LossyConfig {
distance: f32,
effort: u8,
mode: EncoderMode,
use_ans: bool,
gaborish: bool,
noise: bool,
denoise: bool,
error_diffusion: bool,
pixel_domain_loss: bool,
lz77: bool,
lz77_method: Lz77Method,
force_strategy: Option<u8>,
max_strategy_size: Option<u8>,
patches: bool,
splines: Option<Vec<crate::vardct::splines::Spline>>,
progressive: ProgressiveMode,
lf_frame: bool,
#[cfg(feature = "butteraugli-loop")]
butteraugli_iters: u32,
#[cfg(feature = "butteraugli-loop")]
butteraugli_iters_explicit: bool,
#[cfg(feature = "ssim2-loop")]
ssim2_iters: u32,
#[cfg(feature = "zensim-loop")]
zensim_iters: u32,
threads: usize,
}
impl LossyConfig {
pub fn new(distance: f32) -> Self {
Self::new_with_effort(distance, 7)
}
fn new_with_effort(distance: f32, effort: u8) -> Self {
let profile = crate::effort::EffortProfile::lossy(effort, EncoderMode::Reference);
Self {
distance,
effort: profile.effort,
mode: EncoderMode::Reference,
use_ans: profile.use_ans,
gaborish: profile.gaborish,
noise: false,
denoise: false,
error_diffusion: profile.error_diffusion,
pixel_domain_loss: profile.pixel_domain_loss,
lz77: profile.lz77,
lz77_method: profile.lz77_method,
force_strategy: None,
max_strategy_size: None,
patches: profile.patches,
splines: None,
progressive: ProgressiveMode::Single,
lf_frame: false,
#[cfg(feature = "butteraugli-loop")]
butteraugli_iters: profile.butteraugli_iters,
#[cfg(feature = "butteraugli-loop")]
butteraugli_iters_explicit: false,
#[cfg(feature = "ssim2-loop")]
ssim2_iters: 0,
#[cfg(feature = "zensim-loop")]
zensim_iters: 0,
threads: 0,
}
}
pub fn from_quality(quality: Quality) -> core::result::Result<Self, EncodeError> {
let distance = quality.to_distance()?;
Ok(Self::new(distance))
}
pub fn with_effort(self, effort: u8) -> Self {
let mut new = Self::new_with_effort(self.distance, effort);
new.mode = self.mode;
new.noise = self.noise;
new.denoise = self.denoise;
new.force_strategy = self.force_strategy;
new.max_strategy_size = self.max_strategy_size;
new.splines = self.splines;
new.progressive = self.progressive;
#[cfg(feature = "butteraugli-loop")]
if self.butteraugli_iters_explicit {
new.butteraugli_iters = self.butteraugli_iters;
new.butteraugli_iters_explicit = true;
}
#[cfg(feature = "ssim2-loop")]
{
new.ssim2_iters = self.ssim2_iters;
}
#[cfg(feature = "zensim-loop")]
{
new.zensim_iters = self.zensim_iters;
}
new
}
pub fn with_mode(mut self, mode: EncoderMode) -> Self {
self.mode = mode;
self
}
pub fn mode(&self) -> EncoderMode {
self.mode
}
pub fn with_ans(mut self, enable: bool) -> Self {
self.use_ans = enable;
self
}
pub fn with_gaborish(mut self, enable: bool) -> Self {
self.gaborish = enable;
self
}
pub fn with_noise(mut self, enable: bool) -> Self {
self.noise = enable;
self
}
pub fn with_denoise(mut self, enable: bool) -> Self {
self.denoise = enable;
if enable {
self.noise = true;
}
self
}
pub fn with_error_diffusion(mut self, enable: bool) -> Self {
self.error_diffusion = enable;
self
}
pub fn with_pixel_domain_loss(mut self, enable: bool) -> Self {
self.pixel_domain_loss = enable;
self
}
pub fn with_lz77(mut self, enable: bool) -> Self {
self.lz77 = enable;
self
}
pub fn with_lz77_method(mut self, method: Lz77Method) -> Self {
self.lz77_method = method;
self
}
pub fn with_force_strategy(mut self, strategy: Option<u8>) -> Self {
self.force_strategy = strategy;
self
}
pub fn with_max_strategy_size(mut self, size: Option<u8>) -> Self {
self.max_strategy_size = size;
self
}
pub fn with_patches(mut self, enable: bool) -> Self {
self.patches = enable;
self
}
pub fn with_splines(mut self, splines: Vec<crate::vardct::splines::Spline>) -> Self {
self.splines = Some(splines);
self
}
pub fn with_progressive(mut self, mode: ProgressiveMode) -> Self {
self.progressive = mode;
self
}
pub fn with_lf_frame(mut self, enable: bool) -> Self {
self.lf_frame = enable;
self
}
#[cfg(feature = "butteraugli-loop")]
pub fn with_butteraugli_iters(mut self, n: u32) -> Self {
self.butteraugli_iters = n;
self.butteraugli_iters_explicit = true;
self
}
#[cfg(feature = "ssim2-loop")]
pub fn with_ssim2_iters(mut self, n: u32) -> Self {
self.ssim2_iters = n;
self
}
#[cfg(feature = "zensim-loop")]
pub fn with_zensim_iters(mut self, n: u32) -> Self {
self.zensim_iters = n;
self
}
pub fn with_threads(mut self, threads: usize) -> Self {
self.threads = threads;
self
}
pub fn distance(&self) -> f32 {
self.distance
}
pub fn effort(&self) -> u8 {
self.effort
}
pub fn ans(&self) -> bool {
self.use_ans
}
pub fn gaborish(&self) -> bool {
self.gaborish
}
pub fn noise(&self) -> bool {
self.noise
}
pub fn denoise(&self) -> bool {
self.denoise
}
pub fn error_diffusion(&self) -> bool {
self.error_diffusion
}
pub fn pixel_domain_loss(&self) -> bool {
self.pixel_domain_loss
}
pub fn lz77(&self) -> bool {
self.lz77
}
pub fn lz77_method(&self) -> Lz77Method {
self.lz77_method
}
pub fn force_strategy(&self) -> Option<u8> {
self.force_strategy
}
pub fn max_strategy_size(&self) -> Option<u8> {
self.max_strategy_size
}
pub fn progressive(&self) -> ProgressiveMode {
self.progressive
}
pub fn lf_frame(&self) -> bool {
self.lf_frame
}
#[cfg(feature = "butteraugli-loop")]
pub fn butteraugli_iters(&self) -> u32 {
self.butteraugli_iters
}
pub fn threads(&self) -> usize {
self.threads
}
pub fn encode_request(
&self,
width: u32,
height: u32,
layout: PixelLayout,
) -> EncodeRequest<'_> {
EncodeRequest {
config: ConfigRef::Lossy(self),
width,
height,
layout,
metadata: None,
limits: None,
stop: None,
source_gamma: None,
color_encoding: None,
}
}
#[track_caller]
pub fn encode(
&self,
pixels: &[u8],
width: u32,
height: u32,
layout: PixelLayout,
) -> Result<Vec<u8>> {
self.encode_request(width, height, layout).encode(pixels)
}
#[track_caller]
pub fn encode_into(
&self,
pixels: &[u8],
width: u32,
height: u32,
layout: PixelLayout,
out: &mut Vec<u8>,
) -> Result<()> {
self.encode_request(width, height, layout)
.encode_into(pixels, out)
.map(|_| ())
}
#[track_caller]
pub fn encode_animation(
&self,
width: u32,
height: u32,
layout: PixelLayout,
animation: &AnimationParams,
frames: &[AnimationFrame<'_>],
) -> Result<Vec<u8>> {
encode_animation_lossy(self, width, height, layout, animation, frames).map_err(at)
}
}
#[derive(Clone, Copy, Debug)]
enum ConfigRef<'a> {
Lossless(&'a LosslessConfig),
Lossy(&'a LossyConfig),
}
pub struct EncodeRequest<'a> {
config: ConfigRef<'a>,
width: u32,
height: u32,
layout: PixelLayout,
metadata: Option<&'a ImageMetadata<'a>>,
limits: Option<&'a Limits>,
stop: Option<&'a dyn Stop>,
source_gamma: Option<f32>,
color_encoding: Option<crate::headers::color_encoding::ColorEncoding>,
}
impl<'a> EncodeRequest<'a> {
pub fn with_metadata(mut self, meta: &'a ImageMetadata<'a>) -> Self {
self.metadata = Some(meta);
self
}
pub fn with_limits(mut self, limits: &'a Limits) -> Self {
self.limits = Some(limits);
self
}
pub fn with_stop(mut self, stop: &'a dyn Stop) -> Self {
self.stop = Some(stop);
self
}
pub fn with_source_gamma(mut self, gamma: f32) -> Self {
self.source_gamma = Some(gamma);
self
}
pub fn with_color_encoding(
mut self,
ce: crate::headers::color_encoding::ColorEncoding,
) -> Self {
self.color_encoding = Some(ce);
self
}
#[track_caller]
pub fn encode(self, pixels: &[u8]) -> Result<Vec<u8>> {
self.encode_inner(pixels)
.map(|mut r| r.take_data().unwrap())
.map_err(at)
}
#[track_caller]
pub fn encode_with_stats(self, pixels: &[u8]) -> Result<EncodeResult> {
self.encode_inner(pixels).map_err(at)
}
#[track_caller]
pub fn encode_into(self, pixels: &[u8], out: &mut Vec<u8>) -> Result<EncodeResult> {
let mut result = self.encode_inner(pixels).map_err(at)?;
if let Some(data) = result.data.take() {
out.extend_from_slice(&data);
}
Ok(result)
}
#[cfg(feature = "std")]
#[track_caller]
pub fn encode_to(self, pixels: &[u8], mut dest: impl std::io::Write) -> Result<EncodeResult> {
let mut result = self.encode_inner(pixels).map_err(at)?;
if let Some(data) = result.data.take() {
dest.write_all(&data)
.map_err(|e| at(EncodeError::from(e)))?;
}
Ok(result)
}
fn encode_inner(&self, pixels: &[u8]) -> core::result::Result<EncodeResult, EncodeError> {
self.validate_pixels(pixels)?;
self.check_limits()?;
let threads = match self.config {
ConfigRef::Lossless(cfg) => cfg.threads,
ConfigRef::Lossy(cfg) => cfg.threads,
};
let (codestream, mut stats) = run_with_threads(threads, || match self.config {
ConfigRef::Lossless(cfg) => self.encode_lossless(cfg, pixels),
ConfigRef::Lossy(cfg) => self.encode_lossy(cfg, pixels),
})?;
stats.codestream_size = codestream.len();
let output = if let Some(meta) = self.metadata
&& (meta.exif.is_some() || meta.xmp.is_some())
{
crate::container::wrap_in_container(&codestream, meta.exif, meta.xmp)
} else {
codestream
};
stats.output_size = output.len();
Ok(EncodeResult {
data: Some(output),
stats,
})
}
fn validate_pixels(&self, pixels: &[u8]) -> core::result::Result<(), EncodeError> {
let w = self.width as usize;
let h = self.height as usize;
if w == 0 || h == 0 {
return Err(EncodeError::InvalidInput {
message: format!("zero dimensions: {w}x{h}"),
});
}
const MAX_JXL_DIM: u32 = 1 << 30;
if self.width > MAX_JXL_DIM || self.height > MAX_JXL_DIM {
return Err(EncodeError::LimitExceeded {
message: format!(
"image {}x{} exceeds JXL spec maximum of {MAX_JXL_DIM} per dimension",
self.width, self.height
),
});
}
let expected = w
.checked_mul(h)
.and_then(|n| n.checked_mul(self.layout.bytes_per_pixel()));
match expected {
Some(expected) if pixels.len() == expected => Ok(()),
Some(expected) => Err(EncodeError::InvalidInput {
message: format!(
"pixel buffer size mismatch: expected {expected} bytes for {w}x{h} {:?}, got {}",
self.layout,
pixels.len()
),
}),
None => Err(EncodeError::InvalidInput {
message: "image dimensions overflow".into(),
}),
}
}
fn check_limits(&self) -> core::result::Result<(), EncodeError> {
let Some(limits) = self.limits else {
return Ok(());
};
let w = self.width as u64;
let h = self.height as u64;
if let Some(max_w) = limits.max_width
&& w > max_w
{
return Err(EncodeError::LimitExceeded {
message: format!("width {w} > max {max_w}"),
});
}
if let Some(max_h) = limits.max_height
&& h > max_h
{
return Err(EncodeError::LimitExceeded {
message: format!("height {h} > max {max_h}"),
});
}
if let Some(max_px) = limits.max_pixels
&& w * h > max_px
{
return Err(EncodeError::LimitExceeded {
message: format!("pixels {}x{} = {} > max {max_px}", w, h, w * h),
});
}
if let Some(max_mem) = limits.max_memory_bytes {
let estimated = w.saturating_mul(h).saturating_mul(40);
if estimated > max_mem {
return Err(EncodeError::LimitExceeded {
message: format!(
"estimated memory {estimated} bytes > max {max_mem} bytes \
(for {w}x{h} image)"
),
});
}
}
Ok(())
}
fn encode_lossless(
&self,
cfg: &LosslessConfig,
pixels: &[u8],
) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
use crate::bit_writer::BitWriter;
use crate::headers::color_encoding::ColorSpace;
use crate::headers::{ColorEncoding, FileHeader};
use crate::modular::channel::ModularImage;
use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
let w = self.width as usize;
let h = self.height as usize;
let rgb_pixels;
let detection_pixels: &[u8] = match self.layout {
PixelLayout::Bgr8 => {
rgb_pixels = bgr_to_rgb(pixels, 3);
&rgb_pixels
}
PixelLayout::Bgra8 => {
rgb_pixels = bgr_to_rgb(pixels, 4);
&rgb_pixels
}
_ => {
rgb_pixels = Vec::new();
let _ = &rgb_pixels;
pixels
}
};
let mut image = match self.layout {
PixelLayout::Rgb8 => ModularImage::from_rgb8(pixels, w, h),
PixelLayout::Rgba8 => ModularImage::from_rgba8(pixels, w, h),
PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(pixels, 3), w, h),
PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(pixels, 4), w, h),
PixelLayout::Gray8 => ModularImage::from_gray8(pixels, w, h),
PixelLayout::GrayAlpha8 => ModularImage::from_grayalpha8(pixels, w, h),
PixelLayout::Rgb16 => ModularImage::from_rgb16_native(pixels, w, h),
PixelLayout::Rgba16 => ModularImage::from_rgba16_native(pixels, w, h),
PixelLayout::Gray16 => ModularImage::from_gray16_native(pixels, w, h),
PixelLayout::GrayAlpha16 => ModularImage::from_grayalpha16_native(pixels, w, h),
other => return Err(EncodeError::UnsupportedPixelLayout(other)),
}
.map_err(EncodeError::from)?;
let num_channels = self.layout.bytes_per_pixel();
let can_use_patches =
cfg.patches && !image.is_grayscale && image.bit_depth <= 8 && num_channels >= 3;
let patches_data = if can_use_patches {
crate::vardct::patches::find_and_build_lossless(
detection_pixels,
w,
h,
num_channels,
image.bit_depth,
)
} else {
None
};
let mut file_header = if image.is_grayscale {
FileHeader::new_gray(self.width, self.height)
} else if image.has_alpha {
FileHeader::new_rgba(self.width, self.height)
} else {
FileHeader::new_rgb(self.width, self.height)
};
if image.bit_depth == 16 {
file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
for ec in &mut file_header.metadata.extra_channels {
ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
}
}
if let Some(meta) = self.metadata {
if meta.icc_profile.is_some() {
file_header.metadata.color_encoding.want_icc = true;
}
if let Some(it) = meta.intensity_target {
file_header.metadata.intensity_target = it;
}
if let Some(mn) = meta.min_nits {
file_header.metadata.min_nits = mn;
}
if let Some((w, h)) = meta.intrinsic_size {
file_header.metadata.have_intrinsic_size = true;
file_header.metadata.intrinsic_width = w;
file_header.metadata.intrinsic_height = h;
}
}
let mut writer = BitWriter::new();
file_header.write(&mut writer).map_err(EncodeError::from)?;
if let Some(meta) = self.metadata
&& let Some(icc) = meta.icc_profile
{
crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
}
writer.zero_pad_to_byte();
if let Some(ref pd) = patches_data {
let lossless_profile = crate::effort::EffortProfile::lossless(cfg.effort, cfg.mode);
crate::vardct::patches::encode_reference_frame_rgb(
pd,
image.bit_depth,
cfg.use_ans,
lossless_profile.patch_ref_tree_learning,
&mut writer,
)
.map_err(EncodeError::from)?;
writer.zero_pad_to_byte();
let bd = image.bit_depth;
crate::vardct::patches::subtract_patches_modular(&mut image, pd, bd);
}
let use_tree_learning = cfg.tree_learning;
let frame_encoder = FrameEncoder::new(
w,
h,
FrameEncoderOptions {
use_modular: true,
effort: cfg.effort,
use_ans: cfg.use_ans,
use_tree_learning,
use_squeeze: cfg.squeeze,
enable_lz77: cfg.lz77,
lz77_method: cfg.lz77_method,
lossy_palette: cfg.lossy_palette,
encoder_mode: cfg.mode,
profile: crate::effort::EffortProfile::lossless(cfg.effort, cfg.mode),
have_animation: false,
duration: 0,
is_last: true,
crop: None,
skip_rct: false,
},
);
let color_encoding = if let Some(ce) = self.color_encoding.clone() {
if image.is_grayscale && ce.color_space != ColorSpace::Gray {
ColorEncoding {
color_space: ColorSpace::Gray,
..ce
}
} else {
ce
}
} else if let Some(gamma) = self.source_gamma {
if image.is_grayscale {
ColorEncoding::gray_with_gamma(gamma)
} else {
ColorEncoding::with_gamma(gamma)
}
} else if image.is_grayscale {
ColorEncoding::gray()
} else {
ColorEncoding::srgb()
};
frame_encoder
.encode_modular_with_patches(
&image,
&color_encoding,
&mut writer,
patches_data.as_ref(),
)
.map_err(EncodeError::from)?;
let stats = EncodeStats {
mode: EncodeMode::Lossless,
ans: cfg.use_ans,
..Default::default()
};
Ok((writer.finish_with_padding(), stats))
}
fn encode_lossy(
&self,
cfg: &LossyConfig,
pixels: &[u8],
) -> core::result::Result<(Vec<u8>, EncodeStats), EncodeError> {
let w = self.width as usize;
let h = self.height as usize;
let gamma = self.source_gamma;
let (linear_rgb, alpha, bit_depth_16) = match self.layout {
PixelLayout::Rgb8 => {
let linear = if let Some(g) = gamma {
gamma_u8_to_linear_f32(pixels, 3, g)
} else {
srgb_u8_to_linear_f32(pixels, 3)
};
(linear, None, false)
}
PixelLayout::Bgr8 => {
let rgb = bgr_to_rgb(pixels, 3);
let linear = if let Some(g) = gamma {
gamma_u8_to_linear_f32(&rgb, 3, g)
} else {
srgb_u8_to_linear_f32(&rgb, 3)
};
(linear, None, false)
}
PixelLayout::Rgba8 => {
let rgb = if let Some(g) = gamma {
gamma_u8_to_linear_f32(pixels, 4, g)
} else {
srgb_u8_to_linear_f32(pixels, 4)
};
let alpha = extract_alpha(pixels, 4, 3);
(rgb, Some(alpha), false)
}
PixelLayout::Bgra8 => {
let swapped = bgr_to_rgb(pixels, 4);
let rgb = if let Some(g) = gamma {
gamma_u8_to_linear_f32(&swapped, 4, g)
} else {
srgb_u8_to_linear_f32(&swapped, 4)
};
let alpha = extract_alpha(pixels, 4, 3);
(rgb, Some(alpha), false)
}
PixelLayout::Gray8 => {
let rgb = if let Some(g) = gamma {
gamma_gray_u8_to_linear_f32_rgb(pixels, 1, g)
} else {
gray_u8_to_linear_f32_rgb(pixels, 1)
};
(rgb, None, false)
}
PixelLayout::GrayAlpha8 => {
let rgb = if let Some(g) = gamma {
gamma_gray_u8_to_linear_f32_rgb(pixels, 2, g)
} else {
gray_u8_to_linear_f32_rgb(pixels, 2)
};
let alpha = extract_alpha(pixels, 2, 1);
(rgb, Some(alpha), false)
}
PixelLayout::Rgb16 => {
let linear = if let Some(g) = gamma {
gamma_u16_to_linear_f32(pixels, 3, g)
} else {
srgb_u16_to_linear_f32(pixels, 3)
};
(linear, None, true)
}
PixelLayout::Rgba16 => {
let rgb = if let Some(g) = gamma {
gamma_u16_to_linear_f32(pixels, 4, g)
} else {
srgb_u16_to_linear_f32(pixels, 4)
};
let alpha = extract_alpha_u16(pixels, 4, 3);
(rgb, Some(alpha), true)
}
PixelLayout::Gray16 => {
let rgb = if let Some(g) = gamma {
gamma_gray_u16_to_linear_f32_rgb(pixels, 1, g)
} else {
gray_u16_to_linear_f32_rgb(pixels, 1)
};
(rgb, None, true)
}
PixelLayout::GrayAlpha16 => {
let rgb = if let Some(g) = gamma {
gamma_gray_u16_to_linear_f32_rgb(pixels, 2, g)
} else {
gray_u16_to_linear_f32_rgb(pixels, 2)
};
let alpha = extract_alpha_u16(pixels, 2, 1);
(rgb, Some(alpha), true)
}
PixelLayout::RgbLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
(floats.to_vec(), None, false)
}
PixelLayout::RgbaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
let rgb: Vec<f32> = floats
.chunks(4)
.flat_map(|px| [px[0], px[1], px[2]])
.collect();
let alpha = extract_alpha_f32(floats, 4, 3);
(rgb, Some(alpha), false)
}
PixelLayout::GrayLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
(gray_f32_to_linear_f32_rgb(floats, 1), None, false)
}
PixelLayout::GrayAlphaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
let rgb = gray_f32_to_linear_f32_rgb(floats, 2);
let alpha = extract_alpha_f32(floats, 2, 1);
(rgb, Some(alpha), false)
}
};
let mut profile = crate::effort::EffortProfile::lossy(cfg.effort, cfg.mode);
if let Some(max_size) = cfg.max_strategy_size {
if max_size < 16 {
profile.try_dct16 = false;
}
if max_size < 32 {
profile.try_dct32 = false;
}
if max_size < 64 {
profile.try_dct64 = false;
}
}
let mut enc = crate::vardct::VarDctEncoder::new(cfg.distance);
enc.effort = cfg.effort;
enc.profile = profile;
enc.use_ans = cfg.use_ans;
enc.optimize_codes = enc.profile.optimize_codes;
enc.custom_orders = enc.profile.custom_orders;
enc.ac_strategy_enabled = enc.profile.ac_strategy_enabled;
enc.enable_noise = cfg.noise;
enc.enable_denoise = cfg.denoise;
enc.enable_gaborish = cfg.gaborish && cfg.distance > 0.5;
enc.error_diffusion = cfg.error_diffusion;
enc.pixel_domain_loss = cfg.pixel_domain_loss;
enc.enable_lz77 = cfg.lz77;
enc.lz77_method = cfg.lz77_method;
enc.force_strategy = cfg.force_strategy;
enc.enable_patches = cfg.patches;
enc.encoder_mode = cfg.mode;
enc.splines = cfg.splines.clone();
enc.is_grayscale = self.layout.is_grayscale();
enc.progressive = cfg.progressive;
enc.use_lf_frame = cfg.lf_frame;
#[cfg(feature = "butteraugli-loop")]
{
enc.butteraugli_iters = cfg.butteraugli_iters;
}
#[cfg(feature = "ssim2-loop")]
{
enc.ssim2_iters = cfg.ssim2_iters;
}
#[cfg(feature = "zensim-loop")]
{
enc.zensim_iters = cfg.zensim_iters;
}
enc.bit_depth_16 = bit_depth_16;
enc.source_gamma = self.source_gamma;
enc.color_encoding = self.color_encoding.clone();
if let Some(meta) = self.metadata {
if let Some(it) = meta.intensity_target {
enc.intensity_target = it;
}
if let Some(mn) = meta.min_nits {
enc.min_nits = mn;
}
if meta.intrinsic_size.is_some() {
enc.intrinsic_size = meta.intrinsic_size;
}
}
if let Some(meta) = self.metadata
&& let Some(icc) = meta.icc_profile
{
enc.icc_profile = Some(icc.to_vec());
}
let output = enc
.encode(w, h, &linear_rgb, alpha.as_deref())
.map_err(EncodeError::from)?;
#[cfg(feature = "butteraugli-loop")]
let butteraugli_iters_actual = cfg.butteraugli_iters;
#[cfg(not(feature = "butteraugli-loop"))]
let butteraugli_iters_actual = 0u32;
let stats = EncodeStats {
mode: EncodeMode::Lossy,
strategy_counts: output.strategy_counts,
gaborish: cfg.gaborish,
ans: cfg.use_ans,
butteraugli_iters: butteraugli_iters_actual,
pixel_domain_loss: cfg.pixel_domain_loss,
..Default::default()
};
Ok((output.data, stats))
}
}
pub struct LossyEncoder {
cfg: LossyConfig,
width: u32,
height: u32,
layout: PixelLayout,
rows_pushed: u32,
linear_rgb: Vec<f32>,
alpha: Option<Vec<u8>>,
bit_depth_16: bool,
icc_profile: Option<Vec<u8>>,
exif: Option<Vec<u8>>,
xmp: Option<Vec<u8>>,
source_gamma: Option<f32>,
color_encoding: Option<crate::headers::color_encoding::ColorEncoding>,
intensity_target: f32,
min_nits: f32,
intrinsic_size: Option<(u32, u32)>,
}
impl LossyEncoder {
pub fn with_icc_profile(mut self, data: &[u8]) -> Self {
self.icc_profile = Some(data.to_vec());
self
}
pub fn with_exif(mut self, data: &[u8]) -> Self {
self.exif = Some(data.to_vec());
self
}
pub fn with_xmp(mut self, data: &[u8]) -> Self {
self.xmp = Some(data.to_vec());
self
}
pub fn with_source_gamma(mut self, gamma: f32) -> Self {
self.source_gamma = Some(gamma);
self
}
pub fn with_color_encoding(
mut self,
ce: crate::headers::color_encoding::ColorEncoding,
) -> Self {
self.color_encoding = Some(ce);
self
}
pub fn with_intensity_target(mut self, nits: f32) -> Self {
self.intensity_target = nits;
self
}
pub fn with_min_nits(mut self, nits: f32) -> Self {
self.min_nits = nits;
self
}
pub fn with_intrinsic_size(mut self, width: u32, height: u32) -> Self {
self.intrinsic_size = Some((width, height));
self
}
pub fn rows_pushed(&self) -> u32 {
self.rows_pushed
}
pub fn height(&self) -> u32 {
self.height
}
#[track_caller]
pub fn push_rows(&mut self, pixels: &[u8], num_rows: u32) -> Result<()> {
self.push_rows_inner(pixels, num_rows).map_err(at)
}
fn push_rows_inner(
&mut self,
pixels: &[u8],
num_rows: u32,
) -> core::result::Result<(), EncodeError> {
if num_rows == 0 {
return Ok(());
}
let remaining = self.height - self.rows_pushed;
if num_rows > remaining {
return Err(EncodeError::InvalidInput {
message: format!(
"push_rows: {num_rows} rows would exceed image height \
({} pushed + {num_rows} > {})",
self.rows_pushed, self.height
),
});
}
let w = self.width as usize;
let n = num_rows as usize;
let expected = w
.checked_mul(n)
.and_then(|wn| wn.checked_mul(self.layout.bytes_per_pixel()));
match expected {
Some(expected) if pixels.len() == expected => {}
Some(expected) => {
return Err(EncodeError::InvalidInput {
message: format!(
"push_rows: expected {expected} bytes for {w}x{n} {:?}, got {}",
self.layout,
pixels.len()
),
});
}
None => {
return Err(EncodeError::InvalidInput {
message: "push_rows: row dimensions overflow".into(),
});
}
}
let gamma = self.source_gamma;
let new_linear: Vec<f32> = match self.layout {
PixelLayout::Rgb8 => {
if let Some(g) = gamma {
gamma_u8_to_linear_f32(pixels, 3, g)
} else {
srgb_u8_to_linear_f32(pixels, 3)
}
}
PixelLayout::Bgr8 => {
let rgb = bgr_to_rgb(pixels, 3);
if let Some(g) = gamma {
gamma_u8_to_linear_f32(&rgb, 3, g)
} else {
srgb_u8_to_linear_f32(&rgb, 3)
}
}
PixelLayout::Rgba8 => {
if let Some(g) = gamma {
gamma_u8_to_linear_f32(pixels, 4, g)
} else {
srgb_u8_to_linear_f32(pixels, 4)
}
}
PixelLayout::Bgra8 => {
let swapped = bgr_to_rgb(pixels, 4);
if let Some(g) = gamma {
gamma_u8_to_linear_f32(&swapped, 4, g)
} else {
srgb_u8_to_linear_f32(&swapped, 4)
}
}
PixelLayout::Gray8 => {
if let Some(g) = gamma {
gamma_gray_u8_to_linear_f32_rgb(pixels, 1, g)
} else {
gray_u8_to_linear_f32_rgb(pixels, 1)
}
}
PixelLayout::GrayAlpha8 => {
if let Some(g) = gamma {
gamma_gray_u8_to_linear_f32_rgb(pixels, 2, g)
} else {
gray_u8_to_linear_f32_rgb(pixels, 2)
}
}
PixelLayout::Rgb16 => {
if let Some(g) = gamma {
gamma_u16_to_linear_f32(pixels, 3, g)
} else {
srgb_u16_to_linear_f32(pixels, 3)
}
}
PixelLayout::Rgba16 => {
if let Some(g) = gamma {
gamma_u16_to_linear_f32(pixels, 4, g)
} else {
srgb_u16_to_linear_f32(pixels, 4)
}
}
PixelLayout::Gray16 => {
if let Some(g) = gamma {
gamma_gray_u16_to_linear_f32_rgb(pixels, 1, g)
} else {
gray_u16_to_linear_f32_rgb(pixels, 1)
}
}
PixelLayout::GrayAlpha16 => {
if let Some(g) = gamma {
gamma_gray_u16_to_linear_f32_rgb(pixels, 2, g)
} else {
gray_u16_to_linear_f32_rgb(pixels, 2)
}
}
PixelLayout::RgbLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
floats.to_vec()
}
PixelLayout::RgbaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
floats
.chunks(4)
.flat_map(|px| [px[0], px[1], px[2]])
.collect()
}
PixelLayout::GrayLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
gray_f32_to_linear_f32_rgb(floats, 1)
}
PixelLayout::GrayAlphaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
gray_f32_to_linear_f32_rgb(floats, 2)
}
};
self.linear_rgb.extend_from_slice(&new_linear);
match self.layout {
PixelLayout::Rgba8 | PixelLayout::Bgra8 => {
let new_alpha = extract_alpha(pixels, 4, 3);
self.alpha
.get_or_insert_with(Vec::new)
.extend_from_slice(&new_alpha);
}
PixelLayout::GrayAlpha8 => {
let new_alpha = extract_alpha(pixels, 2, 1);
self.alpha
.get_or_insert_with(Vec::new)
.extend_from_slice(&new_alpha);
}
PixelLayout::Rgba16 => {
let new_alpha = extract_alpha_u16(pixels, 4, 3);
self.alpha
.get_or_insert_with(Vec::new)
.extend_from_slice(&new_alpha);
}
PixelLayout::GrayAlpha16 => {
let new_alpha = extract_alpha_u16(pixels, 2, 1);
self.alpha
.get_or_insert_with(Vec::new)
.extend_from_slice(&new_alpha);
}
PixelLayout::RgbaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
let new_alpha = extract_alpha_f32(floats, 4, 3);
self.alpha
.get_or_insert_with(Vec::new)
.extend_from_slice(&new_alpha);
}
PixelLayout::GrayAlphaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(pixels);
let new_alpha = extract_alpha_f32(floats, 2, 1);
self.alpha
.get_or_insert_with(Vec::new)
.extend_from_slice(&new_alpha);
}
_ => {}
}
self.rows_pushed += num_rows;
Ok(())
}
#[track_caller]
pub fn finish(self) -> Result<Vec<u8>> {
self.finish_inner()
.map(|mut r| r.take_data().unwrap())
.map_err(at)
}
#[track_caller]
pub fn finish_with_stats(self) -> Result<EncodeResult> {
self.finish_inner().map_err(at)
}
#[track_caller]
pub fn finish_into(self, out: &mut Vec<u8>) -> Result<EncodeResult> {
let mut result = self.finish_inner().map_err(at)?;
if let Some(data) = result.data.take() {
out.extend_from_slice(&data);
}
Ok(result)
}
#[cfg(feature = "std")]
#[track_caller]
pub fn finish_to(self, mut dest: impl std::io::Write) -> Result<EncodeResult> {
let mut result = self.finish_inner().map_err(at)?;
if let Some(data) = result.data.take() {
dest.write_all(&data)
.map_err(|e| at(EncodeError::from(e)))?;
}
Ok(result)
}
fn finish_inner(self) -> core::result::Result<EncodeResult, EncodeError> {
if self.rows_pushed != self.height {
return Err(EncodeError::InvalidInput {
message: format!(
"incomplete image: {} of {} rows pushed",
self.rows_pushed, self.height
),
});
}
let cfg = &self.cfg;
let w = self.width as usize;
let h = self.height as usize;
let linear_rgb = self.linear_rgb;
let alpha = self.alpha;
let (codestream, mut stats) = run_with_threads(cfg.threads, || {
let mut profile = crate::effort::EffortProfile::lossy(cfg.effort, cfg.mode);
if let Some(max_size) = cfg.max_strategy_size {
if max_size < 16 {
profile.try_dct16 = false;
}
if max_size < 32 {
profile.try_dct32 = false;
}
if max_size < 64 {
profile.try_dct64 = false;
}
}
let mut enc = crate::vardct::VarDctEncoder::new(cfg.distance);
enc.effort = cfg.effort;
enc.profile = profile;
enc.use_ans = cfg.use_ans;
enc.optimize_codes = enc.profile.optimize_codes;
enc.custom_orders = enc.profile.custom_orders;
enc.ac_strategy_enabled = enc.profile.ac_strategy_enabled;
enc.enable_noise = cfg.noise;
enc.enable_denoise = cfg.denoise;
enc.enable_gaborish = cfg.gaborish && cfg.distance > 0.5;
enc.error_diffusion = cfg.error_diffusion;
enc.pixel_domain_loss = cfg.pixel_domain_loss;
enc.enable_lz77 = cfg.lz77;
enc.lz77_method = cfg.lz77_method;
enc.force_strategy = cfg.force_strategy;
enc.enable_patches = cfg.patches;
enc.encoder_mode = cfg.mode;
enc.splines = cfg.splines.clone();
enc.is_grayscale = self.layout.is_grayscale();
enc.progressive = cfg.progressive;
enc.use_lf_frame = cfg.lf_frame;
#[cfg(feature = "butteraugli-loop")]
{
enc.butteraugli_iters = cfg.butteraugli_iters;
}
enc.bit_depth_16 = self.bit_depth_16;
enc.source_gamma = self.source_gamma;
enc.color_encoding = self.color_encoding.clone();
enc.intensity_target = self.intensity_target;
enc.min_nits = self.min_nits;
enc.intrinsic_size = self.intrinsic_size;
if let Some(ref icc) = self.icc_profile {
enc.icc_profile = Some(icc.clone());
}
let output = enc
.encode(w, h, &linear_rgb, alpha.as_deref())
.map_err(EncodeError::from)?;
#[cfg(feature = "butteraugli-loop")]
let butteraugli_iters_actual = cfg.butteraugli_iters;
#[cfg(not(feature = "butteraugli-loop"))]
let butteraugli_iters_actual = 0u32;
let stats = EncodeStats {
mode: EncodeMode::Lossy,
strategy_counts: output.strategy_counts,
gaborish: cfg.gaborish,
ans: cfg.use_ans,
butteraugli_iters: butteraugli_iters_actual,
pixel_domain_loss: cfg.pixel_domain_loss,
..Default::default()
};
Ok::<_, EncodeError>((output.data, stats))
})?;
stats.codestream_size = codestream.len();
let output = if self.exif.is_some() || self.xmp.is_some() {
crate::container::wrap_in_container(
&codestream,
self.exif.as_deref(),
self.xmp.as_deref(),
)
} else {
codestream
};
stats.output_size = output.len();
Ok(EncodeResult {
data: Some(output),
stats,
})
}
}
impl LossyConfig {
#[track_caller]
pub fn encoder(&self, width: u32, height: u32, layout: PixelLayout) -> Result<LossyEncoder> {
if width == 0 || height == 0 {
return Err(at(EncodeError::InvalidInput {
message: format!("zero dimensions: {width}x{height}"),
}));
}
let w = width as usize;
let h = height as usize;
let rgb_capacity = w.checked_mul(h).and_then(|n| n.checked_mul(3));
let Some(rgb_capacity) = rgb_capacity else {
return Err(at(EncodeError::InvalidInput {
message: "image dimensions overflow".into(),
}));
};
let bit_depth_16 = layout.is_16bit();
let has_alpha = layout.has_alpha();
let alpha = if has_alpha {
let mut v = Vec::new();
v.try_reserve(w * h)
.map_err(|e| at(EncodeError::from(crate::error::Error::from(e))))?;
Some(v)
} else {
None
};
let mut linear_rgb = Vec::new();
linear_rgb
.try_reserve(rgb_capacity)
.map_err(|e| at(EncodeError::from(crate::error::Error::from(e))))?;
Ok(LossyEncoder {
cfg: self.clone(),
width,
height,
layout,
rows_pushed: 0,
linear_rgb,
alpha,
bit_depth_16,
icc_profile: None,
exif: None,
xmp: None,
source_gamma: None,
color_encoding: None,
intensity_target: 255.0,
min_nits: 0.0,
intrinsic_size: None,
})
}
}
pub struct LosslessEncoder {
cfg: LosslessConfig,
width: u32,
height: u32,
layout: PixelLayout,
rows_pushed: u32,
channels: Vec<crate::modular::channel::Channel>,
num_source_channels: usize,
bit_depth: u32,
is_grayscale: bool,
has_alpha: bool,
icc_profile: Option<Vec<u8>>,
exif: Option<Vec<u8>>,
xmp: Option<Vec<u8>>,
source_gamma: Option<f32>,
color_encoding: Option<crate::headers::color_encoding::ColorEncoding>,
intensity_target: f32,
min_nits: f32,
intrinsic_size: Option<(u32, u32)>,
}
impl LosslessEncoder {
pub fn with_icc_profile(mut self, data: &[u8]) -> Self {
self.icc_profile = Some(data.to_vec());
self
}
pub fn with_exif(mut self, data: &[u8]) -> Self {
self.exif = Some(data.to_vec());
self
}
pub fn with_xmp(mut self, data: &[u8]) -> Self {
self.xmp = Some(data.to_vec());
self
}
pub fn with_source_gamma(mut self, gamma: f32) -> Self {
self.source_gamma = Some(gamma);
self
}
pub fn with_color_encoding(
mut self,
ce: crate::headers::color_encoding::ColorEncoding,
) -> Self {
self.color_encoding = Some(ce);
self
}
pub fn with_intensity_target(mut self, nits: f32) -> Self {
self.intensity_target = nits;
self
}
pub fn with_min_nits(mut self, nits: f32) -> Self {
self.min_nits = nits;
self
}
pub fn with_intrinsic_size(mut self, width: u32, height: u32) -> Self {
self.intrinsic_size = Some((width, height));
self
}
pub fn rows_pushed(&self) -> u32 {
self.rows_pushed
}
pub fn height(&self) -> u32 {
self.height
}
#[track_caller]
pub fn push_rows(&mut self, pixels: &[u8], num_rows: u32) -> Result<()> {
self.push_rows_inner(pixels, num_rows).map_err(at)
}
fn push_rows_inner(
&mut self,
pixels: &[u8],
num_rows: u32,
) -> core::result::Result<(), EncodeError> {
if num_rows == 0 {
return Ok(());
}
let remaining = self.height - self.rows_pushed;
if num_rows > remaining {
return Err(EncodeError::InvalidInput {
message: format!(
"push_rows: {num_rows} rows would exceed image height \
({} pushed + {num_rows} > {})",
self.rows_pushed, self.height
),
});
}
let w = self.width as usize;
let n = num_rows as usize;
let bpp = self.layout.bytes_per_pixel();
let expected = w.checked_mul(n).and_then(|wn| wn.checked_mul(bpp));
match expected {
Some(expected) if pixels.len() == expected => {}
Some(expected) => {
return Err(EncodeError::InvalidInput {
message: format!(
"push_rows: expected {expected} bytes for {w}x{n} {:?}, got {}",
self.layout,
pixels.len()
),
});
}
None => {
return Err(EncodeError::InvalidInput {
message: "push_rows: row dimensions overflow".into(),
});
}
}
let y_start = self.rows_pushed as usize;
let nc = self.num_source_channels;
match self.layout {
PixelLayout::Rgb8 | PixelLayout::Bgr8 => {
let is_bgr = matches!(self.layout, PixelLayout::Bgr8);
for y in 0..n {
let row_offset = y * w * 3;
let dst_y = y_start + y;
for x in 0..w {
let src = row_offset + x * 3;
let (r, g, b) = if is_bgr {
(pixels[src + 2], pixels[src + 1], pixels[src])
} else {
(pixels[src], pixels[src + 1], pixels[src + 2])
};
self.channels[0].set(x, dst_y, r as i32);
self.channels[1].set(x, dst_y, g as i32);
self.channels[2].set(x, dst_y, b as i32);
}
}
}
PixelLayout::Rgba8 | PixelLayout::Bgra8 => {
let is_bgr = matches!(self.layout, PixelLayout::Bgra8);
for y in 0..n {
let row_offset = y * w * 4;
let dst_y = y_start + y;
for x in 0..w {
let src = row_offset + x * 4;
let (r, g, b) = if is_bgr {
(pixels[src + 2], pixels[src + 1], pixels[src])
} else {
(pixels[src], pixels[src + 1], pixels[src + 2])
};
self.channels[0].set(x, dst_y, r as i32);
self.channels[1].set(x, dst_y, g as i32);
self.channels[2].set(x, dst_y, b as i32);
self.channels[3].set(x, dst_y, pixels[src + 3] as i32);
}
}
}
PixelLayout::Gray8 => {
for y in 0..n {
let row_offset = y * w;
let dst_y = y_start + y;
for x in 0..w {
self.channels[0].set(x, dst_y, pixels[row_offset + x] as i32);
}
}
}
PixelLayout::GrayAlpha8 => {
for y in 0..n {
let row_offset = y * w * 2;
let dst_y = y_start + y;
for x in 0..w {
let src = row_offset + x * 2;
self.channels[0].set(x, dst_y, pixels[src] as i32);
self.channels[1].set(x, dst_y, pixels[src + 1] as i32);
}
}
}
PixelLayout::Rgb16
| PixelLayout::Rgba16
| PixelLayout::Gray16
| PixelLayout::GrayAlpha16 => {
let pixels_u16: &[u16] = bytemuck::cast_slice(pixels);
for y in 0..n {
let row_offset = y * w * nc;
let dst_y = y_start + y;
for x in 0..w {
let src = row_offset + x * nc;
for c in 0..nc {
self.channels[c].set(x, dst_y, pixels_u16[src + c] as i32);
}
}
}
}
_ => {
return Err(EncodeError::UnsupportedPixelLayout(self.layout));
}
}
self.rows_pushed += num_rows;
Ok(())
}
#[track_caller]
pub fn finish(self) -> Result<Vec<u8>> {
self.finish_inner()
.map(|mut r| r.take_data().unwrap())
.map_err(at)
}
#[track_caller]
pub fn finish_with_stats(self) -> Result<EncodeResult> {
self.finish_inner().map_err(at)
}
#[track_caller]
pub fn finish_into(self, out: &mut Vec<u8>) -> Result<EncodeResult> {
let mut result = self.finish_inner().map_err(at)?;
if let Some(data) = result.data.take() {
out.extend_from_slice(&data);
}
Ok(result)
}
#[cfg(feature = "std")]
#[track_caller]
pub fn finish_to(self, mut dest: impl std::io::Write) -> Result<EncodeResult> {
let mut result = self.finish_inner().map_err(at)?;
if let Some(data) = result.data.take() {
dest.write_all(&data)
.map_err(|e| at(EncodeError::from(e)))?;
}
Ok(result)
}
fn finish_inner(self) -> core::result::Result<EncodeResult, EncodeError> {
use crate::bit_writer::BitWriter;
use crate::headers::color_encoding::ColorSpace;
use crate::headers::{ColorEncoding, FileHeader};
use crate::modular::channel::ModularImage;
use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
if self.rows_pushed != self.height {
return Err(EncodeError::InvalidInput {
message: format!(
"incomplete image: {} of {} rows pushed",
self.rows_pushed, self.height
),
});
}
let cfg = &self.cfg;
let w = self.width as usize;
let h = self.height as usize;
let mut image = ModularImage {
channels: self.channels,
bit_depth: self.bit_depth,
is_grayscale: self.is_grayscale,
has_alpha: self.has_alpha,
};
let (codestream, mut stats) = run_with_threads(cfg.threads, || {
let num_channels = self.layout.bytes_per_pixel();
let can_use_patches =
cfg.patches && !image.is_grayscale && image.bit_depth <= 8 && num_channels >= 3;
let patches_data = if can_use_patches {
let mut detection_pixels = vec![0u8; w * h * num_channels];
let nc = core::cmp::min(num_channels, image.channels.len());
for y in 0..h {
for x in 0..w {
for c in 0..nc {
detection_pixels[(y * w + x) * num_channels + c] =
image.channels[c].get(x, y) as u8;
}
for c in nc..num_channels {
if c < image.channels.len() {
detection_pixels[(y * w + x) * num_channels + c] =
image.channels[c].get(x, y) as u8;
}
}
}
}
crate::vardct::patches::find_and_build_lossless(
&detection_pixels,
w,
h,
num_channels,
image.bit_depth,
)
} else {
None
};
let mut file_header = if image.is_grayscale {
FileHeader::new_gray(self.width, self.height)
} else if image.has_alpha {
FileHeader::new_rgba(self.width, self.height)
} else {
FileHeader::new_rgb(self.width, self.height)
};
if image.bit_depth == 16 {
file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
for ec in &mut file_header.metadata.extra_channels {
ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
}
}
if self.icc_profile.is_some() {
file_header.metadata.color_encoding.want_icc = true;
}
file_header.metadata.intensity_target = self.intensity_target;
file_header.metadata.min_nits = self.min_nits;
if let Some((w, h)) = self.intrinsic_size {
file_header.metadata.have_intrinsic_size = true;
file_header.metadata.intrinsic_width = w;
file_header.metadata.intrinsic_height = h;
}
let mut writer = BitWriter::new();
file_header.write(&mut writer).map_err(EncodeError::from)?;
if let Some(ref icc) = self.icc_profile {
crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
}
writer.zero_pad_to_byte();
if let Some(ref pd) = patches_data {
let lossless_profile = crate::effort::EffortProfile::lossless(cfg.effort, cfg.mode);
crate::vardct::patches::encode_reference_frame_rgb(
pd,
image.bit_depth,
cfg.use_ans,
lossless_profile.patch_ref_tree_learning,
&mut writer,
)
.map_err(EncodeError::from)?;
writer.zero_pad_to_byte();
let bd = image.bit_depth;
crate::vardct::patches::subtract_patches_modular(&mut image, pd, bd);
}
let frame_encoder = FrameEncoder::new(
w,
h,
FrameEncoderOptions {
use_modular: true,
effort: cfg.effort,
use_ans: cfg.use_ans,
use_tree_learning: cfg.tree_learning,
use_squeeze: cfg.squeeze,
enable_lz77: cfg.lz77,
lz77_method: cfg.lz77_method,
lossy_palette: cfg.lossy_palette,
encoder_mode: cfg.mode,
profile: crate::effort::EffortProfile::lossless(cfg.effort, cfg.mode),
have_animation: false,
duration: 0,
is_last: true,
crop: None,
skip_rct: false,
},
);
let color_encoding = if let Some(ce) = self.color_encoding.clone() {
if image.is_grayscale && ce.color_space != ColorSpace::Gray {
ColorEncoding {
color_space: ColorSpace::Gray,
..ce
}
} else {
ce
}
} else if let Some(gamma) = self.source_gamma {
if image.is_grayscale {
ColorEncoding::gray_with_gamma(gamma)
} else {
ColorEncoding::with_gamma(gamma)
}
} else if image.is_grayscale {
ColorEncoding::gray()
} else {
ColorEncoding::srgb()
};
frame_encoder
.encode_modular_with_patches(
&image,
&color_encoding,
&mut writer,
patches_data.as_ref(),
)
.map_err(EncodeError::from)?;
let stats = EncodeStats {
mode: EncodeMode::Lossless,
ans: cfg.use_ans,
..Default::default()
};
Ok::<_, EncodeError>((writer.finish_with_padding(), stats))
})?;
stats.codestream_size = codestream.len();
let output = if self.exif.is_some() || self.xmp.is_some() {
crate::container::wrap_in_container(
&codestream,
self.exif.as_deref(),
self.xmp.as_deref(),
)
} else {
codestream
};
stats.output_size = output.len();
Ok(EncodeResult {
data: Some(output),
stats,
})
}
}
impl LosslessConfig {
#[track_caller]
pub fn encoder(&self, width: u32, height: u32, layout: PixelLayout) -> Result<LosslessEncoder> {
use crate::modular::channel::Channel;
if width == 0 || height == 0 {
return Err(at(EncodeError::InvalidInput {
message: format!("zero dimensions: {width}x{height}"),
}));
}
let w = width as usize;
let h = height as usize;
let (num_channels, bit_depth, is_grayscale, has_alpha) = match layout {
PixelLayout::Rgb8 | PixelLayout::Bgr8 => (3, 8u32, false, false),
PixelLayout::Rgba8 | PixelLayout::Bgra8 => (4, 8, false, true),
PixelLayout::Gray8 => (1, 8, true, false),
PixelLayout::GrayAlpha8 => (2, 8, true, true),
PixelLayout::Rgb16 => (3, 16, false, false),
PixelLayout::Rgba16 => (4, 16, false, true),
PixelLayout::Gray16 => (1, 16, true, false),
PixelLayout::GrayAlpha16 => (2, 16, true, true),
other => return Err(at(EncodeError::UnsupportedPixelLayout(other))),
};
let mut channels = Vec::with_capacity(num_channels);
for _ in 0..num_channels {
channels.push(Channel::new(w, h).map_err(|e| at(EncodeError::from(e)))?);
}
Ok(LosslessEncoder {
cfg: self.clone(),
width,
height,
layout,
rows_pushed: 0,
channels,
num_source_channels: num_channels,
bit_depth,
is_grayscale,
has_alpha,
icc_profile: None,
exif: None,
xmp: None,
source_gamma: None,
color_encoding: None,
intensity_target: 255.0,
min_nits: 0.0,
intrinsic_size: None,
})
}
}
#[cfg(feature = "parallel")]
fn run_with_threads<T>(threads: usize, f: impl FnOnce() -> T + Send) -> T
where
T: Send,
{
if threads <= 1 {
return f();
}
match rayon::ThreadPoolBuilder::new().num_threads(threads).build() {
Ok(pool) => pool.install(f),
Err(_) => f(),
}
}
#[cfg(not(feature = "parallel"))]
fn run_with_threads<T>(_threads: usize, f: impl FnOnce() -> T) -> T {
f()
}
fn validate_animation_input(
width: u32,
height: u32,
layout: PixelLayout,
frames: &[AnimationFrame<'_>],
) -> core::result::Result<(), EncodeError> {
if width == 0 || height == 0 {
return Err(EncodeError::InvalidInput {
message: format!("zero dimensions: {width}x{height}"),
});
}
if frames.is_empty() {
return Err(EncodeError::InvalidInput {
message: "animation requires at least one frame".into(),
});
}
let expected_size = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(layout.bytes_per_pixel()))
.ok_or_else(|| EncodeError::InvalidInput {
message: "image dimensions overflow".into(),
})?;
for (i, frame) in frames.iter().enumerate() {
if frame.pixels.len() != expected_size {
return Err(EncodeError::InvalidInput {
message: format!(
"frame {} pixel buffer size mismatch: expected {expected_size}, got {}",
i,
frame.pixels.len()
),
});
}
}
Ok(())
}
fn encode_animation_lossless(
cfg: &LosslessConfig,
width: u32,
height: u32,
layout: PixelLayout,
animation: &AnimationParams,
frames: &[AnimationFrame<'_>],
) -> core::result::Result<Vec<u8>, EncodeError> {
use crate::bit_writer::BitWriter;
use crate::headers::file_header::AnimationHeader;
use crate::headers::{ColorEncoding, FileHeader};
use crate::modular::channel::ModularImage;
use crate::modular::frame::{FrameEncoder, FrameEncoderOptions};
validate_animation_input(width, height, layout, frames)?;
let w = width as usize;
let h = height as usize;
let num_frames = frames.len();
let sample_image = match layout {
PixelLayout::Rgb8 => ModularImage::from_rgb8(frames[0].pixels, w, h),
PixelLayout::Rgba8 => ModularImage::from_rgba8(frames[0].pixels, w, h),
PixelLayout::Bgr8 => ModularImage::from_rgb8(&bgr_to_rgb(frames[0].pixels, 3), w, h),
PixelLayout::Bgra8 => ModularImage::from_rgba8(&bgr_to_rgb(frames[0].pixels, 4), w, h),
PixelLayout::Gray8 => ModularImage::from_gray8(frames[0].pixels, w, h),
PixelLayout::GrayAlpha8 => ModularImage::from_grayalpha8(frames[0].pixels, w, h),
PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frames[0].pixels, w, h),
PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frames[0].pixels, w, h),
PixelLayout::Gray16 => ModularImage::from_gray16_native(frames[0].pixels, w, h),
PixelLayout::GrayAlpha16 => ModularImage::from_grayalpha16_native(frames[0].pixels, w, h),
other => return Err(EncodeError::UnsupportedPixelLayout(other)),
}
.map_err(EncodeError::from)?;
let mut file_header = if sample_image.is_grayscale {
FileHeader::new_gray(width, height)
} else if sample_image.has_alpha {
FileHeader::new_rgba(width, height)
} else {
FileHeader::new_rgb(width, height)
};
if sample_image.bit_depth == 16 {
file_header.metadata.bit_depth = crate::headers::file_header::BitDepth::uint16();
for ec in &mut file_header.metadata.extra_channels {
ec.bit_depth = crate::headers::file_header::BitDepth::uint16();
}
}
file_header.metadata.animation = Some(AnimationHeader {
tps_numerator: animation.tps_numerator,
tps_denominator: animation.tps_denominator,
num_loops: animation.num_loops,
have_timecodes: false,
});
let mut writer = BitWriter::new();
file_header.write(&mut writer).map_err(EncodeError::from)?;
writer.zero_pad_to_byte();
let color_encoding = ColorEncoding::srgb();
let bpp = layout.bytes_per_pixel();
let mut prev_pixels: Option<&[u8]> = None;
for (i, frame) in frames.iter().enumerate() {
let crop = if let Some(prev) = prev_pixels {
match detect_frame_crop(prev, frame.pixels, w, h, bpp, false) {
Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
Some(_) => None, None => {
Some(FrameCrop {
x0: 0,
y0: 0,
width: 1,
height: 1,
})
}
}
} else {
None };
let (frame_w, frame_h, frame_pixels_owned);
let frame_pixels: &[u8] = if let Some(ref crop) = crop {
frame_w = crop.width as usize;
frame_h = crop.height as usize;
frame_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
&frame_pixels_owned
} else {
frame_w = w;
frame_h = h;
frame_pixels_owned = Vec::new();
let _ = &frame_pixels_owned; frame.pixels
};
let image = match layout {
PixelLayout::Rgb8 => ModularImage::from_rgb8(frame_pixels, frame_w, frame_h),
PixelLayout::Rgba8 => ModularImage::from_rgba8(frame_pixels, frame_w, frame_h),
PixelLayout::Bgr8 => {
ModularImage::from_rgb8(&bgr_to_rgb(frame_pixels, 3), frame_w, frame_h)
}
PixelLayout::Bgra8 => {
ModularImage::from_rgba8(&bgr_to_rgb(frame_pixels, 4), frame_w, frame_h)
}
PixelLayout::Gray8 => ModularImage::from_gray8(frame_pixels, frame_w, frame_h),
PixelLayout::GrayAlpha8 => {
ModularImage::from_grayalpha8(frame_pixels, frame_w, frame_h)
}
PixelLayout::Rgb16 => ModularImage::from_rgb16_native(frame_pixels, frame_w, frame_h),
PixelLayout::Rgba16 => ModularImage::from_rgba16_native(frame_pixels, frame_w, frame_h),
PixelLayout::Gray16 => ModularImage::from_gray16_native(frame_pixels, frame_w, frame_h),
PixelLayout::GrayAlpha16 => {
ModularImage::from_grayalpha16_native(frame_pixels, frame_w, frame_h)
}
other => return Err(EncodeError::UnsupportedPixelLayout(other)),
}
.map_err(EncodeError::from)?;
let use_tree_learning = cfg.tree_learning;
let frame_encoder = FrameEncoder::new(
frame_w,
frame_h,
FrameEncoderOptions {
use_modular: true,
effort: cfg.effort,
use_ans: cfg.use_ans,
use_tree_learning,
use_squeeze: cfg.squeeze,
enable_lz77: cfg.lz77,
lz77_method: cfg.lz77_method,
lossy_palette: cfg.lossy_palette,
encoder_mode: cfg.mode,
profile: crate::effort::EffortProfile::lossless(cfg.effort, cfg.mode),
have_animation: true,
duration: frame.duration,
is_last: i == num_frames - 1,
crop,
skip_rct: false,
},
);
frame_encoder
.encode_modular(&image, &color_encoding, &mut writer)
.map_err(EncodeError::from)?;
prev_pixels = Some(frame.pixels);
}
Ok(writer.finish_with_padding())
}
fn encode_animation_lossy(
cfg: &LossyConfig,
width: u32,
height: u32,
layout: PixelLayout,
animation: &AnimationParams,
frames: &[AnimationFrame<'_>],
) -> core::result::Result<Vec<u8>, EncodeError> {
use crate::bit_writer::BitWriter;
use crate::headers::file_header::AnimationHeader;
use crate::headers::frame_header::FrameOptions;
validate_animation_input(width, height, layout, frames)?;
let w = width as usize;
let h = height as usize;
let num_frames = frames.len();
let mut profile = crate::effort::EffortProfile::lossy(cfg.effort, cfg.mode);
if let Some(max_size) = cfg.max_strategy_size {
if max_size < 16 {
profile.try_dct16 = false;
}
if max_size < 32 {
profile.try_dct32 = false;
}
if max_size < 64 {
profile.try_dct64 = false;
}
}
let mut enc = crate::vardct::VarDctEncoder::new(cfg.distance);
enc.effort = cfg.effort;
enc.profile = profile;
enc.use_ans = cfg.use_ans;
enc.optimize_codes = enc.profile.optimize_codes;
enc.custom_orders = enc.profile.custom_orders;
enc.ac_strategy_enabled = enc.profile.ac_strategy_enabled;
enc.enable_noise = cfg.noise;
enc.enable_denoise = cfg.denoise;
enc.enable_gaborish = cfg.gaborish && cfg.distance > 0.5;
enc.error_diffusion = cfg.error_diffusion;
enc.pixel_domain_loss = cfg.pixel_domain_loss;
enc.enable_lz77 = cfg.lz77;
enc.lz77_method = cfg.lz77_method;
enc.force_strategy = cfg.force_strategy;
enc.progressive = cfg.progressive;
enc.use_lf_frame = cfg.lf_frame;
#[cfg(feature = "butteraugli-loop")]
{
enc.butteraugli_iters = cfg.butteraugli_iters;
}
#[cfg(feature = "ssim2-loop")]
{
enc.ssim2_iters = cfg.ssim2_iters;
}
#[cfg(feature = "zensim-loop")]
{
enc.zensim_iters = cfg.zensim_iters;
}
let has_alpha = layout.has_alpha();
let bit_depth_16 = matches!(layout, PixelLayout::Rgb16 | PixelLayout::Rgba16);
enc.bit_depth_16 = bit_depth_16;
let mut file_header = enc.build_file_header(w, h, has_alpha);
file_header.metadata.animation = Some(AnimationHeader {
tps_numerator: animation.tps_numerator,
tps_denominator: animation.tps_denominator,
num_loops: animation.num_loops,
have_timecodes: false,
});
let mut writer = BitWriter::with_capacity(w * h * 4);
file_header.write(&mut writer).map_err(EncodeError::from)?;
if let Some(ref icc) = enc.icc_profile {
crate::icc::write_icc(icc, &mut writer).map_err(EncodeError::from)?;
}
writer.zero_pad_to_byte();
let bpp = layout.bytes_per_pixel();
let mut prev_pixels: Option<&[u8]> = None;
for (i, frame) in frames.iter().enumerate() {
let crop = if let Some(prev) = prev_pixels {
match detect_frame_crop(prev, frame.pixels, w, h, bpp, true) {
Some(crop) if (crop.width as usize) < w || (crop.height as usize) < h => Some(crop),
Some(_) => None, None => {
Some(FrameCrop {
x0: 0,
y0: 0,
width: 8.min(width),
height: 8.min(height),
})
}
}
} else {
None };
let (frame_w, frame_h) = if let Some(ref crop) = crop {
(crop.width as usize, crop.height as usize)
} else {
(w, h)
};
let crop_pixels_owned;
let src_pixels: &[u8] = if let Some(ref crop) = crop {
crop_pixels_owned = extract_pixel_crop(frame.pixels, w, crop, bpp);
&crop_pixels_owned
} else {
crop_pixels_owned = Vec::new();
let _ = &crop_pixels_owned;
frame.pixels
};
let (linear_rgb, alpha) = match layout {
PixelLayout::Rgb8 => (srgb_u8_to_linear_f32(src_pixels, 3), None),
PixelLayout::Bgr8 => (srgb_u8_to_linear_f32(&bgr_to_rgb(src_pixels, 3), 3), None),
PixelLayout::Rgba8 => {
let rgb = srgb_u8_to_linear_f32(src_pixels, 4);
let alpha = extract_alpha(src_pixels, 4, 3);
(rgb, Some(alpha))
}
PixelLayout::Bgra8 => {
let swapped = bgr_to_rgb(src_pixels, 4);
let rgb = srgb_u8_to_linear_f32(&swapped, 4);
let alpha = extract_alpha(src_pixels, 4, 3);
(rgb, Some(alpha))
}
PixelLayout::Gray8 => (gray_u8_to_linear_f32_rgb(src_pixels, 1), None),
PixelLayout::GrayAlpha8 => {
let rgb = gray_u8_to_linear_f32_rgb(src_pixels, 2);
let alpha = extract_alpha(src_pixels, 2, 1);
(rgb, Some(alpha))
}
PixelLayout::Rgb16 => (srgb_u16_to_linear_f32(src_pixels, 3), None),
PixelLayout::Rgba16 => {
let rgb = srgb_u16_to_linear_f32(src_pixels, 4);
let alpha = extract_alpha_u16(src_pixels, 4, 3);
(rgb, Some(alpha))
}
PixelLayout::Gray16 => (gray_u16_to_linear_f32_rgb(src_pixels, 1), None),
PixelLayout::GrayAlpha16 => {
let rgb = gray_u16_to_linear_f32_rgb(src_pixels, 2);
let alpha = extract_alpha_u16(src_pixels, 2, 1);
(rgb, Some(alpha))
}
PixelLayout::RgbLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(src_pixels);
(floats.to_vec(), None)
}
PixelLayout::RgbaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(src_pixels);
let rgb: Vec<f32> = floats
.chunks(4)
.flat_map(|px| [px[0], px[1], px[2]])
.collect();
let alpha = extract_alpha_f32(floats, 4, 3);
(rgb, Some(alpha))
}
PixelLayout::GrayLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(src_pixels);
(gray_f32_to_linear_f32_rgb(floats, 1), None)
}
PixelLayout::GrayAlphaLinearF32 => {
let floats: &[f32] = bytemuck::cast_slice(src_pixels);
let rgb = gray_f32_to_linear_f32_rgb(floats, 2);
let alpha = extract_alpha_f32(floats, 2, 1);
(rgb, Some(alpha))
}
};
let frame_options = FrameOptions {
have_animation: true,
have_timecodes: false,
duration: frame.duration,
is_last: i == num_frames - 1,
crop,
};
enc.encode_frame_to_writer(
frame_w,
frame_h,
&linear_rgb,
alpha.as_deref(),
&frame_options,
&mut writer,
)
.map_err(EncodeError::from)?;
prev_pixels = Some(frame.pixels);
}
Ok(writer.finish_with_padding())
}
use crate::headers::frame_header::FrameCrop;
fn detect_frame_crop(
prev: &[u8],
curr: &[u8],
width: usize,
height: usize,
bytes_per_pixel: usize,
align_to_8x8: bool,
) -> Option<FrameCrop> {
let stride = width * bytes_per_pixel;
debug_assert_eq!(prev.len(), height * stride);
debug_assert_eq!(curr.len(), height * stride);
let mut top = height;
let mut bottom = 0;
let mut left = width;
let mut right = 0;
for y in 0..height {
let row_start = y * stride;
let prev_row = &prev[row_start..row_start + stride];
let curr_row = &curr[row_start..row_start + stride];
let (prev_prefix, prev_u64, prev_suffix) = bytemuck::pod_align_to::<u8, u64>(prev_row);
let (curr_prefix, curr_u64, curr_suffix) = bytemuck::pod_align_to::<u8, u64>(curr_row);
if prev_prefix == curr_prefix && prev_u64 == curr_u64 && prev_suffix == curr_suffix {
continue;
}
if top == height {
top = y;
}
bottom = y;
for x in 0..width {
let px_start = x * bytes_per_pixel;
if prev_row[px_start..px_start + bytes_per_pixel]
!= curr_row[px_start..px_start + bytes_per_pixel]
{
left = left.min(x);
break;
}
}
for x in (0..width).rev() {
let px_start = x * bytes_per_pixel;
if prev_row[px_start..px_start + bytes_per_pixel]
!= curr_row[px_start..px_start + bytes_per_pixel]
{
right = right.max(x);
break;
}
}
}
if top == height {
return None;
}
let mut crop_x = left as i32;
let mut crop_y = top as i32;
let mut crop_w = (right - left + 1) as u32;
let mut crop_h = (bottom - top + 1) as u32;
if align_to_8x8 {
let aligned_x = (crop_x / 8) * 8;
let aligned_y = (crop_y / 8) * 8;
let end_x = (crop_x as u32 + crop_w).div_ceil(8) * 8;
let end_y = (crop_y as u32 + crop_h).div_ceil(8) * 8;
crop_x = aligned_x;
crop_y = aligned_y;
crop_w = end_x.min(width as u32) - aligned_x as u32;
crop_h = end_y.min(height as u32) - aligned_y as u32;
}
Some(FrameCrop {
x0: crop_x,
y0: crop_y,
width: crop_w,
height: crop_h,
})
}
fn extract_pixel_crop(
pixels: &[u8],
full_width: usize,
crop: &FrameCrop,
bytes_per_pixel: usize,
) -> Vec<u8> {
let cx = crop.x0 as usize;
let cy = crop.y0 as usize;
let cw = crop.width as usize;
let ch = crop.height as usize;
let stride = full_width * bytes_per_pixel;
let mut out = Vec::with_capacity(cw * ch * bytes_per_pixel);
for y in cy..cy + ch {
let row_start = y * stride + cx * bytes_per_pixel;
out.extend_from_slice(&pixels[row_start..row_start + cw * bytes_per_pixel]);
}
out
}
const SRGB_U8_TO_LINEAR: [f32; 256] = {
let mut table = [0.0f32; 256];
let mut i = 0u16;
while i < 256 {
let c = i as f64 / 255.0;
table[i as usize] = if c <= 0.04045 {
(c / 12.92) as f32
} else {
let base = (c + 0.055) / 1.055;
let x2 = base * base;
let x4 = x2 * x2;
let x8 = x4 * x4;
let x12 = x8 * x4;
let mut y = base * base; let mut iter = 0;
while iter < 8 {
let y2 = y * y;
let y4 = y2 * y2;
y = (4.0 * y + x12 / y4) / 5.0;
iter += 1;
}
y as f32
};
i += 1;
}
table
};
#[inline]
fn srgb_to_linear(c: u8) -> f32 {
SRGB_U8_TO_LINEAR[c as usize]
}
fn srgb_u8_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
let num_pixels = data.len() / channels;
let mut out = vec![0.0f32; num_pixels * 3];
let lut = &SRGB_U8_TO_LINEAR;
for (px, rgb) in data.chunks_exact(channels).zip(out.chunks_exact_mut(3)) {
rgb[0] = lut[px[0] as usize];
rgb[1] = lut[px[1] as usize];
rgb[2] = lut[px[2] as usize];
}
out
}
fn srgb_u16_to_linear_f32(data: &[u8], channels: usize) -> Vec<f32> {
let pixels: &[u16] = bytemuck::cast_slice(data);
pixels
.chunks(channels)
.flat_map(|px| {
[
srgb_to_linear_f(px[0] as f32 / 65535.0),
srgb_to_linear_f(px[1] as f32 / 65535.0),
srgb_to_linear_f(px[2] as f32 / 65535.0),
]
})
.collect()
}
#[inline]
fn srgb_to_linear_f(c: f32) -> f32 {
if c <= 0.04045 {
c / 12.92
} else {
jxl_simd::fast_powf((c + 0.055) / 1.055, 2.4)
}
}
fn gamma_u8_to_linear_f32(data: &[u8], channels: usize, gamma: f32) -> Vec<f32> {
let inv_gamma = 1.0 / gamma;
let lut: [f32; 256] =
core::array::from_fn(|i| jxl_simd::fast_powf(i as f32 / 255.0, inv_gamma));
data.chunks(channels)
.flat_map(|px| {
[
lut[px[0] as usize],
lut[px[1] as usize],
lut[px[2] as usize],
]
})
.collect()
}
fn gamma_u16_to_linear_f32(data: &[u8], channels: usize, gamma: f32) -> Vec<f32> {
let inv_gamma = 1.0 / gamma;
let pixels: &[u16] = bytemuck::cast_slice(data);
pixels
.chunks(channels)
.flat_map(|px| {
[
jxl_simd::fast_powf(px[0] as f32 / 65535.0, inv_gamma),
jxl_simd::fast_powf(px[1] as f32 / 65535.0, inv_gamma),
jxl_simd::fast_powf(px[2] as f32 / 65535.0, inv_gamma),
]
})
.collect()
}
fn gamma_gray_u8_to_linear_f32_rgb(data: &[u8], stride: usize, gamma: f32) -> Vec<f32> {
let inv_gamma = 1.0 / gamma;
let lut: [f32; 256] =
core::array::from_fn(|i| jxl_simd::fast_powf(i as f32 / 255.0, inv_gamma));
data.chunks(stride)
.flat_map(|px| {
let v = lut[px[0] as usize];
[v, v, v]
})
.collect()
}
fn gamma_gray_u16_to_linear_f32_rgb(data: &[u8], stride: usize, gamma: f32) -> Vec<f32> {
let inv_gamma = 1.0 / gamma;
let pixels: &[u16] = bytemuck::cast_slice(data);
pixels
.chunks(stride)
.flat_map(|px| {
let v = jxl_simd::fast_powf(px[0] as f32 / 65535.0, inv_gamma);
[v, v, v]
})
.collect()
}
fn extract_alpha_u16(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
let pixels: &[u16] = bytemuck::cast_slice(data);
pixels
.chunks(stride)
.map(|px| (px[alpha_offset] >> 8) as u8)
.collect()
}
fn bgr_to_rgb(data: &[u8], stride: usize) -> Vec<u8> {
let mut out = data.to_vec();
for chunk in out.chunks_mut(stride) {
chunk.swap(0, 2);
}
out
}
fn extract_alpha(data: &[u8], stride: usize, alpha_offset: usize) -> Vec<u8> {
data.chunks(stride).map(|px| px[alpha_offset]).collect()
}
fn extract_alpha_f32(data: &[f32], stride: usize, alpha_offset: usize) -> Vec<u8> {
data.chunks(stride)
.map(|px| (px[alpha_offset].clamp(0.0, 1.0) * 255.0 + 0.5) as u8)
.collect()
}
fn gray_u8_to_linear_f32_rgb(data: &[u8], stride: usize) -> Vec<f32> {
data.chunks(stride)
.flat_map(|px| {
let v = srgb_to_linear(px[0]);
[v, v, v]
})
.collect()
}
fn gray_u16_to_linear_f32_rgb(data: &[u8], stride: usize) -> Vec<f32> {
let pixels: &[u16] = bytemuck::cast_slice(data);
pixels
.chunks(stride)
.flat_map(|px| {
let v = srgb_to_linear_f(px[0] as f32 / 65535.0);
[v, v, v]
})
.collect()
}
fn gray_f32_to_linear_f32_rgb(data: &[f32], stride: usize) -> Vec<f32> {
data.chunks(stride)
.flat_map(|px| {
let v = px[0];
[v, v, v]
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lossless_config_builder_and_getters() {
let cfg = LosslessConfig::new()
.with_effort(5)
.with_ans(false)
.with_squeeze(true)
.with_tree_learning(true);
assert_eq!(cfg.effort(), 5);
assert!(!cfg.ans());
assert!(cfg.squeeze());
assert!(cfg.tree_learning());
}
#[test]
fn test_lossy_config_builder_and_getters() {
let cfg = LossyConfig::new(2.0)
.with_effort(3)
.with_gaborish(false)
.with_noise(true);
assert_eq!(cfg.distance(), 2.0);
assert_eq!(cfg.effort(), 3);
assert!(!cfg.gaborish());
assert!(cfg.noise());
}
#[test]
fn test_pixel_layout_helpers() {
assert_eq!(PixelLayout::Rgb8.bytes_per_pixel(), 3);
assert_eq!(PixelLayout::Rgba8.bytes_per_pixel(), 4);
assert_eq!(PixelLayout::Bgr8.bytes_per_pixel(), 3);
assert_eq!(PixelLayout::Bgra8.bytes_per_pixel(), 4);
assert_eq!(PixelLayout::Gray8.bytes_per_pixel(), 1);
assert_eq!(PixelLayout::GrayAlpha8.bytes_per_pixel(), 2);
assert_eq!(PixelLayout::Rgb16.bytes_per_pixel(), 6);
assert_eq!(PixelLayout::Rgba16.bytes_per_pixel(), 8);
assert_eq!(PixelLayout::Gray16.bytes_per_pixel(), 2);
assert_eq!(PixelLayout::GrayAlpha16.bytes_per_pixel(), 4);
assert_eq!(PixelLayout::RgbLinearF32.bytes_per_pixel(), 12);
assert_eq!(PixelLayout::RgbaLinearF32.bytes_per_pixel(), 16);
assert_eq!(PixelLayout::GrayLinearF32.bytes_per_pixel(), 4);
assert_eq!(PixelLayout::GrayAlphaLinearF32.bytes_per_pixel(), 8);
assert!(!PixelLayout::Rgb8.is_linear());
assert!(PixelLayout::RgbLinearF32.is_linear());
assert!(PixelLayout::RgbaLinearF32.is_linear());
assert!(PixelLayout::GrayLinearF32.is_linear());
assert!(PixelLayout::GrayAlphaLinearF32.is_linear());
assert!(!PixelLayout::Rgb16.is_linear());
assert!(!PixelLayout::Rgb8.has_alpha());
assert!(PixelLayout::Rgba8.has_alpha());
assert!(PixelLayout::Bgra8.has_alpha());
assert!(PixelLayout::GrayAlpha8.has_alpha());
assert!(PixelLayout::Rgba16.has_alpha());
assert!(PixelLayout::GrayAlpha16.has_alpha());
assert!(PixelLayout::RgbaLinearF32.has_alpha());
assert!(PixelLayout::GrayAlphaLinearF32.has_alpha());
assert!(!PixelLayout::Rgb16.has_alpha());
assert!(!PixelLayout::RgbLinearF32.has_alpha());
assert!(PixelLayout::Rgb16.is_16bit());
assert!(PixelLayout::Rgba16.is_16bit());
assert!(PixelLayout::Gray16.is_16bit());
assert!(PixelLayout::GrayAlpha16.is_16bit());
assert!(!PixelLayout::Rgb8.is_16bit());
assert!(!PixelLayout::RgbLinearF32.is_16bit());
assert!(PixelLayout::RgbLinearF32.is_f32());
assert!(PixelLayout::RgbaLinearF32.is_f32());
assert!(PixelLayout::GrayLinearF32.is_f32());
assert!(PixelLayout::GrayAlphaLinearF32.is_f32());
assert!(!PixelLayout::Rgb8.is_f32());
assert!(!PixelLayout::Rgb16.is_f32());
assert!(PixelLayout::Gray8.is_grayscale());
assert!(PixelLayout::GrayAlpha8.is_grayscale());
assert!(PixelLayout::Gray16.is_grayscale());
assert!(PixelLayout::GrayAlpha16.is_grayscale());
assert!(PixelLayout::GrayLinearF32.is_grayscale());
assert!(PixelLayout::GrayAlphaLinearF32.is_grayscale());
assert!(!PixelLayout::Rgb16.is_grayscale());
assert!(!PixelLayout::RgbLinearF32.is_grayscale());
}
#[test]
fn test_quality_to_distance() {
assert!(Quality::Distance(1.0).to_distance().unwrap() == 1.0);
assert!(Quality::Distance(-1.0).to_distance().is_err());
assert!(Quality::Percent(100).to_distance().is_err()); assert!(Quality::Percent(90).to_distance().unwrap() == 1.0);
}
#[test]
fn test_pixel_validation() {
let cfg = LosslessConfig::new();
let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
assert!(req.validate_pixels(&[0u8; 12]).is_ok());
}
#[test]
fn test_pixel_validation_wrong_size() {
let cfg = LosslessConfig::new();
let req = cfg.encode_request(2, 2, PixelLayout::Rgb8);
assert!(req.validate_pixels(&[0u8; 11]).is_err());
}
#[test]
fn test_limits_check() {
let limits = Limits::new().with_max_width(100);
let cfg = LosslessConfig::new();
let req = cfg
.encode_request(200, 100, PixelLayout::Rgb8)
.with_limits(&limits);
assert!(req.check_limits().is_err());
}
#[test]
fn test_lossless_encode_rgb8_small() {
let pixels = [255u8, 0, 0].repeat(16);
let result = LosslessConfig::new()
.encode_request(4, 4, PixelLayout::Rgb8)
.encode(&pixels);
assert!(result.is_ok());
let jxl = result.unwrap();
assert_eq!(&jxl[..2], &[0xFF, 0x0A]); }
#[test]
fn test_lossy_encode_rgb8_small() {
let mut pixels = Vec::with_capacity(8 * 8 * 3);
for y in 0..8u8 {
for x in 0..8u8 {
pixels.push(x * 32);
pixels.push(y * 32);
pixels.push(128);
}
}
let result = LossyConfig::new(2.0)
.with_gaborish(false)
.encode_request(8, 8, PixelLayout::Rgb8)
.encode(&pixels);
assert!(result.is_ok());
let jxl = result.unwrap();
assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
}
#[test]
fn test_fluent_lossless() {
let pixels = vec![128u8; 4 * 4 * 3];
let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Rgb8);
assert!(result.is_ok());
}
#[test]
fn test_lossy_gray8() {
let pixels = vec![128u8; 8 * 8];
let result = LossyConfig::new(2.0)
.with_gaborish(false)
.encode_request(8, 8, PixelLayout::Gray8)
.encode(&pixels);
assert!(result.is_ok(), "lossy Gray8 should encode: {result:?}");
}
#[test]
fn test_lossy_gray_alpha8() {
let pixels: Vec<u8> = (0..8 * 8).flat_map(|_| [128u8, 255]).collect();
let result = LossyConfig::new(2.0)
.with_gaborish(false)
.encode_request(8, 8, PixelLayout::GrayAlpha8)
.encode(&pixels);
assert!(result.is_ok(), "lossy GrayAlpha8 should encode: {result:?}");
}
#[test]
fn test_lossy_gray16() {
let pixels_u16: Vec<u16> = (0..8 * 8).map(|_| 32768u16).collect();
let pixels: &[u8] = bytemuck::cast_slice(&pixels_u16);
let result = LossyConfig::new(2.0)
.with_gaborish(false)
.encode_request(8, 8, PixelLayout::Gray16)
.encode(pixels);
assert!(result.is_ok(), "lossy Gray16 should encode: {result:?}");
}
#[test]
fn test_lossy_rgba_linear_f32() {
let pixels_f32: Vec<f32> = (0..8 * 8).flat_map(|_| [0.5f32, 0.3, 0.7, 1.0]).collect();
let pixels: &[u8] = bytemuck::cast_slice(&pixels_f32);
let result = LossyConfig::new(2.0)
.with_gaborish(false)
.encode_request(8, 8, PixelLayout::RgbaLinearF32)
.encode(pixels);
assert!(
result.is_ok(),
"lossy RgbaLinearF32 should encode: {result:?}"
);
}
#[test]
fn test_lossy_gray_linear_f32() {
let pixels_f32: Vec<f32> = (0..8 * 8).map(|_| 0.5f32).collect();
let pixels: &[u8] = bytemuck::cast_slice(&pixels_f32);
let result = LossyConfig::new(2.0)
.with_gaborish(false)
.encode_request(8, 8, PixelLayout::GrayLinearF32)
.encode(pixels);
assert!(
result.is_ok(),
"lossy GrayLinearF32 should encode: {result:?}"
);
}
#[test]
fn test_lossless_grayalpha8() {
let pixels: Vec<u8> = (0..8 * 8).flat_map(|_| [200u8, 255]).collect();
let result = LosslessConfig::new().encode(&pixels, 8, 8, PixelLayout::GrayAlpha8);
assert!(
result.is_ok(),
"lossless GrayAlpha8 should encode: {result:?}"
);
}
#[test]
fn test_lossless_grayalpha16() {
let pixels_u16: Vec<u16> = (0..8 * 8).flat_map(|_| [32768u16, 65535]).collect();
let pixels: &[u8] = bytemuck::cast_slice(&pixels_u16);
let result = LosslessConfig::new().encode(pixels, 8, 8, PixelLayout::GrayAlpha16);
assert!(
result.is_ok(),
"lossless GrayAlpha16 should encode: {result:?}"
);
}
#[test]
fn test_bgra_lossless() {
let pixels = [0u8, 0, 255, 255].repeat(16);
let result = LosslessConfig::new().encode(&pixels, 4, 4, PixelLayout::Bgra8);
assert!(result.is_ok());
let jxl = result.unwrap();
assert_eq!(&jxl[..2], &[0xFF, 0x0A]);
}
#[test]
fn test_lossy_alpha_encodes() {
let pixels = [255u8, 0, 0, 255].repeat(64);
let result =
LossyConfig::new(2.0)
.with_gaborish(false)
.encode(&pixels, 8, 8, PixelLayout::Bgra8);
assert!(
result.is_ok(),
"BGRA lossy encode failed: {:?}",
result.err()
);
let result2 = LossyConfig::new(2.0).encode(&pixels, 8, 8, PixelLayout::Rgba8);
assert!(
result2.is_ok(),
"RGBA lossy encode failed: {:?}",
result2.err()
);
}
#[test]
fn test_stop_cancellation() {
use enough::Unstoppable;
let pixels = vec![128u8; 4 * 4 * 3];
let cfg = LosslessConfig::new();
let result = cfg
.encode_request(4, 4, PixelLayout::Rgb8)
.with_stop(&Unstoppable)
.encode(&pixels);
assert!(result.is_ok());
}
#[test]
fn test_lossy_palette_encode() {
let colors = [[255u8, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]];
let mut pixels = Vec::with_capacity(16 * 16 * 3);
for y in 0..16u8 {
for x in 0..16u8 {
let ci = ((y / 4) * 4 + x / 4) as usize % 4;
let noise = ((x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13))) % 5) as i16 - 2;
for &channel in &colors[ci][..3] {
let v = (channel as i16 + noise).clamp(0, 255) as u8;
pixels.push(v);
}
}
}
let cfg = LosslessConfig::new()
.with_lossy_palette(true)
.with_ans(true);
let result = cfg.encode(&pixels, 16, 16, PixelLayout::Rgb8);
assert!(
result.is_ok(),
"lossy palette encode failed: {:?}",
result.err()
);
let jxl = result.unwrap();
assert_eq!(&jxl[..2], &[0xFF, 0x0A], "JXL signature");
let cursor = std::io::Cursor::new(&jxl);
let reader = std::io::BufReader::new(cursor);
let image = jxl_oxide::JxlImage::builder()
.read(reader)
.expect("jxl-oxide parse");
assert!(
image.width() > 0,
"decoded image should have non-zero width"
);
}
#[test]
fn test_lossy_palette_multi_group() {
let colors = [
[255u8, 0, 0],
[0, 255, 0],
[0, 0, 255],
[255, 255, 0],
[255, 0, 255],
[0, 255, 255],
[128, 128, 128],
[64, 64, 64],
];
let mut pixels = Vec::with_capacity(300 * 300 * 3);
for y in 0..300u32 {
for x in 0..300u32 {
let ci = ((y / 40) * 8 + x / 40) as usize % colors.len();
let noise = ((x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13))) % 7) as i16 - 3;
for &channel in &colors[ci][..3] {
let v = (channel as i16 + noise).clamp(0, 255) as u8;
pixels.push(v);
}
}
}
let cfg = LosslessConfig::new()
.with_lossy_palette(true)
.with_ans(true);
let jxl = cfg
.encode(&pixels, 300, 300, PixelLayout::Rgb8)
.expect("lossy palette multi-group encode");
assert_eq!(&jxl[..2], &[0xFF, 0x0A], "JXL signature");
assert!(jxl.len() < 300 * 300 * 3, "should compress");
let out = crate::test_helpers::output_dir("lossy_palette");
let jxl_out = out.join("lossy_palette_multi.jxl");
let png_out = out.join("lossy_palette_multi.png");
std::fs::write(&jxl_out, &jxl).ok();
eprintln!(
"LOSSY_PALETTE_MULTI test: encoded {} bytes ({}x{})",
jxl.len(),
300,
300
);
let djxl_result = std::process::Command::new("djxl")
.args([jxl_out.to_str().unwrap(), png_out.to_str().unwrap()])
.output();
if let Ok(output) = djxl_result {
eprintln!(
"djxl: status={}, stderr={}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
let decoded = crate::test_helpers::decode_with_jxl_rs(&jxl).expect("jxl-rs decode failed");
assert_eq!(decoded.width, 300);
assert_eq!(decoded.height, 300);
assert_eq!(decoded.channels, 3);
let mut max_error = 0i32;
let mut error_pos = (0, 0, 0);
for (i, (&orig, &dec)) in pixels.iter().zip(decoded.pixels.iter()).enumerate() {
let dec_u8 = (dec * 255.0).round().clamp(0.0, 255.0) as u8;
let diff = (orig as i32 - dec_u8 as i32).abs();
if diff > max_error {
max_error = diff;
let pixel = i / 3;
error_pos = (pixel % 300, pixel / 300, i % 3);
}
}
let err_idx = error_pos.1 * 300 * 3 + error_pos.0 * 3 + error_pos.2;
let dec_u8 = (decoded.pixels[err_idx] * 255.0).round().clamp(0.0, 255.0) as u8;
eprintln!(
"max_error={} at ({},{}) ch={}, orig={} decoded={}",
max_error, error_pos.0, error_pos.1, error_pos.2, pixels[err_idx], dec_u8,
);
assert!(
max_error <= 80,
"lossy palette max error {} too large (expected <= 80)",
max_error
);
}
#[test]
fn test_palette_256_colors_regression() {
use crate::modular::channel::{Channel, ModularImage};
use crate::modular::encode::write_modular_stream_with_palette;
let mut pixels = Vec::with_capacity(32 * 32 * 3);
for i in 0..1024u32 {
let idx = (i / 4) as u8;
pixels.push(idx);
pixels.push(((idx as u32 * 7 + 13) & 0xFF) as u8);
pixels.push(((idx as u32 * 31 + 97) & 0xFF) as u8);
}
let cfg = LosslessConfig::new().with_ans(true);
let jxl = cfg
.encode(&pixels, 32, 32, PixelLayout::Rgb8)
.expect("palette 256-colors encode");
let decoded = crate::test_helpers::decode_with_jxl_rs(&jxl).expect("jxl-rs decode failed");
for (i, (&orig, &dec)) in pixels.iter().zip(decoded.pixels.iter()).enumerate() {
let dec_u8 = (dec * 255.0).round().clamp(0.0, 255.0) as u8;
assert_eq!(
orig, dec_u8,
"32x32: mismatch at byte {}: orig={} decoded={}",
i, orig, dec_u8
);
}
let mut channels = Vec::new();
for c in 0..3 {
let mut ch = Channel::new(16, 16).unwrap();
for y in 0..16 {
for x in 0..16 {
let idx = y * 16 + x;
let val = match c {
0 => idx as i32,
1 => ((idx * 3 + 17) & 0xFF) as i32,
2 => (255 - idx) as i32,
_ => 0,
};
ch.set(x, y, val);
}
}
channels.push(ch);
}
let image = ModularImage {
channels,
bit_depth: 8,
is_grayscale: false,
has_alpha: false,
};
let mut writer = crate::bit_writer::BitWriter::new();
write_modular_stream_with_palette(&image, &mut writer, true, 0, 3)
.expect("palette encode with 256 unique colors must not fail");
}
#[test]
fn test_16bit_tree_learning() {
for &(w, h, layout, label) in &[
(32u32, 32u32, PixelLayout::Rgb16, "32x32 RGB16"),
(8, 8, PixelLayout::Rgba16, "8x8 RGBA16"),
(8, 8, PixelLayout::Rgb16, "8x8 RGB16"),
(16, 16, PixelLayout::Gray16, "16x16 Gray16"),
] {
let nc = layout.bytes_per_pixel()
/ if layout.is_16bit() {
2
} else if layout.is_f32() {
4
} else {
1
};
let mut pixels = vec![0u16; (w * h) as usize * nc];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) as usize) * nc;
pixels[idx] = (x * 2048) as u16;
if nc >= 2 {
pixels[idx + 1] = (y * 2048) as u16;
}
if nc >= 3 {
pixels[idx + 2] = ((x + y) * 1024) as u16;
}
if nc >= 4 {
pixels[idx + 3] = 65535; }
}
}
let bytes: Vec<u8> = pixels.iter().flat_map(|v| v.to_ne_bytes()).collect();
let cfg = LosslessConfig::new().with_effort(7).with_ans(true);
let jxl = cfg
.encode(&bytes, w, h, layout)
.unwrap_or_else(|e| panic!("{}: encode failed: {}", label, e));
let decoded = crate::test_helpers::decode_with_jxl_rs(&jxl)
.unwrap_or_else(|e| panic!("{}: jxl-rs decode failed: {}", label, e));
assert_eq!(decoded.width, w as usize, "{}: width", label);
assert_eq!(decoded.height, h as usize, "{}: height", label);
let scale = 65535.0;
let mut mismatches = 0;
for (i, (&orig, &dec_f)) in pixels.iter().zip(decoded.pixels.iter()).enumerate() {
let dec = (dec_f * scale).round().clamp(0.0, scale) as u16;
if orig != dec && mismatches < 3 {
eprintln!("{}: mismatch[{}]: orig={} dec={}", label, i, orig, dec);
mismatches += 1;
}
}
assert_eq!(mismatches, 0, "{}: {} mismatches", label, mismatches);
eprintln!("{}: PASS ({} bytes)", label, jxl.len());
}
}
#[test]
fn test_srgb_lut_matches_powf() {
for i in 0u16..256 {
let lut_val = SRGB_U8_TO_LINEAR[i as usize];
let fast_val = srgb_to_linear_f(i as f32 / 255.0);
let diff = (lut_val - fast_val).abs();
let tol = fast_val.abs() * 5e-5 + 1e-7;
assert!(
diff <= tol,
"sRGB LUT mismatch at {i}: LUT={lut_val}, fast={fast_val}, diff={diff}"
);
}
}
#[test]
fn test_quality_to_distance_f32_mapping() {
assert_eq!(quality_to_distance(100.0), 0.0);
assert_eq!(quality_to_distance(90.0), 1.0); assert_eq!(quality_to_distance(80.0), 1.5);
assert_eq!(quality_to_distance(70.0), 2.0);
assert_eq!(quality_to_distance(50.0), 4.0);
assert_eq!(quality_to_distance(0.0), 9.0);
assert_eq!(quality_to_distance(110.0), 0.0);
}
#[test]
fn test_calibrated_jxl_quality() {
assert_eq!(calibrated_jxl_quality(0.0), 5.0);
assert_eq!(calibrated_jxl_quality(100.0), 93.8);
assert_eq!(calibrated_jxl_quality(90.0), 84.2);
let mid = calibrated_jxl_quality(52.5);
let expected = 48.5 + 0.5 * (51.9 - 48.5);
assert!(
(mid - expected).abs() < 0.01,
"expected {expected}, got {mid}"
);
}
#[test]
fn test_interp_quality_edge_cases() {
let table = &[(10.0f32, 20.0f32), (20.0, 40.0), (30.0, 60.0)];
assert_eq!(interp_quality(table, 5.0), 20.0);
assert_eq!(interp_quality(table, 35.0), 60.0);
assert_eq!(interp_quality(table, 20.0), 40.0);
assert!((interp_quality(table, 15.0) - 30.0).abs() < 0.001);
}
}