use crate::bit_writer::BitWriter;
use crate::color::{lut_high_bit, srgb_to_linear_f32, srgb_to_linear_u8};
use crate::color_encoding::write_color_encoding_with_icc;
use crate::enc_frame::encode_frame;
use crate::enc_lossless::{encode_frame_lossless, encode_frame_lossless_float, forward_ycocg};
use crate::image::{Image3F, Image3Si};
use crate::orientation::Orientation;
use crate::{ColorEncoding, EncodeError};
fn checked_buffer_size<T>(
width: usize,
height: usize,
channels: usize,
) -> Result<usize, EncodeError> {
let pixel_size = size_of::<T>();
let total_size = width
.checked_mul(height)
.and_then(|v| v.checked_mul(channels));
_ = total_size
.and_then(|v| v.checked_mul(pixel_size))
.and_then(|v| isize::try_from(v).ok())
.map(|v| v as usize)
.ok_or(EncodeError::DimensionTooLarge {
width: height,
height,
})?;
total_size
.and_then(|v| isize::try_from(v).ok())
.map(|v| v as usize)
.ok_or(EncodeError::DimensionTooLarge {
width: height,
height,
})
}
#[derive(Debug, Clone)]
pub(crate) enum AlphaPlane {
U8(Vec<u8>),
U16 { data: Vec<u16>, bits: u8 },
F32(Vec<i32>),
}
const CODESTREAM_MARKER: u8 = 0x0A;
const MIN_DISTANCE: f32 = 0.03;
pub(crate) const MAX_DIMENSION: usize = 0x3FFF_FFFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum BitsPerSample {
#[default]
Eight,
Ten,
Twelve,
Sixteen,
F16,
F32,
}
impl BitsPerSample {
pub(crate) fn bits(self) -> u32 {
match self {
BitsPerSample::Eight => 8,
BitsPerSample::Ten => 10,
BitsPerSample::Twelve => 12,
BitsPerSample::Sixteen => 16,
BitsPerSample::F16 => 16,
BitsPerSample::F32 => 32,
}
}
pub(crate) fn is_float(self) -> bool {
matches!(self, BitsPerSample::F16 | BitsPerSample::F32)
}
pub(crate) fn exp_bits(self) -> u32 {
match self {
BitsPerSample::F16 => 5,
BitsPerSample::F32 => 8,
_ => 0,
}
}
}
impl AlphaPlane {
#[inline]
pub(crate) fn from_u8(data: Vec<u8>) -> Self {
Self::U8(data)
}
#[inline]
pub(crate) fn from_u16_10bit(data: Vec<u16>) -> Self {
Self::U16 { data, bits: 10 }
}
#[inline]
pub(crate) fn from_u16_12bit(data: Vec<u16>) -> Self {
Self::U16 { data, bits: 12 }
}
#[inline]
pub(crate) fn from_u16_16bit(data: Vec<u16>) -> Self {
Self::U16 { data, bits: 16 }
}
#[inline]
pub(crate) fn len(&self) -> usize {
match self {
Self::U8(v) => v.len(),
Self::U16 { data, .. } => data.len(),
Self::F32(v) => v.len(),
}
}
#[inline]
pub(crate) fn bits(&self) -> u8 {
match self {
Self::U8(_) => 8,
Self::U16 { bits, .. } => *bits,
Self::F32(_) => 32,
}
}
#[inline]
pub(crate) fn is_float(&self) -> bool {
matches!(self, Self::F32(_))
}
#[inline]
pub(crate) fn get_i32(&self, idx: usize) -> i32 {
match self {
Self::U8(v) => v[idx] as i32,
Self::U16 { data, .. } => data[idx] as i32,
Self::F32(v) => v[idx],
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ToneMappingParams {
pub intensity_target: Option<f32>,
pub min_nits: f32,
pub relative_to_max_display: bool,
pub linear_below: f32,
}
impl ToneMappingParams {
fn is_default(&self) -> bool {
self.intensity_target.is_none()
&& self.min_nits == 0.0
&& !self.relative_to_max_display
&& self.linear_below == 0.0
}
}
#[derive(Debug, Clone)]
pub struct EncodeConfig {
pub distance: f32,
pub color_encoding: ColorEncoding,
pub icc_profile: Option<Vec<u8>>,
pub exif: Option<Vec<u8>>,
pub orientation: Orientation,
pub lossless: bool,
pub progressive: bool,
pub progressive_passes: Option<u32>,
pub progressive_shifts: Option<Vec<u32>>,
pub intensity_target: Option<f32>,
pub min_nits: f32,
pub relative_to_max_display: bool,
pub linear_below: f32,
pub num_threads: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct EncodeConfigImpl {
pub(crate) distance: f32,
pub(crate) color_encoding: ColorEncoding,
pub(crate) icc_profile: Option<Vec<u8>>,
pub(crate) exif: Option<Vec<u8>>,
pub(crate) orientation: Orientation,
pub(crate) alpha: Option<AlphaPlane>,
pub(crate) bits_per_sample: BitsPerSample,
pub(crate) lossless: bool,
pub(crate) progressive: bool,
pub(crate) progressive_passes: Option<u32>,
pub(crate) progressive_shifts: Option<Vec<u32>>,
pub(crate) grayscale: bool,
pub(crate) intensity_target: Option<f32>,
pub(crate) min_nits: f32,
pub(crate) relative_to_max_display: bool,
pub(crate) linear_below: f32,
pub(crate) num_threads: usize,
}
impl Default for EncodeConfig {
fn default() -> Self {
Self {
distance: 1.0,
color_encoding: ColorEncoding::default(),
icc_profile: None,
exif: None,
orientation: Orientation::Normal,
lossless: false,
progressive: false,
progressive_passes: None,
progressive_shifts: None,
intensity_target: None,
min_nits: 0.0,
relative_to_max_display: false,
linear_below: 0.0,
num_threads: 1,
}
}
}
impl Default for EncodeConfigImpl {
fn default() -> Self {
Self {
distance: 1.0,
color_encoding: ColorEncoding::default(),
icc_profile: None,
exif: None,
orientation: Orientation::Normal,
alpha: None,
bits_per_sample: BitsPerSample::Eight,
lossless: false,
progressive: false,
grayscale: false,
progressive_passes: None,
progressive_shifts: None,
intensity_target: None,
min_nits: 0.0,
relative_to_max_display: false,
linear_below: 0.0,
num_threads: 1,
}
}
}
impl EncodeConfigImpl {
pub(crate) fn with_distance(distance: f32) -> Self {
Self {
distance,
..Self::default()
}
}
pub(crate) fn with_num_threads(mut self, n: usize) -> Self {
self.num_threads = n;
self
}
pub(crate) fn with_icc_profile(mut self, icc: Option<Vec<u8>>) -> Self {
self.icc_profile = icc;
self
}
pub(crate) fn with_exif(mut self, exif: Option<Vec<u8>>) -> Self {
self.exif = exif;
self
}
pub(crate) fn with_orientation(mut self, orientation: Orientation) -> Self {
self.orientation = orientation;
self
}
pub(crate) fn with_alpha(mut self, alpha: AlphaPlane) -> Self {
self.alpha = Some(alpha);
self
}
pub(crate) fn with_bits_per_sample(mut self, bps: BitsPerSample) -> Self {
self.bits_per_sample = bps;
self
}
pub(crate) fn with_lossless(mut self, lossless: bool) -> Self {
self.lossless = lossless;
self
}
pub(crate) fn with_progressive(mut self, progressive: bool) -> Self {
self.progressive = progressive;
self
}
pub(crate) fn with_progressive_passes(mut self, passes: Option<u32>) -> Self {
self.progressive_passes = passes;
self
}
pub(crate) fn with_progressive_shifts(mut self, shifts: Option<Vec<u32>>) -> Self {
self.progressive_shifts = shifts;
self
}
pub(crate) fn with_progressive_from(self, config: &EncodeConfig) -> Self {
self.with_progressive(config.progressive)
.with_progressive_passes(config.progressive_passes)
.with_progressive_shifts(config.progressive_shifts.clone())
}
pub(crate) fn with_grayscale(mut self, grayscale: bool) -> Self {
self.grayscale = grayscale;
self
}
pub(crate) fn with_color_encoding(mut self, enc: ColorEncoding) -> Self {
self.color_encoding = enc;
self
}
pub(crate) fn tone_mapping(&self) -> ToneMappingParams {
ToneMappingParams {
intensity_target: self.intensity_target,
min_nits: self.min_nits,
relative_to_max_display: self.relative_to_max_display,
linear_below: self.linear_below,
}
}
pub(crate) fn with_intensity_target(mut self, nits: Option<f32>) -> Self {
self.intensity_target = nits;
self
}
}
impl EncodeConfig {
pub fn with_distance(mut self, distance: f32) -> Self {
self.distance = distance;
self
}
pub fn with_quality(self, quality: f32) -> Self {
self.with_distance(distance_from_quality(quality))
}
pub fn with_color_encoding(mut self, enc: ColorEncoding) -> Self {
self.color_encoding = enc;
self
}
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 = nits;
self
}
pub fn with_relative_to_max_display(mut self, rel: bool) -> Self {
self.relative_to_max_display = rel;
self
}
pub fn with_linear_below(mut self, v: f32) -> Self {
self.linear_below = v;
self
}
pub(crate) fn tone_mapping(&self) -> ToneMappingParams {
ToneMappingParams {
intensity_target: self.intensity_target,
min_nits: self.min_nits,
relative_to_max_display: self.relative_to_max_display,
linear_below: self.linear_below,
}
}
pub fn with_icc_profile(mut self, icc: Vec<u8>) -> Self {
self.icc_profile = Some(icc);
self
}
pub fn with_exif(mut self, exif: Vec<u8>) -> Self {
self.exif = Some(exif);
self
}
pub fn with_orientation(mut self, orientation: Orientation) -> Self {
self.orientation = orientation;
self
}
pub fn with_lossless(mut self, lossless: bool) -> Self {
self.lossless = lossless;
self
}
pub fn with_progressive(mut self, progressive: bool) -> Self {
self.progressive = progressive;
self
}
pub fn with_num_threads(mut self, n: usize) -> Self {
self.num_threads = n;
self
}
}
pub fn distance_from_quality(quality: f32) -> f32 {
assert!(!quality.is_nan(), "quality must not be NaN");
let q = quality.clamp(0.0, 100.0);
let d = if q >= 99.0 {
0.05 * 2.0f32.powf(100.0 - q)
} else if q >= 90.0 {
10.0f32.powf((99.0 - q) / 9.0 - 1.0)
} else if q >= 30.0 {
0.1 + (100.0 - q) * 0.09
} else {
6.24 + 2.5f32.powf((30.0 - q) / 5.0) / 6.25
};
d.min(25.0)
}
pub fn encode_image(
input: &[u8],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if width == 0 || height == 0 {
return Err(EncodeError::EmptyImage);
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(EncodeError::DimensionTooLarge { width, height });
}
let expected = checked_buffer_size::<u8>(width, height, 3)?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected,
actual: input.len(),
});
}
if !config.distance.is_finite() || config.distance <= 0.0 {
return Err(EncodeError::InvalidDistance(config.distance));
}
if config.lossless {
return encode_with_config_loseless(
input,
width,
height,
false,
8,
&EncodeConfigImpl::with_distance(config.distance)
.with_lossless(config.lossless)
.with_progressive(config.progressive)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
);
}
let distance = config.distance.max(MIN_DISTANCE);
let mut linear = Image3F::new(width, height);
for (y, row) in input.chunks_exact(width * 3).enumerate() {
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for (((r, g), b), src) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<3>().0.iter())
{
*r = srgb_to_linear_u8(src[0]);
*g = srgb_to_linear_u8(src[1]);
*b = srgb_to_linear_u8(src[2]);
}
}
encode_with_config(
&linear,
&EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
)
}
pub fn encode_image_with_alpha(
input: &[u8],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if width == 0 || height == 0 {
return Err(EncodeError::EmptyImage);
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(EncodeError::DimensionTooLarge { width, height });
}
let expected = checked_buffer_size::<u8>(width, height, 4)?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected,
actual: input.len(),
});
}
if !config.distance.is_finite() || config.distance <= 0.0 {
return Err(EncodeError::InvalidDistance(config.distance));
}
if config.lossless {
return encode_with_config_loseless(
input,
width,
height,
true,
8,
&EncodeConfigImpl::with_distance(config.distance)
.with_lossless(config.lossless)
.with_progressive(config.progressive)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
);
}
let distance = config.distance.max(MIN_DISTANCE);
let mut linear = Image3F::new(width, height);
let mut alpha_plane = vec![0u8; width * height];
for (y, (row, alpha_row)) in input
.chunks_exact(width * 4)
.zip(alpha_plane.chunks_exact_mut(width))
.enumerate()
{
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for ((((r, g), b), src), alpha) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<4>().0.iter())
.zip(alpha_row.iter_mut())
{
*r = srgb_to_linear_u8(src[0]);
*g = srgb_to_linear_u8(src[1]);
*b = srgb_to_linear_u8(src[2]);
*alpha = src[3];
}
}
encode_with_config(
&linear,
&EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_alpha(AlphaPlane::from_u8(alpha_plane))
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
)
}
pub fn encode_image_with_alpha_10bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_high_depth_rgba(input, width, height, true, config, BitsPerSample::Ten)
}
pub fn encode_image_with_alpha_12bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_high_depth_rgba(input, width, height, true, config, BitsPerSample::Twelve)
}
pub fn encode_image_with_alpha_16bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_high_depth_rgba(input, width, height, true, config, BitsPerSample::Sixteen)
}
pub fn encode_image_16bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_high_depth_rgba(input, width, height, false, config, BitsPerSample::Sixteen)
}
pub fn encode_image_10bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_high_depth_rgba(input, width, height, false, config, BitsPerSample::Ten)
}
pub fn encode_image_12bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_high_depth_rgba(input, width, height, false, config, BitsPerSample::Twelve)
}
pub fn encode_image_gray(
input: &[u8],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_gray_impl(input, None, width, height, config)
}
pub fn encode_image_gray_alpha(
input: &[u8],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
let expected = checked_buffer_size::<u8>(width, height, 2)?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected: width * height * 2,
actual: input.len(),
});
}
let (luma, alpha): (Vec<u8>, Vec<u8>) = input
.as_chunks::<2>()
.0
.iter()
.map(|px| (px[0], px[1]))
.unzip();
encode_gray_impl(&luma, Some(alpha), width, height, config)
}
fn encode_gray_impl(
luma: &[u8],
alpha: Option<Vec<u8>>,
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if width == 0 || height == 0 {
return Err(EncodeError::EmptyImage);
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(EncodeError::DimensionTooLarge { width, height });
}
let expected = checked_buffer_size::<u8>(width, height, 1)?;
if luma.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected: width * height,
actual: luma.len(),
});
}
if !config.distance.is_finite() || config.distance <= 0.0 {
return Err(EncodeError::InvalidDistance(config.distance));
}
if config.lossless {
let nchan = if alpha.is_some() { 4 } else { 3 };
let mut interleaved = vec![0u8; width * height * nchan];
match alpha.as_ref() {
None => {
for (out, &v) in interleaved
.as_chunks_mut::<3>()
.0
.iter_mut()
.zip(luma.iter())
{
out[0] = v;
out[1] = v;
out[2] = v;
}
}
Some(a) => {
for (out, (&v, &av)) in interleaved
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(luma.iter().zip(a.iter()))
{
out[0] = v;
out[1] = v;
out[2] = v;
out[3] = av;
}
}
}
return encode_with_config_loseless(
&interleaved,
width,
height,
alpha.is_some(),
8,
&EncodeConfigImpl::with_distance(config.distance)
.with_lossless(true)
.with_grayscale(true)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
);
}
let distance = config.distance.max(MIN_DISTANCE);
let mut linear = Image3F::new(width, height);
for (y, row) in luma.chunks_exact(width).enumerate() {
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for (((r, g), b), &v) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.iter())
{
let lin = srgb_to_linear_u8(v);
*r = lin;
*g = lin;
*b = lin;
}
}
let mut cfg = EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_grayscale(true)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding);
if let Some(a) = alpha {
cfg = cfg.with_alpha(AlphaPlane::from_u8(a));
}
encode_with_config(&linear, &cfg)
}
pub fn encode_image_gray_10bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_gray_high_depth_impl(input, None, width, height, config, BitsPerSample::Ten)
}
pub fn encode_image_gray_12bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_gray_high_depth_impl(input, None, width, height, config, BitsPerSample::Twelve)
}
pub fn encode_image_gray_alpha_10bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
let expected = checked_buffer_size::<u16>(width, height, 2)?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected: width * height * 2,
actual: input.len(),
});
}
let (luma, alpha): (Vec<u16>, Vec<u16>) = input
.as_chunks::<2>()
.0
.iter()
.map(|px| (px[0], px[1]))
.unzip();
encode_gray_high_depth_impl(
&luma,
Some(alpha),
width,
height,
config,
BitsPerSample::Ten,
)
}
pub fn encode_image_gray_alpha_12bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
let expected = checked_buffer_size::<u16>(width, height, 2)?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected: width * height * 2,
actual: input.len(),
});
}
let (luma, alpha): (Vec<u16>, Vec<u16>) = input
.as_chunks::<2>()
.0
.iter()
.map(|px| (px[0], px[1]))
.unzip();
encode_gray_high_depth_impl(
&luma,
Some(alpha),
width,
height,
config,
BitsPerSample::Twelve,
)
}
pub fn encode_image_gray_16bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_gray_high_depth_impl(input, None, width, height, config, BitsPerSample::Sixteen)
}
pub fn encode_image_gray_alpha_16bit(
input: &[u16],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
let expected = checked_buffer_size::<u16>(width, height, 2)?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected: width * height * 2,
actual: input.len(),
});
}
let (luma, alpha): (Vec<u16>, Vec<u16>) = input
.as_chunks::<2>()
.0
.iter()
.map(|px| (px[0], px[1]))
.unzip();
encode_gray_high_depth_impl(
&luma,
Some(alpha),
width,
height,
config,
BitsPerSample::Sixteen,
)
}
fn encode_gray_high_depth_impl(
luma: &[u16],
alpha: Option<Vec<u16>>,
width: usize,
height: usize,
config: &EncodeConfig,
bps: BitsPerSample,
) -> Result<Vec<u8>, EncodeError> {
if width == 0 || height == 0 {
return Err(EncodeError::EmptyImage);
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(EncodeError::DimensionTooLarge { width, height });
}
let expected = checked_buffer_size::<u16>(width, height, 1)?;
if luma.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected: width * height,
actual: luma.len(),
});
}
if let Some(alpha) = alpha.as_ref()
&& alpha.len() != expected
{
return Err(EncodeError::InputSizeMismatch {
expected: width * height,
actual: luma.len(),
});
}
if !config.distance.is_finite() || config.distance <= 0.0 {
return Err(EncodeError::InvalidDistance(config.distance));
}
if config.lossless {
let nchan = if alpha.is_some() { 4 } else { 3 };
let mut interleaved = vec![0u16; width * height * nchan];
match alpha.as_ref() {
None => {
for (out, &v) in interleaved
.as_chunks_mut::<3>()
.0
.iter_mut()
.zip(luma.iter())
{
out[0] = v;
out[1] = v;
out[2] = v;
}
}
Some(a) => {
for (out, (&v, &av)) in interleaved
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(luma.iter().zip(a.iter()))
{
out[0] = v;
out[1] = v;
out[2] = v;
out[3] = av;
}
}
}
return encode_with_config_loseless(
&interleaved,
width,
height,
alpha.is_some(),
bps.bits() as u8,
&EncodeConfigImpl::with_distance(config.distance)
.with_lossless(true)
.with_grayscale(true)
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
);
}
let distance = config.distance.max(MIN_DISTANCE);
let lut = &lut_high_bit(bps.bits() as u8).table;
let mut linear = Image3F::new(width, height);
for (y, row) in luma.chunks_exact(width).enumerate() {
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for (((r, g), b), &v) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.iter())
{
let lin = lut[v as usize];
*r = lin;
*g = lin;
*b = lin;
}
}
let alpha_plane = alpha.map(|a| match bps {
BitsPerSample::Ten => AlphaPlane::from_u16_10bit(a),
BitsPerSample::Twelve => AlphaPlane::from_u16_12bit(a),
BitsPerSample::Sixteen => AlphaPlane::from_u16_16bit(a),
BitsPerSample::Eight => unreachable!("high-depth gray path called with 8-bit bps"),
BitsPerSample::F16 | BitsPerSample::F32 => {
unreachable!("float path does not use the integer alpha match")
}
});
let mut cfg = EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_grayscale(true)
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding);
if let Some(ap) = alpha_plane {
cfg = cfg.with_alpha(ap);
}
encode_with_config(&linear, &cfg)
}
fn encode_high_depth_rgba(
input: &[u16],
width: usize,
height: usize,
has_alpha: bool,
config: &EncodeConfig,
bps: BitsPerSample,
) -> Result<Vec<u8>, EncodeError> {
let expected = checked_buffer_size::<u16>(width, height, if has_alpha { 4 } else { 3 })?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected,
actual: input.len(),
});
}
if config.lossless {
return encode_with_config_loseless(
input,
width,
height,
has_alpha,
bps.bits() as u8,
&EncodeConfigImpl::with_distance(config.distance)
.with_lossless(config.lossless)
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
);
}
let distance = config.distance.max(MIN_DISTANCE);
let mut linear = Image3F::new(width, height);
let lut = &lut_high_bit(bps.bits() as u8).table;
let bp_max: u16 = if bps.bits() >= 16 {
u16::MAX
} else {
((1u32 << bps.bits()) - 1) as u16
};
if has_alpha {
let mut alpha_plane = vec![0u16; width * height];
for (y, (row, alpha_row)) in input
.chunks_exact(width * 4)
.zip(alpha_plane.chunks_exact_mut(width))
.enumerate()
{
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for ((((r, g), b), src), alpha) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<4>().0.iter())
.zip(alpha_row.iter_mut())
{
*r = lut[src[0] as usize];
*g = lut[src[1] as usize];
*b = lut[src[2] as usize];
*alpha = src[3].min(bp_max);
}
}
encode_with_config(
&linear,
&EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_alpha(match bps {
BitsPerSample::Ten => AlphaPlane::from_u16_10bit(alpha_plane),
BitsPerSample::Twelve => AlphaPlane::from_u16_12bit(alpha_plane),
BitsPerSample::Sixteen => AlphaPlane::from_u16_16bit(alpha_plane),
BitsPerSample::Eight => unreachable!("high-depth path called with 8-bit bps"),
BitsPerSample::F16 | BitsPerSample::F32 => {
unreachable!("float path does not use the integer alpha match")
}
})
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
)
} else {
for (y, row) in input.chunks_exact(width * 3).enumerate() {
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for (((r, g), b), src) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<3>().0.iter())
{
*r = lut[src[0] as usize];
*g = lut[src[1] as usize];
*b = lut[src[2] as usize];
}
}
encode_with_config(
&linear,
&EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
)
}
}
fn encode_f32_lossless_rgba(
input: &[f32],
width: usize,
height: usize,
has_alpha: bool,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
if width == 0 || height == 0 {
return Err(EncodeError::EmptyImage);
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(EncodeError::DimensionTooLarge { width, height });
}
for &v in input.iter() {
if !v.is_finite() || v < 0.0 {
return Err(EncodeError::Unsupported(
"f32 lossless v1 supports only finite non-negative values",
));
}
}
let nchan = if has_alpha { 4 } else { 3 };
let mut image3s = Image3Si::new(width, height);
let mut alpha_bits: Vec<i32> = if has_alpha {
Vec::with_capacity(width * height)
} else {
Vec::new()
};
for (y, row) in input.chunks_exact(width * nchan).enumerate() {
let [r_row, g_row, b_row] = image3s.all_plane_rows_mut(y);
for (x, src) in row.chunks_exact(nchan).enumerate() {
r_row[x] = src[0].to_bits() as i32;
g_row[x] = src[1].to_bits() as i32;
b_row[x] = src[2].to_bits() as i32;
if has_alpha {
alpha_bits.push(src[3].to_bits() as i32);
}
}
}
let alpha = if has_alpha {
Some(AlphaPlane::F32(alpha_bits))
} else {
None
};
let mut w = BitWriter::new();
w.write(8, 0xFF);
w.write(8, CODESTREAM_MARKER as u64);
write_size_header(width, height, &mut w);
write_image_metadata(
config.tone_mapping(),
&config.color_encoding,
alpha.as_ref(),
config.icc_profile.as_deref(),
BitsPerSample::F32,
true,
false,
config.orientation,
&mut w,
);
encode_frame_lossless_float(&image3s, alpha.as_ref(), &mut w);
let codestream = w.into_bytes();
let alpha_bits_md = if has_alpha { 32 } else { 0 };
Ok(finalize_container(
codestream,
config.exif.as_deref(),
needs_level_10(32, true, alpha_bits_md),
))
}
fn encode_float_rgba(
input: &[f32],
width: usize,
height: usize,
has_alpha: bool,
config: &EncodeConfig,
bps: BitsPerSample,
) -> Result<Vec<u8>, EncodeError> {
let expected = checked_buffer_size::<f32>(width, height, if has_alpha { 4 } else { 3 })?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected,
actual: input.len(),
});
}
if config.lossless && matches!(bps, BitsPerSample::F32) {
return encode_f32_lossless_rgba(input, width, height, has_alpha, config);
}
let distance = config.distance.max(MIN_DISTANCE);
let mut linear = Image3F::new(width, height);
if has_alpha {
let mut alpha_plane = vec![0u16; width * height];
for (y, (row, alpha_row)) in input
.chunks_exact(width * 4)
.zip(alpha_plane.chunks_exact_mut(width))
.enumerate()
{
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for ((((r, g), b), src), a) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<4>().0.iter())
.zip(alpha_row.iter_mut())
{
*r = srgb_to_linear_f32(src[0]);
*g = srgb_to_linear_f32(src[1]);
*b = srgb_to_linear_f32(src[2]);
*a = (src[3].clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
}
}
encode_with_config(
&linear,
&EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_alpha(AlphaPlane::from_u16_16bit(alpha_plane))
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
)
} else {
for (y, row) in input.chunks_exact(width * 3).enumerate() {
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for (((r, g), b), src) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<3>().0.iter())
{
*r = srgb_to_linear_f32(src[0]);
*g = srgb_to_linear_f32(src[1]);
*b = srgb_to_linear_f32(src[2]);
}
}
encode_with_config(
&linear,
&EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
)
}
}
fn encode_float_gray(
luma: &[f32],
width: usize,
height: usize,
config: &EncodeConfig,
bps: BitsPerSample,
) -> Result<Vec<u8>, EncodeError> {
let expected = checked_buffer_size::<f32>(width, height, 1)?;
if luma.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected: width * height,
actual: luma.len(),
});
}
let distance = config.distance.max(MIN_DISTANCE);
let mut linear = Image3F::new(width, height);
for (y, row) in luma.chunks_exact(width).enumerate() {
let [r_row, g_row, b_row] = linear.all_plane_rows_mut(y);
for (((r, g), b), &v) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.iter())
{
let lin = srgb_to_linear_f32(v);
*r = lin;
*g = lin;
*b = lin;
}
}
encode_with_config(
&linear,
&EncodeConfigImpl::with_distance(distance)
.with_progressive_from(config)
.with_grayscale(true)
.with_bits_per_sample(bps)
.with_icc_profile(config.icc_profile.clone())
.with_exif(config.exif.clone())
.with_orientation(config.orientation)
.with_color_encoding(config.color_encoding)
.with_intensity_target(config.intensity_target)
.with_num_threads(config.num_threads),
)
}
pub fn encode_image_f32(
input: &[f32],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_float_rgba(input, width, height, false, config, BitsPerSample::F32)
}
pub fn encode_image_f16(
input: &[f32],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_float_rgba(input, width, height, false, config, BitsPerSample::F16)
}
pub fn encode_image_with_alpha_f32(
input: &[f32],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_float_rgba(input, width, height, true, config, BitsPerSample::F32)
}
pub fn encode_image_with_alpha_f16(
input: &[f32],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_float_rgba(input, width, height, true, config, BitsPerSample::F16)
}
pub fn encode_image_gray_f32(
input: &[f32],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_float_gray(input, width, height, config, BitsPerSample::F32)
}
pub fn encode_image_gray_f16(
input: &[f32],
width: usize,
height: usize,
config: &EncodeConfig,
) -> Result<Vec<u8>, EncodeError> {
encode_float_gray(input, width, height, config, BitsPerSample::F16)
}
fn progressive_schedule(
progressive: bool,
passes: Option<u32>,
shifts: Option<&[u32]>,
) -> Vec<u32> {
if let Some(s) = shifts {
let valid = !s.is_empty()
&& s.len() <= 11
&& *s.last().unwrap() == 0
&& s.iter().all(|&v| v <= 3)
&& s.windows(2).all(|w| w[0] >= w[1]);
if valid {
return s.to_vec();
}
}
let n = passes
.map(|p| p.clamp(1, 4))
.unwrap_or(if progressive { 2 } else { 1 }) as usize;
(0..n).rev().map(|p| p as u32).collect()
}
pub(crate) fn encode_with_config(
input: &Image3F,
config: &EncodeConfigImpl,
) -> Result<Vec<u8>, EncodeError> {
if input.xsize() == 0 || input.ysize() == 0 {
return Err(EncodeError::EmptyImage);
}
if input.xsize() > MAX_DIMENSION || input.ysize() > MAX_DIMENSION {
return Err(EncodeError::DimensionTooLarge {
width: input.xsize(),
height: input.ysize(),
});
}
if !config.distance.is_finite() || config.distance <= 0.0 {
return Err(EncodeError::InvalidDistance(config.distance));
}
if let Some(alpha) = config.alpha.as_ref() {
let expected = match &alpha {
AlphaPlane::U8(_) => checked_buffer_size::<u8>(input.xsize(), input.ysize(), 1)?,
AlphaPlane::U16 { .. } => checked_buffer_size::<u16>(input.xsize(), input.ysize(), 1)?,
AlphaPlane::F32(_) => checked_buffer_size::<f32>(input.xsize(), input.ysize(), 1)?,
};
if alpha.len() != expected {
return Err(EncodeError::AlphaSizeMismatch {
expected,
actual: alpha.len(),
});
}
}
let distance = config.distance.max(MIN_DISTANCE);
let mut w = BitWriter::new();
w.write(8, 0xFF);
w.write(8, CODESTREAM_MARKER as u64);
write_size_header(input.xsize(), input.ysize(), &mut w);
write_image_metadata(
config.tone_mapping(),
&config.color_encoding,
config.alpha.as_ref(),
config.icc_profile.as_deref(),
config.bits_per_sample,
config.lossless,
config.grayscale,
config.orientation,
&mut w,
);
let coeff_shifts = progressive_schedule(
config.progressive,
config.progressive_passes,
config.progressive_shifts.as_deref(),
);
encode_frame(
distance,
input,
config.alpha.as_ref(),
&coeff_shifts,
config.num_threads.max(1),
&mut w,
);
let codestream = w.into_bytes();
let alpha_bits = config.alpha.as_ref().map(|a| a.bits() as u32).unwrap_or(0);
Ok(finalize_container(
codestream,
config.exif.as_deref(),
needs_level_10(config.bits_per_sample.bits(), config.lossless, alpha_bits),
))
}
pub(crate) trait AsSignedInt {
fn to_signed_int(self, max_bp: u8) -> i32;
}
impl AsSignedInt for u8 {
#[inline]
fn to_signed_int(self, _: u8) -> i32 {
self as i32
}
}
impl AsSignedInt for u16 {
#[inline]
fn to_signed_int(self, max_bp: u8) -> i32 {
let max_colors = ((1u32 << max_bp) - 1) as i32;
(self as i32).min(max_colors)
}
}
fn encode_with_config_loseless<T: AsSignedInt + Copy>(
input: &[T],
width: usize,
height: usize,
has_alpha: bool,
max_bp: u8,
config: &EncodeConfigImpl,
) -> Result<Vec<u8>, EncodeError> {
if width == 0 || height == 0 {
return Err(EncodeError::EmptyImage);
}
let expected = checked_buffer_size::<T>(width, height, if has_alpha { 4 } else { 3 })?;
if input.len() != expected {
return Err(EncodeError::InputSizeMismatch {
expected,
actual: input.len(),
});
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(EncodeError::DimensionTooLarge { width, height });
}
if !config.distance.is_finite() || config.distance <= 0.0 {
return Err(EncodeError::InvalidDistance(config.distance));
}
let mut image3s = Image3Si::new(width, height);
let mut alpha_plane: Option<AlphaPlane> = None;
if has_alpha {
if max_bp > 8 {
let mut new_alpha_plane = vec![0u16; width * height];
for (y, (row, alpha_row)) in input
.chunks_exact(width * 4)
.zip(new_alpha_plane.chunks_exact_mut(width))
.enumerate()
{
let [r_row, g_row, b_row] = image3s.all_plane_rows_mut(y);
for ((((r, g), b), src), alpha) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<4>().0.iter())
.zip(alpha_row.iter_mut())
{
let ycocg = forward_ycocg(
src[0].to_signed_int(max_bp),
src[1].to_signed_int(max_bp),
src[2].to_signed_int(max_bp),
);
*r = ycocg.0;
*g = ycocg.1;
*b = ycocg.2;
*alpha = src[3].to_signed_int(max_bp) as u16;
}
}
alpha_plane = Some(AlphaPlane::U16 {
data: new_alpha_plane,
bits: max_bp,
});
} else {
let mut new_alpha_plane = vec![0u8; width * height];
for (y, (row, alpha_row)) in input
.chunks_exact(width * 4)
.zip(new_alpha_plane.chunks_exact_mut(width))
.enumerate()
{
let [r_row, g_row, b_row] = image3s.all_plane_rows_mut(y);
for ((((r, g), b), src), alpha) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<4>().0.iter())
.zip(alpha_row.iter_mut())
{
let ycocg = forward_ycocg(
src[0].to_signed_int(max_bp),
src[1].to_signed_int(max_bp),
src[2].to_signed_int(max_bp),
);
*r = ycocg.0;
*g = ycocg.1;
*b = ycocg.2;
*alpha = src[3].to_signed_int(max_bp) as u8;
}
}
alpha_plane = Some(AlphaPlane::U8(new_alpha_plane));
}
} else {
for (y, row) in input.chunks_exact(width * 3).enumerate() {
let [r_row, g_row, b_row] = image3s.all_plane_rows_mut(y);
for (((r, g), b), src) in r_row
.iter_mut()
.zip(g_row.iter_mut())
.zip(b_row.iter_mut())
.zip(row.as_chunks::<3>().0.iter())
{
let ycocg = forward_ycocg(
src[0].to_signed_int(max_bp),
src[1].to_signed_int(max_bp),
src[2].to_signed_int(max_bp),
);
*r = ycocg.0;
*g = ycocg.1;
*b = ycocg.2;
}
}
}
let mut w = BitWriter::new();
w.write(8, 0xFF);
w.write(8, CODESTREAM_MARKER as u64);
write_size_header(width, height, &mut w);
write_image_metadata(
config.tone_mapping(),
&config.color_encoding,
alpha_plane.as_ref(),
config.icc_profile.as_deref(),
config.bits_per_sample,
config.lossless,
config.grayscale,
config.orientation,
&mut w,
);
let alpha_bits = alpha_plane.as_ref().map(|a| a.bits() as u32).unwrap_or(0);
let eff_bits = (max_bp as u32).max(alpha_bits);
let num_color = if config.grayscale { 1 } else { 3 };
encode_frame_lossless(
&image3s,
alpha_plane.as_ref(),
eff_bits,
config.progressive,
num_color,
&mut w,
);
let codestream = w.into_bytes();
let alpha_bits = alpha_plane.as_ref().map(|a| a.bits() as u32).unwrap_or(0);
Ok(finalize_container(
codestream,
config.exif.as_deref(),
needs_level_10(max_bp as u32, true, alpha_bits),
))
}
fn write_size(size: u32, w: &mut BitWriter) {
let size_minus_one = size - 1;
const BUCKET_BITS: [u32; 4] = [9, 13, 18, 30];
for (selector, &bits) in BUCKET_BITS.iter().enumerate() {
if size_minus_one < (1 << bits) {
w.write(2, selector as u64);
w.write(bits as usize, size_minus_one as u64);
return;
}
}
unreachable!("dimension was bounds-checked against MAX_DIMENSION");
}
fn write_size_header(xsize: usize, ysize: usize, w: &mut BitWriter) {
assert!(
xsize <= MAX_DIMENSION && ysize <= MAX_DIMENSION,
"image too large: max dimension is {MAX_DIMENSION}"
);
w.write(1, 0); write_size(ysize as u32, w);
w.write(3, 0); write_size(xsize as u32, w);
}
pub(crate) fn needs_level_10(bits: u32, lossless: bool, alpha_bits: u32) -> bool {
(lossless && bits >= 16) || alpha_bits >= 16
}
pub(crate) fn wrap_jxl_container(codestream: Vec<u8>, level: u8, exif: Option<&[u8]>) -> Vec<u8> {
let exif_extra = exif.map(|e| e.len() + 12).unwrap_or(0);
let mut out = Vec::with_capacity(codestream.len() + 41 + exif_extra);
out.extend_from_slice(&[
0, 0, 0, 0x0C, b'J', b'X', b'L', b' ', 0x0D, 0x0A, 0x87, 0x0A,
]);
out.extend_from_slice(&[
0, 0, 0, 0x14, b'f', b't', b'y', b'p', b'j', b'x', b'l', b' ', 0, 0, 0, 0, b'j', b'x',
b'l', b' ',
]);
out.extend_from_slice(&[0, 0, 0, 0x09, b'j', b'x', b'l', b'l', level]);
let payload = 8u64 + codestream.len() as u64;
if payload <= u32::MAX as u64 {
out.extend_from_slice(&(payload as u32).to_be_bytes());
out.extend_from_slice(b"jxlc");
} else {
out.extend_from_slice(&1u32.to_be_bytes()); out.extend_from_slice(b"jxlc");
out.extend_from_slice(&(payload + 8).to_be_bytes());
}
out.extend_from_slice(&codestream);
if let Some(e) = exif {
let content = 4u64 + e.len() as u64; let payload = 8u64 + content;
if payload <= u32::MAX as u64 {
out.extend_from_slice(&(payload as u32).to_be_bytes());
out.extend_from_slice(b"Exif");
} else {
out.extend_from_slice(&1u32.to_be_bytes());
out.extend_from_slice(b"Exif");
out.extend_from_slice(&(payload + 8).to_be_bytes());
}
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(e);
}
out
}
fn finalize_container(codestream: Vec<u8>, exif: Option<&[u8]>, need_l10: bool) -> Vec<u8> {
if need_l10 || exif.is_some() {
wrap_jxl_container(codestream, if need_l10 { 10 } else { 5 }, exif)
} else {
codestream
}
}
fn write_int_bit_depth(bits: u32, w: &mut BitWriter) {
w.write(1, 0); match bits {
8 => w.write(2, 0),
10 => w.write(2, 1),
12 => w.write(2, 2),
_ => {
w.write(2, 3); w.write(6, (bits - 1) as u64);
}
}
}
fn write_float_bit_depth(bits: u32, exp_bits: u32, w: &mut BitWriter) {
w.write(1, 1); match bits {
32 => w.write(2, 0),
16 => w.write(2, 1),
24 => w.write(2, 2),
_ => {
w.write(2, 3);
w.write(6, (bits - 1) as u64);
}
}
w.write(4, (exp_bits - 1) as u64);
}
fn write_image_metadata(
tm: ToneMappingParams,
color_encoding: &ColorEncoding,
alpha: Option<&AlphaPlane>,
icc_profile: Option<&[u8]>,
bps: BitsPerSample,
lossless: bool,
grayscale: bool,
orientation: Orientation,
w: &mut BitWriter,
) {
w.write(1, 0); let extra_fields = !tm.is_default() || orientation != Orientation::Normal;
w.write(1, if extra_fields { 1 } else { 0 }); if extra_fields {
w.write(3, orientation.to_u3()); w.write(1, 0); w.write(1, 0); w.write(1, 0); }
if bps.is_float() {
write_float_bit_depth(bps.bits(), bps.exp_bits(), w);
} else {
write_int_bit_depth(bps.bits(), w);
}
let alpha_bits = alpha.map(|a| a.bits() as u32).unwrap_or(0);
let needs_32 = (lossless && bps.bits() >= 16) || alpha_bits >= 16;
w.write(1, if needs_32 { 0 } else { 1 });
if let Some(alpha) = alpha {
w.write(2, 1); match alpha.bits() {
8 => {
w.write(1, 1); }
bits => {
w.write(1, 0); w.write(2, 0); if alpha.is_float() {
write_float_bit_depth(32, 8, w); } else {
write_int_bit_depth(bits as u32, w);
}
w.write(2, 0); w.write(2, 0); w.write(1, 0); }
}
} else {
w.write(2, 0); }
w.write(1, if lossless { 0 } else { 1 }); let want_icc = icc_profile.is_some();
write_color_encoding_with_icc(color_encoding, want_icc, grayscale, w);
if extra_fields {
w.write(1, 0);
let it = tm.intensity_target.unwrap_or(255.0);
w.write(16, crate::util::f32_to_f16(it) as u64); w.write(16, crate::util::f32_to_f16(tm.min_nits) as u64); w.write(1, if tm.relative_to_max_display { 1 } else { 0 }); w.write(16, crate::util::f32_to_f16(tm.linear_below) as u64); }
w.write(2, 0); w.write(1, 1); if let Some(icc) = icc_profile {
crate::icc_codec::write_icc_stream(icc, w);
}
w.zero_pad_to_byte();
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn quality_mapping_anchor_points() {
assert!(close(distance_from_quality(100.0), 0.05));
assert!(close(distance_from_quality(99.0), 0.1));
assert!(close(distance_from_quality(90.0), 1.0));
assert!(close(distance_from_quality(30.0), 6.4));
let d25 = distance_from_quality(25.0);
assert!(d25 > 6.4 && d25 < 7.0, "q=25 -> {d25}");
assert!(close(distance_from_quality(0.0), 25.0));
assert!(close(distance_from_quality(-50.0), 25.0));
assert!(close(distance_from_quality(110.0), 0.05));
}
#[test]
fn quality_mapping_monotone() {
let mut prev = distance_from_quality(0.0);
for q in 1..=100 {
let d = distance_from_quality(q as f32);
assert!(d <= prev, "non-monotonic at q={q}: prev={prev}, d={d}");
prev = d;
}
}
#[test]
#[should_panic(expected = "quality must not be NaN")]
fn quality_nan_panics() {
let _ = distance_from_quality(f32::NAN);
}
}
#[cfg(test)]
mod encode_smoke_tests {
use super::*;
const W: usize = 16;
const H: usize = 16;
fn rgb8() -> Vec<u8> {
(0..W * H * 3).map(|i| (i % 256) as u8).collect()
}
fn rgba8() -> Vec<u8> {
(0..W * H * 4).map(|i| (i % 256) as u8).collect()
}
fn rgb16() -> Vec<u16> {
(0..W * H * 3).map(|i| (i % 65536) as u16).collect()
}
fn rgba16() -> Vec<u16> {
(0..W * H * 4).map(|i| (i % 65536) as u16).collect()
}
fn gray8() -> Vec<u8> {
(0..W * H).map(|i| (i % 256) as u8).collect()
}
fn gray_alpha8() -> Vec<u8> {
(0..W * H * 2).map(|i| (i % 256) as u8).collect()
}
fn gray16() -> Vec<u16> {
(0..W * H).map(|i| (i % 65536) as u16).collect()
}
fn gray_alpha16() -> Vec<u16> {
(0..W * H * 2).map(|i| (i % 65536) as u16).collect()
}
fn rgb10() -> Vec<u16> {
(0..W * H * 3).map(|i| (i % 1024) as u16).collect()
}
fn rgba10() -> Vec<u16> {
(0..W * H * 4).map(|i| (i % 1024) as u16).collect()
}
fn rgb12() -> Vec<u16> {
(0..W * H * 3).map(|i| (i % 4096) as u16).collect()
}
fn rgba12() -> Vec<u16> {
(0..W * H * 4).map(|i| (i % 4096) as u16).collect()
}
fn gray10() -> Vec<u16> {
(0..W * H).map(|i| (i % 1024) as u16).collect()
}
fn gray_alpha10() -> Vec<u16> {
(0..W * H * 2).map(|i| (i % 1024) as u16).collect()
}
fn gray12() -> Vec<u16> {
(0..W * H).map(|i| (i % 4096) as u16).collect()
}
fn gray_alpha12() -> Vec<u16> {
(0..W * H * 2).map(|i| (i % 4096) as u16).collect()
}
fn rgb_f32() -> Vec<f32> {
(0..W * H * 3).map(|i| (i % 256) as f32 / 255.0).collect()
}
fn rgba_f32() -> Vec<f32> {
(0..W * H * 4).map(|i| (i % 256) as f32 / 255.0).collect()
}
fn gray_f32() -> Vec<f32> {
(0..W * H).map(|i| (i % 256) as f32 / 255.0).collect()
}
fn lossy() -> EncodeConfig {
EncodeConfig::default().with_distance(1.0)
}
fn lossless() -> EncodeConfig {
EncodeConfig::default().with_lossless(true)
}
fn ok(r: Result<Vec<u8>, EncodeError>) {
let bytes = r.expect("encode failed");
assert!(!bytes.is_empty(), "encoded output is empty");
}
#[test]
fn rgb8_lossy() {
ok(encode_image(&rgb8(), W, H, &lossy()));
}
#[test]
fn rgb8_lossless() {
ok(encode_image(&rgb8(), W, H, &lossless()));
}
#[test]
fn rgb8_quality() {
ok(encode_image(
&rgb8(),
W,
H,
&EncodeConfig::default().with_quality(85.0),
));
}
#[test]
fn rgba8_lossy() {
ok(encode_image_with_alpha(&rgba8(), W, H, &lossy()));
}
#[test]
fn rgba8_lossless() {
ok(encode_image_with_alpha(&rgba8(), W, H, &lossless()));
}
#[test]
fn rgb16_lossy() {
ok(encode_image_16bit(&rgb16(), W, H, &lossy()));
}
#[test]
fn rgb16_lossless() {
ok(encode_image_16bit(&rgb16(), W, H, &lossless()));
}
#[test]
fn rgba16_lossy() {
ok(encode_image_with_alpha_16bit(&rgba16(), W, H, &lossy()));
}
#[test]
fn rgba16_lossless() {
ok(encode_image_with_alpha_16bit(&rgba16(), W, H, &lossless()));
}
#[test]
fn rgb10_lossy() {
ok(encode_image_10bit(&rgb10(), W, H, &lossy()));
}
#[test]
fn rgb10_lossless() {
ok(encode_image_10bit(&rgb10(), W, H, &lossless()));
}
#[test]
fn rgba10_lossy() {
ok(encode_image_with_alpha_10bit(&rgba10(), W, H, &lossy()));
}
#[test]
fn rgba10_lossless() {
ok(encode_image_with_alpha_10bit(&rgba10(), W, H, &lossless()));
}
#[test]
fn rgb12_lossy() {
ok(encode_image_12bit(&rgb12(), W, H, &lossy()));
}
#[test]
fn rgb12_lossless() {
ok(encode_image_12bit(&rgb12(), W, H, &lossless()));
}
#[test]
fn rgba12_lossy() {
ok(encode_image_with_alpha_12bit(&rgba12(), W, H, &lossy()));
}
#[test]
fn rgba12_lossless() {
ok(encode_image_with_alpha_12bit(&rgba12(), W, H, &lossless()));
}
#[test]
fn rgb_f32_lossy() {
ok(encode_image_f32(&rgb_f32(), W, H, &lossy()));
}
#[test]
fn rgb_f16_lossy() {
ok(encode_image_f16(&rgb_f32(), W, H, &lossy()));
}
#[test]
fn rgba_f32_lossy() {
ok(encode_image_with_alpha_f32(&rgba_f32(), W, H, &lossy()));
}
#[test]
fn rgba_f16_lossy() {
ok(encode_image_with_alpha_f16(&rgba_f32(), W, H, &lossy()));
}
#[test]
fn gray8_lossy() {
ok(encode_image_gray(&gray8(), W, H, &lossy()));
}
#[test]
fn gray8_lossless() {
ok(encode_image_gray(&gray8(), W, H, &lossless()));
}
#[test]
fn gray_alpha8_lossy() {
ok(encode_image_gray_alpha(&gray_alpha8(), W, H, &lossy()));
}
#[test]
fn gray_alpha8_lossless() {
ok(encode_image_gray_alpha(&gray_alpha8(), W, H, &lossless()));
}
#[test]
fn gray10_lossy() {
ok(encode_image_gray_10bit(&gray10(), W, H, &lossy()));
}
#[test]
fn gray10_lossless() {
ok(encode_image_gray_10bit(&gray10(), W, H, &lossless()));
}
#[test]
fn gray_alpha10_lossy() {
ok(encode_image_gray_alpha_10bit(
&gray_alpha10(),
W,
H,
&lossy(),
));
}
#[test]
fn gray_alpha10_lossless() {
ok(encode_image_gray_alpha_10bit(
&gray_alpha10(),
W,
H,
&lossless(),
));
}
#[test]
fn gray12_lossy() {
ok(encode_image_gray_12bit(&gray12(), W, H, &lossy()));
}
#[test]
fn gray12_lossless() {
ok(encode_image_gray_12bit(&gray12(), W, H, &lossless()));
}
#[test]
fn gray_alpha12_lossy() {
ok(encode_image_gray_alpha_12bit(
&gray_alpha12(),
W,
H,
&lossy(),
));
}
#[test]
fn gray_alpha12_lossless() {
ok(encode_image_gray_alpha_12bit(
&gray_alpha12(),
W,
H,
&lossless(),
));
}
#[test]
fn gray16_lossy() {
ok(encode_image_gray_16bit(&gray16(), W, H, &lossy()));
}
#[test]
fn gray16_lossless() {
ok(encode_image_gray_16bit(&gray16(), W, H, &lossless()));
}
#[test]
fn gray_alpha16_lossy() {
ok(encode_image_gray_alpha_16bit(
&gray_alpha16(),
W,
H,
&lossy(),
));
}
#[test]
fn gray_alpha16_lossless() {
ok(encode_image_gray_alpha_16bit(
&gray_alpha16(),
W,
H,
&lossless(),
));
}
#[test]
fn gray_f32_lossy() {
ok(encode_image_gray_f32(&gray_f32(), W, H, &lossy()));
}
#[test]
fn gray_f16_lossy() {
ok(encode_image_gray_f16(&gray_f32(), W, H, &lossy()));
}
#[test]
fn empty_width_rejected() {
assert!(matches!(
encode_image(&[], 0, H, &lossy()),
Err(EncodeError::EmptyImage)
));
}
#[test]
fn empty_height_rejected() {
assert!(matches!(
encode_image(&[], W, 0, &lossy()),
Err(EncodeError::EmptyImage)
));
}
#[test]
fn wrong_buffer_size_rejected() {
let short = vec![0u8; W * H]; assert!(matches!(
encode_image(&short, W, H, &lossy()),
Err(EncodeError::InputSizeMismatch { .. })
));
}
#[test]
fn invalid_distance_rejected() {
assert!(matches!(
encode_image(&rgb8(), W, H, &EncodeConfig::default().with_distance(-1.0)),
Err(EncodeError::InvalidDistance(_))
));
}
#[test]
fn dimension_too_large_rejected() {
let huge = MAX_DIMENSION + 1;
assert!(matches!(
encode_image(&[], huge, H, &lossy()),
Err(EncodeError::DimensionTooLarge { .. })
));
}
#[test]
fn one_by_one_rgb8() {
ok(encode_image(&[128u8, 64, 32], 1, 1, &lossy()));
}
#[test]
fn one_by_one_rgba8_lossless() {
ok(encode_image_with_alpha(
&[128u8, 64, 32, 255],
1,
1,
&lossless(),
));
}
#[test]
fn one_by_one_gray8() {
ok(encode_image_gray(&[200u8], 1, 1, &lossy()));
}
}