use crate::color::icc::TargetColorSpace;
use crate::foundation::alloc::{DEFAULT_MAX_MEMORY, DEFAULT_MAX_PIXELS};
use crate::lossless::LosslessTransform;
use crate::types::Dimensions;
use zenpixels::Orientation;
use super::extras::{DecodedExtras, PreserveConfig};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OrientationHint {
Preserve,
#[default]
Correct,
CorrectAndTransform(Orientation),
ExactTransform(Orientation),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ChromaUpsampling {
NearestNeighbor,
#[default]
Triangle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum IdctMethod {
#[default]
Jpegli,
Libjpeg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DeblockMode {
#[default]
Off,
Auto,
AutoStreamable,
Boundary4Tap,
Knusperli,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ParallelStrategy {
PerSegment,
Grouped {
groups_per_thread: usize,
},
FixedStride(usize),
#[default]
Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Strictness {
Strict,
#[default]
Balanced,
Lenient,
Permissive,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DecodeWarning {
MissingHuffmanTables,
TruncatedScan {
blocks_decoded: u32,
blocks_expected: u32,
},
PaddingBlockError,
DnlHeightConflict {
sof_height: u32,
dnl_height: u32,
},
TruncatedProgressiveScan,
AcIndexOverflow,
InvalidHuffmanCode,
ZeroQuantValue {
table_idx: u8,
},
MalformedSegmentSkipped,
RestartMarkerResync {
count: u32,
},
ExtraneousBytesSkipped {
count: u32,
},
}
impl core::fmt::Display for DecodeWarning {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::MissingHuffmanTables => {
write!(f, "missing DHT markers; standard Huffman tables used")
}
Self::TruncatedScan {
blocks_decoded,
blocks_expected,
} => write!(
f,
"scan truncated at block {}/{}; remaining filled with zeros",
blocks_decoded, blocks_expected
),
Self::PaddingBlockError => {
write!(f, "padding block decode failed; filled with zeros")
}
Self::DnlHeightConflict {
sof_height,
dnl_height,
} => write!(
f,
"DNL height {} conflicts with SOF height {}; DNL ignored",
dnl_height, sof_height
),
Self::TruncatedProgressiveScan => {
write!(
f,
"progressive scan truncated; remaining coefficients are zero"
)
}
Self::AcIndexOverflow => {
write!(f, "AC index overflow; treated as end-of-block")
}
Self::InvalidHuffmanCode => {
write!(f, "invalid Huffman code; treated as end-of-block")
}
Self::ZeroQuantValue { table_idx } => {
write!(
f,
"zero quantization value in table {}; clamped to 1",
table_idx
)
}
Self::MalformedSegmentSkipped => {
write!(f, "malformed segment with invalid length; skipped")
}
Self::RestartMarkerResync { count } => {
write!(f, "{} restart marker resync(s) during scan", count)
}
Self::ExtraneousBytesSkipped { count } => {
write!(f, "{} extraneous byte(s) skipped between markers", count)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum OutputTarget {
#[default]
Srgb8,
SrgbF32,
LinearF32,
SrgbF32Precise,
LinearF32Precise,
}
impl OutputTarget {
#[inline]
#[must_use]
pub fn is_f32(self) -> bool {
!matches!(self, Self::Srgb8)
}
#[inline]
#[must_use]
pub fn is_linear(self) -> bool {
matches!(self, Self::LinearF32 | Self::LinearF32Precise)
}
#[inline]
#[must_use]
pub fn is_precise(self) -> bool {
matches!(self, Self::SrgbF32Precise | Self::LinearF32Precise)
}
#[inline]
pub(crate) fn needs_unclamped_idct(self) -> bool {
matches!(self, Self::SrgbF32 | Self::LinearF32)
}
#[inline]
pub(crate) fn uses_dequant_bias(self) -> bool {
self.is_precise()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum GainMapHandling {
#[default]
Discard,
PreserveRaw,
Decode,
}
#[derive(Debug, Clone)]
pub struct GainMapResult {
pub jpeg: Vec<u8>,
pub pixels: Option<Vec<u8>>,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CropRegion {
Pixels {
x: u32,
y: u32,
width: u32,
height: u32,
},
Percent {
x: f32,
y: f32,
width: f32,
height: f32,
},
}
impl CropRegion {
#[must_use]
pub fn pixels(x: u32, y: u32, width: u32, height: u32) -> Self {
Self::Pixels {
x,
y,
width,
height,
}
}
#[must_use]
pub fn percent(x: f32, y: f32, width: f32, height: f32) -> Self {
Self::Percent {
x,
y,
width,
height,
}
}
pub(crate) fn resolve(
self,
img_w: u32,
img_h: u32,
mcu_height: usize,
) -> crate::error::Result<ResolvedCrop> {
let (x, y, w, h) = match self {
CropRegion::Pixels {
x,
y,
width,
height,
} => (x, y, width, height),
CropRegion::Percent {
x,
y,
width,
height,
} => {
if !(0.0..=1.0).contains(&x)
|| !(0.0..=1.0).contains(&y)
|| !(0.0..=1.0).contains(&width)
|| !(0.0..=1.0).contains(&height)
{
return Err(crate::error::Error::invalid_jpeg_data(
"crop percentages must be in 0.0..=1.0",
));
}
let px = (x * img_w as f32).round() as u32;
let py = (y * img_h as f32).round() as u32;
let pw = (width * img_w as f32).round() as u32;
let ph = (height * img_h as f32).round() as u32;
(px, py, pw, ph)
}
};
if w == 0 || h == 0 {
return Err(crate::error::Error::invalid_jpeg_data(
"crop region must have non-zero width and height",
));
}
if x.saturating_add(w) > img_w || y.saturating_add(h) > img_h {
return Err(crate::error::Error::invalid_jpeg_data(
"crop region extends beyond image bounds",
));
}
let crop_end_y = (y + h) as usize;
let mcu_row_start = y as usize / mcu_height;
let mcu_row_end = (crop_end_y + mcu_height - 1) / mcu_height;
Ok(ResolvedCrop {
x,
y,
width: w,
height: h,
mcu_row_start,
mcu_row_end,
})
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ResolvedCrop {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
pub mcu_row_start: usize,
pub mcu_row_end: usize,
}
#[derive(Clone)]
pub struct DecodeConfig {
pub output_format: Option<crate::types::PixelFormat>,
pub output_target: OutputTarget,
pub gain_map: GainMapHandling,
pub chroma_upsampling: ChromaUpsampling,
pub correct_color: Option<TargetColorSpace>,
pub(crate) max_pixels: u64,
pub(crate) max_memory: u64,
pub preserve: PreserveConfig,
pub strictness: Strictness,
pub(crate) auto_orient: bool,
pub(crate) decode_transform: Option<LosslessTransform>,
pub(crate) force_f32_idct: bool,
pub(crate) crop_region: Option<CropRegion>,
pub(crate) num_threads: usize,
pub(crate) parallel_strategy: ParallelStrategy,
pub(crate) idct_method: Option<IdctMethod>,
pub(crate) deblock_mode: DeblockMode,
}
impl core::fmt::Debug for DecodeConfig {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DecodeConfig")
.field("output_format", &self.output_format)
.field("output_target", &self.output_target)
.field("gain_map", &self.gain_map)
.field("chroma_upsampling", &self.chroma_upsampling)
.field("correct_color", &self.correct_color)
.field("max_pixels", &self.max_pixels)
.field("max_memory", &self.max_memory)
.field("preserve", &self.preserve)
.field("strictness", &self.strictness)
.field("auto_orient", &self.auto_orient)
.field("decode_transform", &self.decode_transform)
.field("crop_region", &self.crop_region)
.field("num_threads", &self.num_threads)
.field("parallel_strategy", &self.parallel_strategy)
.field("idct_method", &self.idct_method)
.field("deblock_mode", &self.deblock_mode)
.finish()
}
}
impl Default for DecodeConfig {
fn default() -> Self {
Self {
output_format: None,
output_target: OutputTarget::default(),
gain_map: GainMapHandling::default(),
chroma_upsampling: ChromaUpsampling::default(),
correct_color: None,
max_pixels: DEFAULT_MAX_PIXELS,
max_memory: DEFAULT_MAX_MEMORY,
preserve: PreserveConfig::default(),
strictness: Strictness::default(),
auto_orient: true,
decode_transform: None,
force_f32_idct: false,
crop_region: None,
num_threads: 0,
parallel_strategy: ParallelStrategy::default(),
idct_method: None,
deblock_mode: DeblockMode::default(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum DecodedPixels<'a> {
U8(&'a [u8]),
F32(&'a [f32]),
}
#[derive(Debug, Clone)]
pub enum OwnedDecodedPixels {
U8(Vec<u8>),
F32(Vec<f32>),
}
#[derive(Clone)]
#[non_exhaustive]
pub struct DecodeResult {
pub width: u32,
pub height: u32,
pub format: crate::types::PixelFormat,
output_target: OutputTarget,
pixels_u8: Option<Vec<u8>>,
pixels_f32: Option<Vec<f32>>,
pub gain_map: Option<GainMapResult>,
pub(crate) extras: Option<DecodedExtras>,
pub(crate) warnings: Vec<DecodeWarning>,
}
impl core::fmt::Debug for DecodeResult {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DecodeResult")
.field("width", &self.width)
.field("height", &self.height)
.field("format", &self.format)
.field("output_target", &self.output_target)
.field("pixels_u8_len", &self.pixels_u8.as_ref().map(|v| v.len()))
.field("pixels_f32_len", &self.pixels_f32.as_ref().map(|v| v.len()))
.field("has_gain_map", &self.gain_map.is_some())
.field("has_extras", &self.extras.is_some())
.finish()
}
}
impl DecodeResult {
pub(crate) fn new_u8(
width: u32,
height: u32,
format: crate::types::PixelFormat,
output_target: OutputTarget,
pixels: Vec<u8>,
extras: Option<DecodedExtras>,
warnings: Vec<DecodeWarning>,
) -> Self {
Self {
width,
height,
format,
output_target,
pixels_u8: Some(pixels),
pixels_f32: None,
gain_map: None,
extras,
warnings,
}
}
pub(crate) fn new_f32(
width: u32,
height: u32,
format: crate::types::PixelFormat,
output_target: OutputTarget,
pixels: Vec<f32>,
extras: Option<DecodedExtras>,
warnings: Vec<DecodeWarning>,
) -> Self {
Self {
width,
height,
format,
output_target,
pixels_u8: None,
pixels_f32: Some(pixels),
gain_map: None,
extras,
warnings,
}
}
pub(crate) fn set_gain_map(&mut self, gain_map: Option<GainMapResult>) {
self.gain_map = gain_map;
}
#[must_use]
pub fn width(&self) -> u32 {
self.width
}
#[must_use]
pub fn height(&self) -> u32 {
self.height
}
#[must_use]
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
#[must_use]
pub fn format(&self) -> crate::types::PixelFormat {
self.format
}
#[must_use]
pub fn output_target(&self) -> OutputTarget {
self.output_target
}
#[must_use]
pub fn pixels_u8(&self) -> Option<&[u8]> {
self.pixels_u8.as_deref()
}
#[must_use]
pub fn pixels_f32(&self) -> Option<&[f32]> {
self.pixels_f32.as_deref()
}
#[must_use]
pub fn into_pixels_u8(self) -> Option<Vec<u8>> {
self.pixels_u8
}
#[must_use]
pub fn into_pixels_f32(self) -> Option<Vec<f32>> {
self.pixels_f32
}
#[must_use]
pub fn pixels(&self) -> DecodedPixels<'_> {
if let Some(ref data) = self.pixels_u8 {
DecodedPixels::U8(data)
} else if let Some(ref data) = self.pixels_f32 {
DecodedPixels::F32(data)
} else {
panic!("DecodeResult contains no pixel data")
}
}
#[must_use]
pub fn into_pixels(self) -> OwnedDecodedPixels {
if let Some(data) = self.pixels_u8 {
OwnedDecodedPixels::U8(data)
} else if let Some(data) = self.pixels_f32 {
OwnedDecodedPixels::F32(data)
} else {
panic!("DecodeResult contains no pixel data")
}
}
#[must_use]
pub fn bytes_per_pixel(&self) -> usize {
self.format.bytes_per_pixel()
}
#[must_use]
pub fn stride(&self) -> usize {
if self.output_target.is_f32() {
self.width as usize * self.format.num_channels()
} else {
self.width as usize * self.bytes_per_pixel()
}
}
#[must_use]
pub fn extras(&self) -> Option<&DecodedExtras> {
self.extras.as_ref()
}
#[must_use]
pub fn take_extras(&mut self) -> Option<DecodedExtras> {
self.extras.take()
}
#[must_use]
pub fn warnings(&self) -> &[DecodeWarning] {
&self.warnings
}
#[must_use]
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
#[must_use]
pub fn to_u16(&self) -> Option<Vec<u16>> {
let data = self.pixels_f32.as_ref()?;
let len = data.len();
let mut result = vec![0u16; len];
for i in 0..len {
result[i] = (data[i] * 65535.0).round().clamp(0.0, 65535.0) as u16;
}
Some(result)
}
#[must_use]
pub fn into_parts(
self,
) -> (
Option<Vec<u8>>,
Option<Vec<f32>>,
u32,
u32,
crate::types::PixelFormat,
Option<DecodedExtras>,
) {
(
self.pixels_u8,
self.pixels_f32,
self.width,
self.height,
self.format,
self.extras,
)
}
}
#[derive(Debug, Clone)]
pub struct DecodeInfo {
pub width: u32,
pub height: u32,
pub format: crate::types::PixelFormat,
pub bytes_written: usize,
pub gain_map: Option<GainMapResult>,
pub(crate) extras: Option<DecodedExtras>,
pub(crate) warnings: Vec<DecodeWarning>,
}
impl DecodeInfo {
#[must_use]
pub fn extras(&self) -> Option<&DecodedExtras> {
self.extras.as_ref()
}
#[must_use]
pub fn take_extras(&mut self) -> Option<DecodedExtras> {
self.extras.take()
}
#[must_use]
pub fn warnings(&self) -> &[DecodeWarning] {
&self.warnings
}
}
#[derive(Debug, Clone)]
pub struct JpegInfo {
pub dimensions: Dimensions,
pub color_space: crate::types::ColorSpace,
pub precision: u8,
pub num_components: u8,
pub mode: crate::types::JpegMode,
pub subsampling: crate::types::Subsampling,
pub has_icc_profile: bool,
pub is_xyb: bool,
pub icc_profile: Option<Vec<u8>>,
pub exif: Option<Vec<u8>>,
pub xmp: Option<String>,
pub jfif: Option<crate::encode::extras::JfifInfo>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_crop_basic() {
let crop = CropRegion::pixels(10, 20, 100, 50);
let resolved = crop.resolve(640, 480, 16).unwrap();
assert_eq!(resolved.x, 10);
assert_eq!(resolved.y, 20);
assert_eq!(resolved.width, 100);
assert_eq!(resolved.height, 50);
assert_eq!(resolved.mcu_row_start, 1); assert_eq!(resolved.mcu_row_end, 5); }
#[test]
fn resolve_crop_percent() {
let crop = CropRegion::percent(0.25, 0.25, 0.5, 0.5);
let resolved = crop.resolve(640, 480, 16).unwrap();
assert_eq!(resolved.x, 160);
assert_eq!(resolved.y, 120);
assert_eq!(resolved.width, 320);
assert_eq!(resolved.height, 240);
}
#[test]
fn resolve_crop_out_of_bounds() {
let crop = CropRegion::pixels(600, 0, 100, 100);
assert!(crop.resolve(640, 480, 16).is_err());
}
}