mod gainmap;
mod lossless;
mod lossy;
use alloc::vec::Vec;
use enough::Stop;
use zenlayout::{Command, Constraint, ConstraintMode, FlipAxis, Rotation, SourceCrop};
use crate::decode::DecodeConfig;
use crate::encode::encoder_config::EncoderConfig;
use crate::encode::encoder_types::ChromaSubsampling;
use crate::error::Result;
pub use crate::lossless::EdgeHandling;
#[derive(Clone)]
pub struct LayoutConfig {
quality: f32,
subsampling: ChromaSubsampling,
progressive: bool,
auto_optimize: bool,
filter: zenresize::Filter,
edge_handling: EdgeHandling,
pub(crate) fancy_upsampling: bool,
}
impl LayoutConfig {
pub fn new(quality: impl Into<f32>) -> Self {
Self {
quality: quality.into(),
subsampling: ChromaSubsampling::Quarter,
progressive: true,
auto_optimize: true,
filter: zenresize::Filter::default(),
edge_handling: EdgeHandling::TrimPartialBlocks,
fancy_upsampling: true,
}
}
pub fn with_filter(mut self, f: zenresize::Filter) -> Self {
self.filter = f;
self
}
pub fn with_edge_handling(mut self, eh: EdgeHandling) -> Self {
self.edge_handling = eh;
self
}
pub fn with_progressive(mut self, p: bool) -> Self {
self.progressive = p;
self
}
pub fn with_subsampling(mut self, s: ChromaSubsampling) -> Self {
self.subsampling = s;
self
}
pub fn with_fancy_upsampling(mut self, f: bool) -> Self {
self.fancy_upsampling = f;
self
}
pub fn with_auto_optimize(mut self, enable: bool) -> Self {
self.auto_optimize = enable;
self
}
pub fn request<'a>(&'a self, jpeg_data: &'a [u8]) -> LayoutRequest<'a> {
LayoutRequest {
config: self,
jpeg_data,
commands: Vec::new(),
optimize_for_decode: false,
}
}
pub(crate) fn build_encoder_config(&self) -> EncoderConfig {
let config =
EncoderConfig::ycbcr(self.quality, self.subsampling).progressive(self.progressive);
#[cfg(feature = "trellis")]
let config = config.auto_optimize(self.auto_optimize);
config
}
}
pub struct LayoutRequest<'a> {
config: &'a LayoutConfig,
jpeg_data: &'a [u8],
commands: Vec<Command>,
optimize_for_decode: bool,
}
impl<'a> LayoutRequest<'a> {
pub fn auto_orient(mut self, exif_orientation: u8) -> Self {
self.commands.push(Command::AutoOrient(exif_orientation));
self
}
pub fn rotate_90(mut self) -> Self {
self.commands.push(Command::Rotate(Rotation::Rotate90));
self
}
pub fn rotate_180(mut self) -> Self {
self.commands.push(Command::Rotate(Rotation::Rotate180));
self
}
pub fn rotate_270(mut self) -> Self {
self.commands.push(Command::Rotate(Rotation::Rotate270));
self
}
pub fn flip_h(mut self) -> Self {
self.commands.push(Command::Flip(FlipAxis::Horizontal));
self
}
pub fn flip_v(mut self) -> Self {
self.commands.push(Command::Flip(FlipAxis::Vertical));
self
}
pub fn crop(mut self, crop: SourceCrop) -> Self {
self.commands.push(Command::Crop(crop));
self
}
pub fn fit(mut self, w: u32, h: u32) -> Self {
self.commands.push(Command::Constrain(Constraint::new(
ConstraintMode::Fit,
w,
h,
)));
self
}
pub fn within(mut self, w: u32, h: u32) -> Self {
self.commands.push(Command::Constrain(Constraint::new(
ConstraintMode::Within,
w,
h,
)));
self
}
pub fn fit_crop(mut self, w: u32, h: u32) -> Self {
self.commands.push(Command::Constrain(Constraint::new(
ConstraintMode::FitCrop,
w,
h,
)));
self
}
pub fn command(mut self, cmd: Command) -> Self {
self.commands.push(cmd);
self
}
pub fn optimize_for_decode(mut self) -> Self {
self.optimize_for_decode = true;
self
}
pub fn execute(self, stop: &dyn Stop) -> Result<LayoutResult> {
stop.check()?;
let decoder = DecodeConfig::new();
let info = decoder.read_info(self.jpeg_data)?;
let src_w = info.dimensions.width;
let src_h = info.dimensions.height;
let gain_map_jpeg = self.detect_and_extract_gainmap(&info);
let mut commands = self.resolve_auto_orient(&info);
let auto_oriented = if self.optimize_for_decode {
let has_explicit_orient = commands
.iter()
.any(|cmd| matches!(cmd, Command::AutoOrient(_)));
if !has_explicit_orient {
if let Some(exif_val) = lossless::safe_auto_orient(&info) {
commands.insert(0, Command::AutoOrient(exif_val));
true
} else {
false
}
} else {
false
}
} else {
false
};
if let Some(transform) = lossless::detect_lossless(&commands) {
let primary = if self.optimize_for_decode {
if transform == crate::lossless::LosslessTransform::None
&& info.mode == crate::types::JpegMode::Baseline
&& lossless::is_decode_ready(self.jpeg_data, &info)
{
self.jpeg_data.to_vec()
} else {
lossless::execute_restructure(
self.jpeg_data,
transform,
self.config.edge_handling,
stop,
)?
}
} else {
lossless::execute_lossless(
self.jpeg_data,
transform,
self.config.edge_handling,
stop,
)?
};
let (out_w, out_h) = if transform.swaps_dimensions() {
(src_h, src_w)
} else {
(src_w, src_h)
};
let mut data = if transform != crate::lossless::LosslessTransform::None {
if let Some(gm_bytes) = gain_map_jpeg {
let gm_fn = if self.optimize_for_decode {
lossless::execute_restructure
} else {
lossless::execute_lossless
};
let gm_transformed =
gm_fn(&gm_bytes, transform, self.config.edge_handling, stop)?;
gainmap::assemble_ultrahdr(primary, gm_transformed)
} else {
primary
}
} else {
primary
};
if auto_oriented && transform != crate::lossless::LosslessTransform::None {
lossless::reset_exif_orientation_in_jpeg(&mut data);
}
return Ok(LayoutResult {
data,
lossless: true,
width: out_w,
height: out_h,
});
}
let (ideal, request) = self.compute_layout(&commands, src_w, src_h)?;
let offer = zenlayout::DecoderOffer::full_decode(src_w, src_h);
let plan = ideal.finalize(&request, &offer);
let target_w = plan.canvas.width;
let target_h = plan.canvas.height;
let has_orientation = commands.iter().any(|cmd| {
matches!(
cmd,
Command::AutoOrient(o) if *o != 1
) || matches!(cmd, Command::Rotate(_) | Command::Flip(_))
});
let primary = lossy::execute_lossy(
self.jpeg_data,
&info,
self.config,
&plan,
has_orientation,
self.optimize_for_decode,
stop,
)?;
let data = match gain_map_jpeg {
Some(gm_bytes) => match self
.transform_gainmap_lossy(&gm_bytes, src_w, src_h, target_w, target_h, stop)?
{
Some(gm_transformed) => gainmap::assemble_ultrahdr(primary, gm_transformed),
None => primary,
},
None => primary,
};
Ok(LayoutResult {
data,
lossless: false,
width: target_w,
height: target_h,
})
}
fn detect_and_extract_gainmap(&self, info: &crate::decode::JpegInfo) -> Option<Vec<u8>> {
let xmp = info.xmp.as_deref()?;
if !gainmap::is_ultrahdr_xmp(xmp) {
return None;
}
gainmap::find_secondary_jpeg(self.jpeg_data)
}
fn transform_gainmap_lossy(
&self,
gm_bytes: &[u8],
primary_src_w: u32,
primary_src_h: u32,
primary_dst_w: u32,
primary_dst_h: u32,
stop: &dyn Stop,
) -> Result<Option<Vec<u8>>> {
let decoder = DecodeConfig::new();
let gm_info = match decoder.read_info(gm_bytes) {
Ok(info) => info,
Err(_) => return Ok(None), };
let gm_src_w = gm_info.dimensions.width;
let gm_src_h = gm_info.dimensions.height;
let (gm_dst_w, gm_dst_h) = gainmap::compute_gainmap_target(
primary_src_w,
primary_src_h,
primary_dst_w,
primary_dst_h,
gm_src_w,
gm_src_h,
);
let gm_transformed =
lossy::resize_simple(gm_bytes, &gm_info, self.config, gm_dst_w, gm_dst_h, stop)?;
Ok(Some(gm_transformed))
}
fn resolve_auto_orient(&self, info: &crate::decode::JpegInfo) -> Vec<Command> {
self.commands
.iter()
.map(|cmd| {
if let Command::AutoOrient(0) = cmd {
let exif_orient = info
.exif
.as_ref()
.and_then(|e| crate::lossless::parse_exif_orientation(e))
.unwrap_or(1);
Command::AutoOrient(exif_orient)
} else {
cmd.clone()
}
})
.collect()
}
fn compute_layout(
&self,
commands: &[Command],
src_w: u32,
src_h: u32,
) -> Result<(zenlayout::IdealLayout, zenlayout::DecoderRequest)> {
zenlayout::compute_layout(commands, src_w, src_h, None)
.map_err(|e| crate::error::Error::invalid_config(alloc::format!("layout error: {e}")))
}
}
pub struct LayoutResult {
pub data: Vec<u8>,
pub lossless: bool,
pub width: u32,
pub height: u32,
}
pub use zenlayout::Command as LayoutCommand;
pub use zenresize::Filter;
#[cfg(test)]
mod tests {
use super::*;
use crate::encode::encoder_config::EncoderConfig;
use crate::encode::encoder_types::{ChromaSubsampling, PixelLayout};
use enough::Unstoppable;
fn make_test_jpeg(width: u32, height: u32) -> Vec<u8> {
let config = EncoderConfig::ycbcr(85.0, ChromaSubsampling::Quarter).progressive(true);
let pixel_count = (width * height) as usize;
let mut pixels = alloc::vec![0u8; pixel_count * 3];
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) as usize * 3;
pixels[idx] = (x * 255 / width.max(1)) as u8;
pixels[idx + 1] = (y * 255 / height.max(1)) as u8;
pixels[idx + 2] = 128;
}
}
let mut encoder = config
.request()
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.unwrap();
encoder.push_packed(&pixels, Unstoppable).unwrap();
encoder.finish().unwrap()
}
#[test]
fn identity_is_lossless() {
let jpeg = make_test_jpeg(64, 64);
let result = LayoutConfig::new(85.0)
.request(&jpeg)
.execute(&Unstoppable)
.unwrap();
assert!(result.lossless);
assert_eq!(result.width, 64);
assert_eq!(result.height, 64);
}
#[test]
fn rotate_90_is_lossless() {
let jpeg = make_test_jpeg(64, 48);
let result = LayoutConfig::new(85.0)
.request(&jpeg)
.rotate_90()
.execute(&Unstoppable)
.unwrap();
assert!(result.lossless);
assert_eq!(result.width, 48);
assert_eq!(result.height, 64);
}
#[test]
fn flip_h_is_lossless() {
let jpeg = make_test_jpeg(64, 64);
let result = LayoutConfig::new(85.0)
.request(&jpeg)
.flip_h()
.execute(&Unstoppable)
.unwrap();
assert!(result.lossless);
assert_eq!(result.width, 64);
assert_eq!(result.height, 64);
}
#[test]
fn fit_is_lossy() {
let jpeg = make_test_jpeg(64, 64);
let result = LayoutConfig::new(85.0)
.request(&jpeg)
.fit(32, 32)
.execute(&Unstoppable)
.unwrap();
assert!(!result.lossless);
assert_eq!(result.width, 32);
assert_eq!(result.height, 32);
}
#[test]
fn within_no_upscale() {
let jpeg = make_test_jpeg(64, 64);
let result = LayoutConfig::new(85.0)
.request(&jpeg)
.within(256, 256)
.execute(&Unstoppable)
.unwrap();
assert_eq!(result.width, 64);
assert_eq!(result.height, 64);
}
#[test]
fn orient_plus_resize_is_lossy() {
let jpeg = make_test_jpeg(64, 64);
let result = LayoutConfig::new(85.0)
.request(&jpeg)
.auto_orient(6) .fit(32, 32)
.execute(&Unstoppable)
.unwrap();
assert!(!result.lossless);
assert_eq!(result.width, 32);
assert_eq!(result.height, 32);
}
}