use crate::decode::{ComponentCoefficients, DecodedCoefficients};
use crate::foundation::consts::JPEG_NATURAL_ORDER;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LosslessTransform {
None,
FlipHorizontal,
FlipVertical,
Transpose,
Rotate90,
Rotate180,
Rotate270,
Transverse,
}
impl LosslessTransform {
#[must_use]
pub fn swaps_dimensions(self) -> bool {
matches!(
self,
Self::Transpose | Self::Rotate90 | Self::Rotate270 | Self::Transverse
)
}
#[must_use]
pub fn output_dimensions(self, width: u32, height: u32) -> (u32, u32) {
if self.swaps_dimensions() {
(height, width)
} else {
(width, height)
}
}
pub const ALL: [Self; 8] = [
Self::None,
Self::FlipHorizontal,
Self::FlipVertical,
Self::Transpose,
Self::Rotate90,
Self::Rotate180,
Self::Rotate270,
Self::Transverse,
];
#[inline]
const fn to_index(self) -> usize {
match self {
Self::None => 0,
Self::FlipHorizontal => 1,
Self::FlipVertical => 2,
Self::Transpose => 3,
Self::Rotate90 => 4,
Self::Rotate180 => 5,
Self::Rotate270 => 6,
Self::Transverse => 7,
}
}
#[must_use]
pub fn then(self, other: Self) -> Self {
Self::ALL[Self::CAYLEY[self.to_index()][other.to_index()]]
}
#[must_use]
pub fn inverse(self) -> Self {
match self {
Self::None => Self::None,
Self::FlipHorizontal => Self::FlipHorizontal,
Self::FlipVertical => Self::FlipVertical,
Self::Transpose => Self::Transpose,
Self::Rotate90 => Self::Rotate270,
Self::Rotate180 => Self::Rotate180,
Self::Rotate270 => Self::Rotate90,
Self::Transverse => Self::Transverse,
}
}
#[rustfmt::skip]
const CAYLEY: [[usize; 8]; 8] = [
[0, 1, 2, 3, 4, 5, 6, 7],
[1, 0, 5, 6, 7, 2, 3, 4],
[2, 5, 0, 4, 3, 1, 7, 6],
[3, 4, 6, 0, 1, 7, 2, 5],
[4, 3, 7, 2, 5, 6, 0, 1],
[5, 2, 1, 7, 6, 0, 4, 3],
[6, 7, 3, 1, 0, 4, 5, 2],
[7, 6, 4, 5, 2, 3, 1, 0],
];
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum EdgeHandling {
#[default]
TrimPartialBlocks,
RejectPartialBlocks,
}
#[derive(Clone, Debug)]
pub struct TransformConfig {
pub transform: LosslessTransform,
pub edge_handling: EdgeHandling,
}
impl Default for TransformConfig {
fn default() -> Self {
Self {
transform: LosslessTransform::None,
edge_handling: EdgeHandling::default(),
}
}
}
pub struct BlockTransform {
pub entries: [(u8, bool); 64],
}
impl BlockTransform {
#[must_use]
pub fn for_transform(transform: LosslessTransform) -> Self {
let mut entries = [(0u8, false); 64];
for src_z in 0..64usize {
let linear = JPEG_NATURAL_ORDER[src_z] as usize;
let src_row = linear / 8;
let src_col = linear % 8;
let (dst_row, dst_col, negate) = match transform {
LosslessTransform::None => (src_row, src_col, false),
LosslessTransform::FlipHorizontal => {
(src_row, src_col, src_col % 2 == 1)
}
LosslessTransform::FlipVertical => {
(src_row, src_col, src_row % 2 == 1)
}
LosslessTransform::Transpose => {
(src_col, src_row, false)
}
LosslessTransform::Rotate90 => {
(src_col, src_row, src_row % 2 == 1)
}
LosslessTransform::Rotate180 => {
(src_row, src_col, (src_row + src_col) % 2 == 1)
}
LosslessTransform::Rotate270 => {
(src_col, src_row, src_col % 2 == 1)
}
LosslessTransform::Transverse => {
(src_col, src_row, (src_row + src_col) % 2 == 1)
}
};
let dst_linear = dst_row * 8 + dst_col;
let dst_z = linear_to_zigzag(dst_linear);
entries[src_z] = (dst_z, negate);
}
Self { entries }
}
#[must_use]
pub fn apply(&self, src: &[i16; 64]) -> [i16; 64] {
let mut dst = [0i16; 64];
for (src_z, &(dst_z, negate)) in self.entries.iter().enumerate() {
let val = src[src_z];
dst[dst_z as usize] = if negate { -val } else { val };
}
dst
}
}
fn linear_to_zigzag(linear: usize) -> u8 {
debug_assert!(linear < 64);
ZIGZAG_FROM_LINEAR[linear]
}
pub(crate) const ZIGZAG_FROM_LINEAR: [u8; 64] = build_zigzag_from_linear();
const fn build_zigzag_from_linear() -> [u8; 64] {
let mut table = [0u8; 64];
let mut z = 0;
while z < 64 {
table[JPEG_NATURAL_ORDER[z] as usize] = z as u8;
z += 1;
}
table
}
pub struct TransformedCoefficients {
pub width: u32,
pub height: u32,
pub components: Vec<ComponentCoefficients>,
pub quant_tables: Vec<Option<[u16; 64]>>,
}
pub fn transform_coefficients(
coeffs: &DecodedCoefficients,
config: &TransformConfig,
) -> Result<TransformedCoefficients, TransformError> {
if config.transform == LosslessTransform::None {
return Ok(TransformedCoefficients {
width: coeffs.width,
height: coeffs.height,
components: coeffs.components.clone(),
quant_tables: coeffs.quant_tables.clone(),
});
}
let block_transform = BlockTransform::for_transform(config.transform);
let swaps = config.transform.swaps_dimensions();
let max_h_samp = coeffs
.components
.iter()
.map(|c| c.h_samp)
.max()
.unwrap_or(1);
let max_v_samp = coeffs
.components
.iter()
.map(|c| c.v_samp)
.max()
.unwrap_or(1);
let mcu_width = max_h_samp as u32 * 8;
let mcu_height = max_v_samp as u32 * 8;
let needs_h_trim = coeffs.width % mcu_width != 0;
let needs_v_trim = coeffs.height % mcu_height != 0;
let needs_trim = match config.transform {
LosslessTransform::FlipHorizontal => needs_h_trim,
LosslessTransform::FlipVertical => needs_v_trim,
LosslessTransform::Rotate90 => needs_v_trim,
LosslessTransform::Rotate180 => needs_h_trim || needs_v_trim,
LosslessTransform::Rotate270 => needs_h_trim,
LosslessTransform::Transpose => false, LosslessTransform::Transverse => needs_h_trim || needs_v_trim,
LosslessTransform::None => false,
};
if needs_trim && config.edge_handling == EdgeHandling::RejectPartialBlocks {
return Err(TransformError::NotMcuAligned {
width: coeffs.width,
height: coeffs.height,
mcu_width,
mcu_height,
});
}
let (new_width, new_height) = if swaps {
let trimmed_w = if needs_trim {
(coeffs.width / mcu_width) * mcu_width
} else {
coeffs.width
};
let trimmed_h = if needs_trim {
(coeffs.height / mcu_height) * mcu_height
} else {
coeffs.height
};
(trimmed_h, trimmed_w)
} else {
let trimmed_w = if needs_h_trim && needs_trim {
(coeffs.width / mcu_width) * mcu_width
} else {
coeffs.width
};
let trimmed_h = if needs_v_trim && needs_trim {
(coeffs.height / mcu_height) * mcu_height
} else {
coeffs.height
};
(trimmed_w, trimmed_h)
};
let mut transformed_components = Vec::with_capacity(coeffs.components.len());
for comp in &coeffs.components {
let src_bw = comp.blocks_wide;
let src_bh = comp.blocks_high;
let (dst_bw, dst_bh) = if swaps {
(src_bh, src_bw)
} else {
(src_bw, src_bh)
};
let total_blocks = dst_bw * dst_bh;
let mut dst_coeffs = vec![0i16; total_blocks * 64];
for src_by in 0..src_bh {
for src_bx in 0..src_bw {
let (dst_bx, dst_by) =
remap_block(src_bx, src_by, src_bw, src_bh, config.transform);
let src_idx = src_by * src_bw + src_bx;
let src_block = comp.block(src_idx);
let mut src_arr = [0i16; 64];
src_arr.copy_from_slice(src_block);
let dst_block = block_transform.apply(&src_arr);
let dst_idx = dst_by * dst_bw + dst_bx;
let dst_start = dst_idx * 64;
dst_coeffs[dst_start..dst_start + 64].copy_from_slice(&dst_block);
}
}
let (dst_h_samp, dst_v_samp) = if swaps {
(comp.v_samp, comp.h_samp)
} else {
(comp.h_samp, comp.v_samp)
};
transformed_components.push(ComponentCoefficients {
id: comp.id,
coeffs: dst_coeffs,
blocks_wide: dst_bw,
blocks_high: dst_bh,
h_samp: dst_h_samp,
v_samp: dst_v_samp,
quant_table_idx: comp.quant_table_idx,
});
}
Ok(TransformedCoefficients {
width: new_width,
height: new_height,
components: transformed_components,
quant_tables: coeffs.quant_tables.clone(),
})
}
pub(crate) fn remap_block(
src_bx: usize,
src_by: usize,
blocks_wide: usize,
blocks_high: usize,
transform: LosslessTransform,
) -> (usize, usize) {
match transform {
LosslessTransform::None => (src_bx, src_by),
LosslessTransform::FlipHorizontal => (blocks_wide - 1 - src_bx, src_by),
LosslessTransform::FlipVertical => (src_bx, blocks_high - 1 - src_by),
LosslessTransform::Transpose => {
(src_by, src_bx)
}
LosslessTransform::Rotate90 => {
(blocks_height_minus_1(blocks_high) - src_by, src_bx)
}
LosslessTransform::Rotate180 => (blocks_wide - 1 - src_bx, blocks_high - 1 - src_by),
LosslessTransform::Rotate270 => {
(src_by, blocks_wide - 1 - src_bx)
}
LosslessTransform::Transverse => {
(blocks_high - 1 - src_by, blocks_wide - 1 - src_bx)
}
}
}
fn blocks_height_minus_1(blocks_high: usize) -> usize {
debug_assert!(blocks_high > 0);
blocks_high - 1
}
#[derive(Clone, Debug)]
pub enum TransformError {
NotMcuAligned {
width: u32,
height: u32,
mcu_width: u32,
mcu_height: u32,
},
}
impl core::fmt::Display for TransformError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotMcuAligned {
width,
height,
mcu_width,
mcu_height,
} => {
write!(
f,
"image dimensions {}×{} are not aligned to MCU size {}×{} \
(use EdgeHandling::TrimPartialBlocks or resize to {}×{})",
width,
height,
mcu_width,
mcu_height,
(width / mcu_width) * mcu_width,
(height / mcu_height) * mcu_height,
)
}
}
}
}