use super::coeff_transform::*;
use crate::foundation::consts::JPEG_NATURAL_ORDER;
fn make_test_block() -> [i16; 64] {
let mut block = [0i16; 64];
for z in 0..64 {
let linear = JPEG_NATURAL_ORDER[z] as usize;
block[z] = (linear + 1) as i16;
}
block
}
fn read_at(block: &[i16; 64], row: usize, col: usize) -> i16 {
let linear = row * 8 + col;
let z = super::coeff_transform::ZIGZAG_FROM_LINEAR[linear] as usize;
block[z]
}
#[test]
fn test_zigzag_roundtrip() {
for z in 0..64usize {
let linear = JPEG_NATURAL_ORDER[z] as usize;
let back = super::coeff_transform::ZIGZAG_FROM_LINEAR[linear];
assert_eq!(back as usize, z, "roundtrip failed for zigzag index {z}");
}
}
#[test]
fn test_identity_transform() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::None);
let result = bt.apply(&block);
assert_eq!(
block, result,
"identity transform should not change coefficients"
);
}
#[test]
fn test_flip_horizontal_negates_odd_cols() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::FlipHorizontal);
let result = bt.apply(&block);
for row in 0..8 {
for col in 0..8 {
let src_val = read_at(&block, row, col);
let dst_val = read_at(&result, row, col);
if col % 2 == 1 {
assert_eq!(
dst_val, -src_val,
"H-flip should negate at ({row},{col}): expected {}, got {dst_val}",
-src_val
);
} else {
assert_eq!(
dst_val, src_val,
"H-flip should preserve at ({row},{col}): expected {src_val}, got {dst_val}"
);
}
}
}
}
#[test]
fn test_flip_vertical_negates_odd_rows() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::FlipVertical);
let result = bt.apply(&block);
for row in 0..8 {
for col in 0..8 {
let src_val = read_at(&block, row, col);
let dst_val = read_at(&result, row, col);
if row % 2 == 1 {
assert_eq!(dst_val, -src_val, "V-flip should negate at ({row},{col})");
} else {
assert_eq!(dst_val, src_val, "V-flip should preserve at ({row},{col})");
}
}
}
}
#[test]
fn test_transpose_swaps_row_col() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::Transpose);
let result = bt.apply(&block);
for row in 0..8 {
for col in 0..8 {
let src_val = read_at(&block, row, col);
let dst_val = read_at(&result, col, row);
assert_eq!(
dst_val, src_val,
"Transpose: src({row},{col})={src_val} should appear at dst({col},{row}), got {dst_val}"
);
}
}
}
#[test]
fn test_rotate90_is_transpose_plus_hflip() {
let block = make_test_block();
let bt_rot90 = BlockTransform::for_transform(LosslessTransform::Rotate90);
let result_rot90 = bt_rot90.apply(&block);
let bt_transpose = BlockTransform::for_transform(LosslessTransform::Transpose);
let bt_hflip = BlockTransform::for_transform(LosslessTransform::FlipHorizontal);
let intermediate = bt_transpose.apply(&block);
let result_composed = bt_hflip.apply(&intermediate);
assert_eq!(
result_rot90, result_composed,
"Rotate90 should equal Transpose + FlipHorizontal"
);
}
#[test]
fn test_rotate180_is_hflip_plus_vflip() {
let block = make_test_block();
let bt_rot180 = BlockTransform::for_transform(LosslessTransform::Rotate180);
let result_rot180 = bt_rot180.apply(&block);
let bt_hflip = BlockTransform::for_transform(LosslessTransform::FlipHorizontal);
let bt_vflip = BlockTransform::for_transform(LosslessTransform::FlipVertical);
let intermediate = bt_hflip.apply(&block);
let result_composed = bt_vflip.apply(&intermediate);
assert_eq!(
result_rot180, result_composed,
"Rotate180 should equal FlipHorizontal + FlipVertical"
);
}
#[test]
fn test_rotate270_is_transpose_plus_vflip() {
let block = make_test_block();
let bt_rot270 = BlockTransform::for_transform(LosslessTransform::Rotate270);
let result_rot270 = bt_rot270.apply(&block);
let bt_transpose = BlockTransform::for_transform(LosslessTransform::Transpose);
let bt_vflip = BlockTransform::for_transform(LosslessTransform::FlipVertical);
let intermediate = bt_transpose.apply(&block);
let result_composed = bt_vflip.apply(&intermediate);
assert_eq!(
result_rot270, result_composed,
"Rotate270 should equal Transpose + FlipVertical"
);
}
#[test]
fn test_transverse_is_transpose_plus_rot180() {
let block = make_test_block();
let bt_transverse = BlockTransform::for_transform(LosslessTransform::Transverse);
let result_transverse = bt_transverse.apply(&block);
let bt_transpose = BlockTransform::for_transform(LosslessTransform::Transpose);
let bt_rot180 = BlockTransform::for_transform(LosslessTransform::Rotate180);
let intermediate = bt_transpose.apply(&block);
let result_composed = bt_rot180.apply(&intermediate);
assert_eq!(
result_transverse, result_composed,
"Transverse should equal Transpose + Rotate180"
);
}
#[test]
fn test_dc_coefficient_never_negated() {
let mut block = [0i16; 64];
block[0] = 1000;
for transform in [
LosslessTransform::None,
LosslessTransform::FlipHorizontal,
LosslessTransform::FlipVertical,
LosslessTransform::Transpose,
LosslessTransform::Rotate90,
LosslessTransform::Rotate180,
LosslessTransform::Rotate270,
LosslessTransform::Transverse,
] {
let bt = BlockTransform::for_transform(transform);
let result = bt.apply(&block);
assert_eq!(
read_at(&result, 0, 0),
1000,
"DC should not be negated by {:?}",
transform
);
}
}
#[test]
fn test_four_rotations_is_identity() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::Rotate90);
let r1 = bt.apply(&block);
let r2 = bt.apply(&r1);
let r3 = bt.apply(&r2);
let r4 = bt.apply(&r3);
assert_eq!(block, r4, "four 90° rotations should be identity");
}
#[test]
fn test_double_flip_h_is_identity() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::FlipHorizontal);
let r1 = bt.apply(&block);
let r2 = bt.apply(&r1);
assert_eq!(block, r2, "double horizontal flip should be identity");
}
#[test]
fn test_double_flip_v_is_identity() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::FlipVertical);
let r1 = bt.apply(&block);
let r2 = bt.apply(&r1);
assert_eq!(block, r2, "double vertical flip should be identity");
}
#[test]
fn test_double_transpose_is_identity() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::Transpose);
let r1 = bt.apply(&block);
let r2 = bt.apply(&r1);
assert_eq!(block, r2, "double transpose should be identity");
}
#[test]
fn test_double_rot180_is_identity() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::Rotate180);
let r1 = bt.apply(&block);
let r2 = bt.apply(&r1);
assert_eq!(block, r2, "double 180° rotation should be identity");
}
#[test]
fn test_rot90_plus_rot270_is_identity() {
let block = make_test_block();
let bt90 = BlockTransform::for_transform(LosslessTransform::Rotate90);
let bt270 = BlockTransform::for_transform(LosslessTransform::Rotate270);
let r1 = bt90.apply(&block);
let r2 = bt270.apply(&r1);
assert_eq!(block, r2, "90° + 270° should be identity");
}
#[test]
fn test_double_transverse_is_identity() {
let block = make_test_block();
let bt = BlockTransform::for_transform(LosslessTransform::Transverse);
let r1 = bt.apply(&block);
let r2 = bt.apply(&r1);
assert_eq!(block, r2, "double transverse should be identity");
}
#[test]
fn test_swaps_dimensions() {
assert!(!LosslessTransform::None.swaps_dimensions());
assert!(!LosslessTransform::FlipHorizontal.swaps_dimensions());
assert!(!LosslessTransform::FlipVertical.swaps_dimensions());
assert!(LosslessTransform::Transpose.swaps_dimensions());
assert!(LosslessTransform::Rotate90.swaps_dimensions());
assert!(!LosslessTransform::Rotate180.swaps_dimensions());
assert!(LosslessTransform::Rotate270.swaps_dimensions());
assert!(LosslessTransform::Transverse.swaps_dimensions());
}
#[test]
fn test_block_remap_identity() {
assert_eq!(remap_block(2, 3, 10, 8, LosslessTransform::None), (2, 3));
}
#[test]
fn test_block_remap_hflip() {
assert_eq!(
remap_block(2, 3, 10, 8, LosslessTransform::FlipHorizontal),
(7, 3)
);
}
#[test]
fn test_block_remap_vflip() {
assert_eq!(
remap_block(2, 3, 10, 8, LosslessTransform::FlipVertical),
(2, 4)
);
}
#[test]
fn test_block_remap_transpose() {
assert_eq!(
remap_block(2, 3, 10, 8, LosslessTransform::Transpose),
(3, 2)
);
}
#[test]
fn test_block_remap_rot90() {
assert_eq!(
remap_block(2, 3, 10, 8, LosslessTransform::Rotate90),
(4, 2)
);
}
#[test]
fn test_block_remap_rot180() {
assert_eq!(
remap_block(2, 3, 10, 8, LosslessTransform::Rotate180),
(7, 4)
);
}
#[test]
fn test_block_remap_rot270() {
assert_eq!(
remap_block(2, 3, 10, 8, LosslessTransform::Rotate270),
(3, 7)
);
}
#[test]
fn test_block_remap_transverse() {
assert_eq!(
remap_block(2, 3, 10, 8, LosslessTransform::Transverse),
(4, 7)
);
}
use crate::decode::{ComponentCoefficients, DecodedCoefficients};
fn make_test_coefficients() -> DecodedCoefficients {
let mut coeffs = vec![0i16; 4 * 64];
coeffs[0] = 10; coeffs[64] = 20; coeffs[2 * 64] = 30; coeffs[3 * 64] = 40;
DecodedCoefficients {
width: 16,
height: 16,
components: vec![ComponentCoefficients {
id: 1,
coeffs,
blocks_wide: 2,
blocks_high: 2,
h_samp: 1,
v_samp: 1,
quant_table_idx: 0,
}],
quant_tables: vec![Some([1u16; 64])],
}
}
fn get_dc(result: &TransformedCoefficients, comp: usize, bx: usize, by: usize) -> i16 {
let c = &result.components[comp];
let idx = by * c.blocks_wide + bx;
c.coeffs[idx * 64]
}
#[test]
fn test_transform_identity() {
let coeffs = make_test_coefficients();
let config = TransformConfig {
transform: LosslessTransform::None,
edge_handling: EdgeHandling::TrimPartialBlocks,
};
let result = transform_coefficients(&coeffs, &config).unwrap();
assert_eq!(result.width, 16);
assert_eq!(result.height, 16);
assert_eq!(get_dc(&result, 0, 0, 0), 10);
assert_eq!(get_dc(&result, 0, 1, 0), 20);
assert_eq!(get_dc(&result, 0, 0, 1), 30);
assert_eq!(get_dc(&result, 0, 1, 1), 40);
}
#[test]
fn test_transform_hflip_block_positions() {
let coeffs = make_test_coefficients();
let config = TransformConfig {
transform: LosslessTransform::FlipHorizontal,
edge_handling: EdgeHandling::TrimPartialBlocks,
};
let result = transform_coefficients(&coeffs, &config).unwrap();
assert_eq!(result.width, 16);
assert_eq!(result.height, 16);
assert_eq!(get_dc(&result, 0, 0, 0), 20); assert_eq!(get_dc(&result, 0, 1, 0), 10); assert_eq!(get_dc(&result, 0, 0, 1), 40); assert_eq!(get_dc(&result, 0, 1, 1), 30); }
#[test]
fn test_transform_vflip_block_positions() {
let coeffs = make_test_coefficients();
let config = TransformConfig {
transform: LosslessTransform::FlipVertical,
edge_handling: EdgeHandling::TrimPartialBlocks,
};
let result = transform_coefficients(&coeffs, &config).unwrap();
assert_eq!(get_dc(&result, 0, 0, 0), 30); assert_eq!(get_dc(&result, 0, 1, 0), 40); assert_eq!(get_dc(&result, 0, 0, 1), 10); assert_eq!(get_dc(&result, 0, 1, 1), 20); }
#[test]
fn test_transform_transpose_swaps_dims() {
let coeffs = make_test_coefficients();
let config = TransformConfig {
transform: LosslessTransform::Transpose,
edge_handling: EdgeHandling::TrimPartialBlocks,
};
let result = transform_coefficients(&coeffs, &config).unwrap();
assert_eq!(result.width, 16); assert_eq!(result.height, 16);
assert_eq!(get_dc(&result, 0, 0, 0), 10);
assert_eq!(get_dc(&result, 0, 1, 0), 30); assert_eq!(get_dc(&result, 0, 0, 1), 20); assert_eq!(get_dc(&result, 0, 1, 1), 40);
}
#[test]
fn test_transform_rot90_block_positions() {
let coeffs = make_test_coefficients();
let config = TransformConfig {
transform: LosslessTransform::Rotate90,
edge_handling: EdgeHandling::TrimPartialBlocks,
};
let result = transform_coefficients(&coeffs, &config).unwrap();
assert_eq!(get_dc(&result, 0, 0, 0), 30); assert_eq!(get_dc(&result, 0, 1, 0), 10); assert_eq!(get_dc(&result, 0, 0, 1), 40); assert_eq!(get_dc(&result, 0, 1, 1), 20); }
#[test]
fn test_transform_rot180_block_positions() {
let coeffs = make_test_coefficients();
let config = TransformConfig {
transform: LosslessTransform::Rotate180,
edge_handling: EdgeHandling::TrimPartialBlocks,
};
let result = transform_coefficients(&coeffs, &config).unwrap();
assert_eq!(get_dc(&result, 0, 0, 0), 40); assert_eq!(get_dc(&result, 0, 1, 0), 30); assert_eq!(get_dc(&result, 0, 0, 1), 20); assert_eq!(get_dc(&result, 0, 1, 1), 10); }
#[test]
fn test_transform_nonsquare_transpose() {
let mut coeffs_data = vec![0i16; 6 * 64];
for i in 0..6 {
coeffs_data[i * 64] = (i as i16 + 1) * 10; }
let coeffs = DecodedCoefficients {
width: 24,
height: 16,
components: vec![ComponentCoefficients {
id: 1,
coeffs: coeffs_data,
blocks_wide: 3,
blocks_high: 2,
h_samp: 1,
v_samp: 1,
quant_table_idx: 0,
}],
quant_tables: vec![Some([1u16; 64])],
};
let config = TransformConfig {
transform: LosslessTransform::Transpose,
edge_handling: EdgeHandling::TrimPartialBlocks,
};
let result = transform_coefficients(&coeffs, &config).unwrap();
assert_eq!(result.width, 16);
assert_eq!(result.height, 24);
assert_eq!(result.components[0].blocks_wide, 2);
assert_eq!(result.components[0].blocks_high, 3);
assert_eq!(get_dc(&result, 0, 0, 0), 10);
assert_eq!(get_dc(&result, 0, 1, 0), 40);
assert_eq!(get_dc(&result, 0, 0, 1), 20);
assert_eq!(get_dc(&result, 0, 1, 1), 50);
assert_eq!(get_dc(&result, 0, 0, 2), 30);
assert_eq!(get_dc(&result, 0, 1, 2), 60);
}
mod pipeline_tests {
use crate::decode::DecodeConfig;
use crate::lossless::{EdgeHandling, LosslessTransform, TransformConfig, transform};
use enough::Unstoppable;
fn create_test_jpeg(width: u32, height: u32) -> Vec<u8> {
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
let mut pixels = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
pixels.push(((x * 255 / width) & 0xFF) as u8); pixels.push(((y * 255 / height) & 0xFF) as u8); pixels.push(128u8); }
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
fn create_test_jpeg_420(width: u32, height: u32) -> Vec<u8> {
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
let mut pixels = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
pixels.push(((x * 255 / width) & 0xFF) as u8);
pixels.push(((y * 255 / height) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::Quarter);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
#[test]
fn test_roundtrip_identity() {
use archmage::testing::{CompileTimePolicy, for_each_token_permutation};
let jpeg = create_test_jpeg(64, 64);
let _ = for_each_token_permutation(CompileTimePolicy::Warn, |perm| {
let result = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::None,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
assert!(result.len() > 100, "output too small at {perm}");
assert_eq!(result[0], 0xFF);
assert_eq!(result[1], 0xD8);
let decoder = DecodeConfig::new();
let orig = decoder.decode(&jpeg, Unstoppable).unwrap();
let transformed = decoder.decode(&result, Unstoppable).unwrap();
assert_eq!(orig.width(), transformed.width());
assert_eq!(orig.height(), transformed.height());
let orig_px = orig.pixels_u8().unwrap();
let trans_px = transformed.pixels_u8().unwrap();
assert_eq!(orig_px.len(), trans_px.len());
let mut max_diff = 0u8;
for (a, b) in orig_px.iter().zip(trans_px.iter()) {
let diff = (*a as i16 - *b as i16).unsigned_abs() as u8;
max_diff = max_diff.max(diff);
}
assert_eq!(
max_diff, 0,
"identity transform should produce identical pixels at {perm}"
);
});
}
#[test]
fn test_rotate90_dimensions() {
let jpeg = create_test_jpeg(64, 48);
let result = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::Rotate90,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let decoder = DecodeConfig::new();
let decoded = decoder.decode(&result, Unstoppable).unwrap();
assert_eq!(decoded.width(), 48);
assert_eq!(decoded.height(), 64);
}
#[test]
fn test_rotate180_same_dimensions() {
let jpeg = create_test_jpeg(64, 48);
let result = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::Rotate180,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let decoder = DecodeConfig::new();
let decoded = decoder.decode(&result, Unstoppable).unwrap();
assert_eq!(decoded.width(), 64);
assert_eq!(decoded.height(), 48);
}
#[test]
fn test_double_rotate90_equals_rotate180() {
use archmage::testing::{CompileTimePolicy, for_each_token_permutation};
let jpeg = create_test_jpeg(64, 64);
let _ = for_each_token_permutation(CompileTimePolicy::Warn, |perm| {
let rot90_1 = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::Rotate90,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let rot90_2 = transform(
&rot90_1,
&TransformConfig {
transform: LosslessTransform::Rotate90,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let rot180 = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::Rotate180,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let decoder = DecodeConfig::new();
let px_2x90 = decoder.decode(&rot90_2, Unstoppable).unwrap();
let px_180 = decoder.decode(&rot180, Unstoppable).unwrap();
let px_a = px_2x90.pixels_u8().unwrap();
let px_b = px_180.pixels_u8().unwrap();
assert_eq!(px_a.len(), px_b.len());
let mut max_diff = 0u8;
for (a, b) in px_a.iter().zip(px_b.iter()) {
let diff = (*a as i16 - *b as i16).unsigned_abs() as u8;
max_diff = max_diff.max(diff);
}
assert_eq!(max_diff, 0, "2×rot90 should equal rot180 at {perm}");
});
}
#[test]
fn test_four_rotations_roundtrip() {
let jpeg = create_test_jpeg(64, 64);
let mut current = jpeg.clone();
for _ in 0..4 {
current = transform(
¤t,
&TransformConfig {
transform: LosslessTransform::Rotate90,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
}
let decoder = DecodeConfig::new();
let orig = decoder.decode(&jpeg, Unstoppable).unwrap();
let roundtrip = decoder.decode(¤t, Unstoppable).unwrap();
assert_eq!(orig.width(), roundtrip.width());
assert_eq!(orig.height(), roundtrip.height());
let px_orig = orig.pixels_u8().unwrap();
let px_rt = roundtrip.pixels_u8().unwrap();
let mut max_diff = 0u8;
for (a, b) in px_orig.iter().zip(px_rt.iter()) {
let diff = (*a as i16 - *b as i16).unsigned_abs() as u8;
max_diff = max_diff.max(diff);
}
assert_eq!(max_diff, 0, "4×rot90 should equal identity");
}
#[test]
fn test_hflip_roundtrip() {
let jpeg = create_test_jpeg(64, 48);
let flipped = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::FlipHorizontal,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let double_flipped = transform(
&flipped,
&TransformConfig {
transform: LosslessTransform::FlipHorizontal,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let decoder = DecodeConfig::new();
let orig = decoder.decode(&jpeg, Unstoppable).unwrap();
let roundtrip = decoder.decode(&double_flipped, Unstoppable).unwrap();
let px_orig = orig.pixels_u8().unwrap();
let px_rt = roundtrip.pixels_u8().unwrap();
let mut max_diff = 0u8;
for (a, b) in px_orig.iter().zip(px_rt.iter()) {
let diff = (*a as i16 - *b as i16).unsigned_abs() as u8;
max_diff = max_diff.max(diff);
}
assert_eq!(max_diff, 0, "2×hflip should equal identity");
}
#[test]
fn test_420_rotate90() {
let jpeg = create_test_jpeg_420(64, 48);
let result = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::Rotate90,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let decoder = DecodeConfig::new();
let decoded = decoder.decode(&result, Unstoppable).unwrap();
assert_eq!(decoded.width(), 48);
assert_eq!(decoded.height(), 64);
}
#[test]
fn test_all_transforms_produce_valid_jpeg() {
let jpeg = create_test_jpeg(64, 64);
let decoder = DecodeConfig::new();
for xform in [
LosslessTransform::None,
LosslessTransform::FlipHorizontal,
LosslessTransform::FlipVertical,
LosslessTransform::Transpose,
LosslessTransform::Rotate90,
LosslessTransform::Rotate180,
LosslessTransform::Rotate270,
LosslessTransform::Transverse,
] {
let result = transform(
&jpeg,
&TransformConfig {
transform: xform,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let decoded = decoder.decode(&result, Unstoppable);
assert!(
decoded.is_ok(),
"transform {:?} should produce valid JPEG, got error: {:?}",
xform,
decoded.err()
);
}
}
}
mod coefficient_roundtrip_tests {
use crate::decode::DecodeConfig;
use crate::lossless::{EdgeHandling, LosslessTransform, TransformConfig, transform};
use enough::Unstoppable;
fn create_test_jpeg(width: u32, height: u32) -> Vec<u8> {
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
let mut pixels = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
pixels.push(((x * 255 / width) & 0xFF) as u8);
pixels.push(((y * 255 / height) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
#[test]
fn test_coefficient_identity_roundtrip() {
let jpeg = create_test_jpeg(64, 64);
let decoder = DecodeConfig::new();
let orig_coeffs = decoder.decode_coefficients(&jpeg, Unstoppable).unwrap();
let result = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::None,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let new_coeffs = decoder.decode_coefficients(&result, Unstoppable).unwrap();
for (comp_idx, (c1, c2)) in orig_coeffs
.components
.iter()
.zip(&new_coeffs.components)
.enumerate()
{
assert_eq!(
c1.blocks_wide, c2.blocks_wide,
"blocks_wide mismatch comp {comp_idx}"
);
assert_eq!(
c1.blocks_high, c2.blocks_high,
"blocks_high mismatch comp {comp_idx}"
);
assert_eq!(c1.h_samp, c2.h_samp, "h_samp mismatch comp {comp_idx}");
assert_eq!(c1.v_samp, c2.v_samp, "v_samp mismatch comp {comp_idx}");
let num_blocks = c1.num_blocks().min(c2.num_blocks());
let mut diffs = 0;
let mut max_diff = 0i16;
for block_idx in 0..num_blocks {
let b1 = c1.block(block_idx);
let b2 = c2.block(block_idx);
for i in 0..64 {
let d = (b1[i] as i32 - b2[i] as i32).abs() as i16;
if d != 0 {
diffs += 1;
max_diff = max_diff.max(d);
}
}
}
if diffs > 0 {
eprintln!(
"Component {comp_idx}: {diffs} differing coefficients, max_diff={max_diff}"
);
}
assert_eq!(
diffs, 0,
"Component {comp_idx}: coefficients should be identical after identity transform"
);
}
}
#[test]
fn test_quant_tables_preserved() {
let jpeg = create_test_jpeg(64, 64);
let decoder = DecodeConfig::new();
let orig_coeffs = decoder.decode_coefficients(&jpeg, Unstoppable).unwrap();
let result = transform(
&jpeg,
&TransformConfig {
transform: LosslessTransform::None,
edge_handling: EdgeHandling::TrimPartialBlocks,
},
Unstoppable,
)
.unwrap();
let new_coeffs = decoder.decode_coefficients(&result, Unstoppable).unwrap();
for (idx, (qt1, qt2)) in orig_coeffs
.quant_tables
.iter()
.zip(&new_coeffs.quant_tables)
.enumerate()
{
match (qt1, qt2) {
(Some(t1), Some(t2)) => {
assert_eq!(t1, t2, "quant table {idx} should be preserved");
}
(None, None) => {}
_ => panic!("quant table {idx} presence mismatch"),
}
}
}
}
#[cfg(test)]
mod debug_tests {
use crate::decode::DecodeConfig;
use enough::Unstoppable;
fn create_test_jpeg_444(width: u32, height: u32) -> Vec<u8> {
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
let pixels = vec![128u8; (width * height * 3) as usize];
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
#[test]
fn debug_quant_table_indices() {
let jpeg = create_test_jpeg_444(64, 64);
let decoder = DecodeConfig::new();
let coeffs = decoder.decode_coefficients(&jpeg, Unstoppable).unwrap();
eprintln!("Number of quant table slots: {}", coeffs.quant_tables.len());
for (i, qt) in coeffs.quant_tables.iter().enumerate() {
eprintln!(
" Table {}: {}",
i,
if qt.is_some() { "present" } else { "absent" }
);
}
eprintln!("Number of components: {}", coeffs.components.len());
for (i, comp) in coeffs.components.iter().enumerate() {
eprintln!(
" Component {}: id={}, h_samp={}, v_samp={}, blocks={}x{}",
i, comp.id, comp.h_samp, comp.v_samp, comp.blocks_wide, comp.blocks_high
);
}
}
}
mod exif_tests {
use crate::lossless::coeff_transform::LosslessTransform;
use crate::lossless::exif::{parse_exif_orientation, set_exif_orientation};
fn build_exif_with_orientation_le(orientation: u16) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(b"Exif\0\0");
data.extend_from_slice(b"II");
data.extend_from_slice(&42u16.to_le_bytes()); data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&1u16.to_le_bytes());
data.extend_from_slice(&0x0112u16.to_le_bytes()); data.extend_from_slice(&3u16.to_le_bytes()); data.extend_from_slice(&1u32.to_le_bytes()); data.extend_from_slice(&(orientation as u32).to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
data
}
fn build_exif_with_orientation_be(orientation: u16) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(b"Exif\0\0");
data.extend_from_slice(b"MM");
data.extend_from_slice(&42u16.to_be_bytes());
data.extend_from_slice(&8u32.to_be_bytes());
data.extend_from_slice(&1u16.to_be_bytes());
data.extend_from_slice(&0x0112u16.to_be_bytes());
data.extend_from_slice(&3u16.to_be_bytes());
data.extend_from_slice(&1u32.to_be_bytes());
data.extend_from_slice(&orientation.to_be_bytes());
data.extend_from_slice(&[0u8; 2]); data.extend_from_slice(&0u32.to_be_bytes());
data
}
#[test]
fn test_parse_exif_orientation_le() {
for orient in 1..=8u16 {
let data = build_exif_with_orientation_le(orient);
assert_eq!(
parse_exif_orientation(&data),
Some(orient as u8),
"failed to parse LE orientation {orient}"
);
}
}
#[test]
fn test_parse_exif_orientation_be() {
for orient in 1..=8u16 {
let data = build_exif_with_orientation_be(orient);
assert_eq!(
parse_exif_orientation(&data),
Some(orient as u8),
"failed to parse BE orientation {orient}"
);
}
}
#[test]
fn test_parse_exif_orientation_no_prefix() {
assert_eq!(parse_exif_orientation(b"not exif data"), None);
}
#[test]
fn test_parse_exif_orientation_too_short() {
assert_eq!(parse_exif_orientation(b"Exif\0\0II"), None);
}
#[test]
fn test_parse_exif_orientation_invalid_value() {
let data = build_exif_with_orientation_le(0);
assert_eq!(parse_exif_orientation(&data), None);
let data = build_exif_with_orientation_le(9);
assert_eq!(parse_exif_orientation(&data), None);
}
#[test]
fn test_set_exif_orientation_le() {
let mut data = build_exif_with_orientation_le(6);
assert_eq!(parse_exif_orientation(&data), Some(6));
let modified = set_exif_orientation(&mut data, 1);
assert!(modified, "should find and modify orientation tag");
assert_eq!(parse_exif_orientation(&data), Some(1));
}
#[test]
fn test_set_exif_orientation_be() {
let mut data = build_exif_with_orientation_be(8);
assert_eq!(parse_exif_orientation(&data), Some(8));
let modified = set_exif_orientation(&mut data, 1);
assert!(modified);
assert_eq!(parse_exif_orientation(&data), Some(1));
}
#[test]
fn test_set_exif_orientation_no_tag() {
let mut data = Vec::new();
data.extend_from_slice(b"Exif\0\0");
data.extend_from_slice(b"II");
data.extend_from_slice(&42u16.to_le_bytes());
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&1u16.to_le_bytes());
data.extend_from_slice(&0x8298u16.to_le_bytes()); data.extend_from_slice(&2u16.to_le_bytes()); data.extend_from_slice(&1u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
let modified = set_exif_orientation(&mut data, 1);
assert!(!modified, "should not modify when orientation tag absent");
}
#[test]
fn test_from_exif_orientation() {
assert_eq!(
LosslessTransform::from_exif_orientation(1),
Some(LosslessTransform::None)
);
assert_eq!(
LosslessTransform::from_exif_orientation(2),
Some(LosslessTransform::FlipHorizontal)
);
assert_eq!(
LosslessTransform::from_exif_orientation(3),
Some(LosslessTransform::Rotate180)
);
assert_eq!(
LosslessTransform::from_exif_orientation(4),
Some(LosslessTransform::FlipVertical)
);
assert_eq!(
LosslessTransform::from_exif_orientation(5),
Some(LosslessTransform::Transpose)
);
assert_eq!(
LosslessTransform::from_exif_orientation(6),
Some(LosslessTransform::Rotate90)
);
assert_eq!(
LosslessTransform::from_exif_orientation(7),
Some(LosslessTransform::Transverse)
);
assert_eq!(
LosslessTransform::from_exif_orientation(8),
Some(LosslessTransform::Rotate270)
);
assert_eq!(LosslessTransform::from_exif_orientation(0), None);
assert_eq!(LosslessTransform::from_exif_orientation(9), None);
}
#[test]
fn test_apply_exif_orientation_no_exif() {
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use crate::lossless::apply_exif_orientation;
use enough::Unstoppable;
let pixels = vec![128u8; 64 * 64 * 3];
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(64, 64, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let result = apply_exif_orientation(&jpeg, Unstoppable).unwrap();
assert_eq!(result, jpeg, "no-EXIF JPEG should be returned unchanged");
}
#[test]
fn test_apply_exif_orientation_normal() {
use crate::encoder::{ChromaSubsampling, EncoderConfig, Exif, Orientation, PixelLayout};
use crate::lossless::apply_exif_orientation;
use enough::Unstoppable;
let pixels = vec![128u8; 64 * 64 * 3];
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.request()
.exif(Exif::build().orientation(Orientation::Normal))
.encode_from_bytes(64, 64, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let result = apply_exif_orientation(&jpeg, Unstoppable).unwrap();
assert_eq!(result, jpeg, "orientation=1 should be returned unchanged");
}
#[test]
fn test_apply_exif_orientation_rotate90() {
use crate::decode::{DecodeConfig, PreserveConfig};
use crate::encoder::{ChromaSubsampling, EncoderConfig, Exif, Orientation, PixelLayout};
use crate::lossless::apply_exif_orientation;
use enough::Unstoppable;
let (w, h) = (64u32, 48u32);
let mut pixels = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h {
for x in 0..w {
pixels.push(((x * 255 / w) & 0xFF) as u8);
pixels.push(((y * 255 / h) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.request()
.exif(Exif::build().orientation(Orientation::Rotate90))
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let decoder = DecodeConfig::new().preserve(PreserveConfig::all());
let src_result = decoder.decode(&jpeg, Unstoppable).unwrap();
let src_exif = src_result.extras().unwrap().exif().unwrap();
assert_eq!(parse_exif_orientation(src_exif), Some(6));
let corrected = apply_exif_orientation(&jpeg, Unstoppable).unwrap();
assert_ne!(corrected, jpeg, "rotated JPEG should differ from source");
let out_result = decoder.decode(&corrected, Unstoppable).unwrap();
assert_eq!(out_result.width(), h, "width should be original height");
assert_eq!(out_result.height(), w, "height should be original width");
let out_exif = out_result.extras().unwrap().exif().unwrap();
assert_eq!(
parse_exif_orientation(out_exif),
Some(1),
"output orientation should be reset to 1"
);
}
}
#[test]
fn test_then_identity() {
for &t in &LosslessTransform::ALL {
assert_eq!(
t.then(LosslessTransform::None),
t,
"{t:?}.then(None) should be {t:?}"
);
assert_eq!(
LosslessTransform::None.then(t),
t,
"None.then({t:?}) should be {t:?}"
);
}
}
#[test]
fn test_inverse_roundtrip() {
for &t in &LosslessTransform::ALL {
let composed = t.then(t.inverse());
assert_eq!(
composed,
LosslessTransform::None,
"{t:?}.then({:?}) should be None, got {composed:?}",
t.inverse()
);
}
}
#[test]
fn test_inverse_both_directions() {
for &t in &LosslessTransform::ALL {
let composed = t.inverse().then(t);
assert_eq!(
composed,
LosslessTransform::None,
"{:?}.then({t:?}) should be None, got {composed:?}",
t.inverse()
);
}
}
#[test]
fn test_then_cayley_table_by_block_transform() {
let src = make_test_block();
for &a in &LosslessTransform::ALL {
let bt_a = BlockTransform::for_transform(a);
for &b in &LosslessTransform::ALL {
let bt_b = BlockTransform::for_transform(b);
let after_a = bt_a.apply(&src);
let sequential = bt_b.apply(&after_a);
let composed = a.then(b);
let bt_composed = BlockTransform::for_transform(composed);
let single = bt_composed.apply(&src);
assert_eq!(
sequential, single,
"{a:?}.then({b:?}) = {composed:?} — block mismatch"
);
}
}
}
#[test]
fn test_then_known_compositions() {
use LosslessTransform::*;
assert_eq!(Transpose.then(FlipHorizontal), Rotate90);
assert_eq!(Transpose.then(FlipVertical), Rotate270);
assert_eq!(FlipHorizontal.then(FlipVertical), Rotate180);
assert_eq!(Rotate90.then(Rotate90), Rotate180);
assert_eq!(Rotate90.then(Rotate180), Rotate270);
assert_eq!(Rotate90.then(Rotate270), None);
}
#[test]
fn test_inverse_values() {
use LosslessTransform::*;
assert_eq!(None.inverse(), None);
assert_eq!(FlipHorizontal.inverse(), FlipHorizontal);
assert_eq!(FlipVertical.inverse(), FlipVertical);
assert_eq!(Transpose.inverse(), Transpose);
assert_eq!(Rotate90.inverse(), Rotate270);
assert_eq!(Rotate180.inverse(), Rotate180);
assert_eq!(Rotate270.inverse(), Rotate90);
assert_eq!(Transverse.inverse(), Transverse);
}
#[test]
fn test_decode_auto_orient() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, Exif, Orientation, PixelLayout};
use enough::Unstoppable;
let (w, h) = (64u32, 48u32);
let mut pixels = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h {
for x in 0..w {
pixels.push(((x * 255 / w) & 0xFF) as u8);
pixels.push(((y * 255 / h) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.request()
.exif(Exif::build().orientation(Orientation::Rotate90))
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let result = DecodeConfig::new()
.auto_orient(true)
.decode(&jpeg, Unstoppable)
.unwrap();
assert_eq!(
result.width(),
h,
"width should be original height after auto_orient"
);
assert_eq!(
result.height(),
w,
"height should be original width after auto_orient"
);
}
#[test]
fn test_decode_transform_rotate90() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use enough::Unstoppable;
let (w, h) = (64u32, 48u32);
let pixels = vec![128u8; (w * h * 3) as usize];
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let result = DecodeConfig::new()
.transform(LosslessTransform::Rotate90)
.decode(&jpeg, Unstoppable)
.unwrap();
assert_eq!(result.width(), h, "Rotate90 should swap dimensions");
assert_eq!(result.height(), w, "Rotate90 should swap dimensions");
}
#[test]
fn test_decode_transform_rotate180_preserves_dimensions() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use enough::Unstoppable;
let (w, h) = (64u32, 48u32);
let pixels = vec![128u8; (w * h * 3) as usize];
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let result = DecodeConfig::new()
.transform(LosslessTransform::Rotate180)
.decode(&jpeg, Unstoppable)
.unwrap();
assert_eq!(result.width(), w, "Rotate180 should preserve width");
assert_eq!(result.height(), h, "Rotate180 should preserve height");
}
#[test]
fn test_decode_composed_exif_plus_transform() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, Exif, Orientation, PixelLayout};
use enough::Unstoppable;
let (w, h) = (64u32, 48u32);
let mut pixels = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h {
for x in 0..w {
pixels.push(((x * 255 / w) & 0xFF) as u8);
pixels.push(((y * 255 / h) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.request()
.exif(Exif::build().orientation(Orientation::Rotate90))
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let composed = DecodeConfig::new()
.auto_orient(true)
.transform(LosslessTransform::Rotate270)
.decode(&jpeg, Unstoppable)
.unwrap();
assert_eq!(composed.width(), w, "composed transform should be identity");
assert_eq!(
composed.height(),
h,
"composed transform should be identity"
);
let plain = DecodeConfig::new()
.auto_orient(false)
.decode(&jpeg, Unstoppable)
.unwrap();
assert_eq!(
composed.pixels_u8().unwrap(),
plain.pixels_u8().unwrap(),
"composed identity transform should produce same pixels as plain decode"
);
}
#[test]
fn test_decode_auto_orient_noop() {
use archmage::testing::{CompileTimePolicy, for_each_token_permutation};
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, Exif, Orientation, PixelLayout};
use enough::Unstoppable;
let (w, h) = (64u32, 64u32);
let pixels = vec![128u8; (w * h * 3) as usize];
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.request()
.exif(Exif::build().orientation(Orientation::Normal))
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let _ = for_each_token_permutation(CompileTimePolicy::Warn, |perm| {
let with_orient = DecodeConfig::new()
.auto_orient(true)
.decode(&jpeg, Unstoppable)
.unwrap();
let without_orient = DecodeConfig::new().decode(&jpeg, Unstoppable).unwrap();
assert_eq!(with_orient.width(), without_orient.width());
assert_eq!(with_orient.height(), without_orient.height());
assert_eq!(
with_orient.pixels_u8().unwrap(),
without_orient.pixels_u8().unwrap(),
"orientation=1 + auto_orient should produce identical pixels at {perm}"
);
});
}
#[test]
fn test_decode_no_exif_auto_orient() {
use archmage::testing::{CompileTimePolicy, for_each_token_permutation};
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use enough::Unstoppable;
let (w, h) = (64u32, 64u32);
let pixels = vec![128u8; (w * h * 3) as usize];
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let _ = for_each_token_permutation(CompileTimePolicy::Warn, |perm| {
let with_orient = DecodeConfig::new()
.auto_orient(true)
.decode(&jpeg, Unstoppable)
.unwrap();
let without_orient = DecodeConfig::new().decode(&jpeg, Unstoppable).unwrap();
assert_eq!(with_orient.width(), without_orient.width());
assert_eq!(with_orient.height(), without_orient.height());
assert_eq!(
with_orient.pixels_u8().unwrap(),
without_orient.pixels_u8().unwrap(),
"no EXIF + auto_orient should produce identical pixels at {perm}"
);
});
}
#[test]
fn test_scanline_transform_matches_decode() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use enough::Unstoppable;
use imgref::ImgRefMut;
let (w, h) = (64u32, 48u32);
let mut pixels = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h {
for x in 0..w {
pixels.push(((x * 255 / w) & 0xFF) as u8);
pixels.push(((y * 255 / h) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let transform = LosslessTransform::Rotate90;
let decode_result = DecodeConfig::new()
.transform(transform)
.decode(&jpeg, Unstoppable)
.unwrap();
let out_w = decode_result.width() as usize;
let out_h = decode_result.height() as usize;
let mut reader = DecodeConfig::new()
.transform(transform)
.scanline_reader(&jpeg)
.unwrap();
assert_eq!(reader.width() as usize, out_w);
assert_eq!(reader.height() as usize, out_h);
let mut scanline_pixels = vec![0u8; out_w * out_h * 3];
let mut rows_read = 0;
while rows_read < out_h {
let remaining = out_h - rows_read;
let output = ImgRefMut::new(
&mut scanline_pixels[rows_read * out_w * 3..],
out_w * 3,
remaining,
);
let count = reader.read_rows_rgb8(output).unwrap();
assert!(count > 0, "read_rows_rgb8 should make progress");
rows_read += count;
}
assert_eq!(
scanline_pixels,
decode_result.pixels_u8().unwrap(),
"scanline reader with transform should produce same pixels as decode()"
);
}
#[test]
fn test_scanline_auto_orient() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, Exif, Orientation, PixelLayout};
use enough::Unstoppable;
use imgref::ImgRefMut;
let (w, h) = (64u32, 48u32);
let mut pixels = Vec::with_capacity((w * h * 3) as usize);
for y in 0..h {
for x in 0..w {
pixels.push(((x * 255 / w) & 0xFF) as u8);
pixels.push(((y * 255 / h) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.request()
.exif(Exif::build().orientation(Orientation::Rotate90))
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let mut reader = DecodeConfig::new()
.auto_orient(true)
.scanline_reader(&jpeg)
.unwrap();
assert_eq!(reader.width(), h, "width should be original height");
assert_eq!(reader.height(), w, "height should be original width");
let out_w = reader.width() as usize;
let out_h = reader.height() as usize;
let mut scanline_pixels = vec![0u8; out_w * out_h * 3];
let mut rows_read = 0;
while rows_read < out_h {
let remaining = out_h - rows_read;
let output = ImgRefMut::new(
&mut scanline_pixels[rows_read * out_w * 3..],
out_w * 3,
remaining,
);
let count = reader.read_rows_rgb8(output).unwrap();
assert!(count > 0, "read_rows_rgb8 should make progress");
rows_read += count;
}
let decode_result = DecodeConfig::new()
.auto_orient(true)
.decode(&jpeg, Unstoppable)
.unwrap();
assert_eq!(
scanline_pixels,
decode_result.pixels_u8().unwrap(),
"scanline auto_orient should match decode auto_orient"
);
}
fn encode_test_image(
w: u32,
h: u32,
pixels: &[u8],
orientation: Option<crate::encoder::Orientation>,
) -> Vec<u8> {
use crate::encoder::{ChromaSubsampling, EncoderConfig, Exif, PixelLayout};
use enough::Unstoppable;
let config = EncoderConfig::ycbcr(97, ChromaSubsampling::None);
let req = config.request();
let req = if let Some(orient) = orientation {
req.exif(Exif::build().orientation(orient))
} else {
req
};
let mut enc = req.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb).unwrap();
enc.push_packed(pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
fn decode_test(jpeg: &[u8], config: &crate::decode::DecodeConfig) -> (u32, u32, Vec<u8>) {
use enough::Unstoppable;
let result = config.decode(jpeg, Unstoppable).unwrap();
let w = result.width();
let h = result.height();
let pixels = result.into_pixels_u8().unwrap();
(w, h, pixels)
}
fn scanline_decode_test(jpeg: &[u8], config: &crate::decode::DecodeConfig) -> (u32, u32, Vec<u8>) {
use imgref::ImgRefMut;
let mut reader = config.scanline_reader(jpeg).unwrap();
let w = reader.width() as usize;
let h = reader.height() as usize;
let mut pixels = vec![0u8; w * h * 3];
let mut rows_read = 0;
while rows_read < h {
let remaining = h - rows_read;
let output = ImgRefMut::new(&mut pixels[rows_read * w * 3..], w * 3, remaining);
let count = reader.read_rows_rgb8(output).unwrap();
assert!(count > 0);
rows_read += count;
}
(w as u32, h as u32, pixels)
}
fn block_avg(pixels: &[u8], stride_w: usize, bx: usize, by: usize) -> (u8, u8, u8) {
let mut r_sum = 0u32;
let mut g_sum = 0u32;
let mut b_sum = 0u32;
let count = 16u32; for dy in 2..6 {
for dx in 2..6 {
let px = bx * 8 + dx;
let py = by * 8 + dy;
let idx = (py * stride_w + px) * 3;
r_sum += pixels[idx] as u32;
g_sum += pixels[idx + 1] as u32;
b_sum += pixels[idx + 2] as u32;
}
}
(
(r_sum / count) as u8,
(g_sum / count) as u8,
(b_sum / count) as u8,
)
}
fn assert_block_near(
pixels: &[u8],
stride_w: usize,
bx: usize,
by: usize,
expected: (u8, u8, u8),
tolerance: u8,
label: &str,
) {
let actual = block_avg(pixels, stride_w, bx, by);
let dr = actual.0.abs_diff(expected.0);
let dg = actual.1.abs_diff(expected.1);
let db = actual.2.abs_diff(expected.2);
assert!(
dr <= tolerance && dg <= tolerance && db <= tolerance,
"{label}: block({bx},{by}) expected ~{expected:?}, got {actual:?} (delta {dr},{dg},{db})"
);
}
fn make_quadrant_image() -> (u32, u32, Vec<u8>) {
let (w, h) = (16u32, 16u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
let (r, g, b) = match (x >= 8, y >= 8) {
(false, false) => (220, 30, 30), (true, false) => (30, 220, 30), (false, true) => (30, 30, 220), (true, true) => (220, 220, 30), };
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
(w, h, pixels)
}
fn make_wide_image() -> (u32, u32, Vec<u8>) {
let (w, h) = (16u32, 8u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
let (r, g, b) = if x < 8 {
(220, 30, 30) } else {
(30, 220, 30) };
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
(w, h, pixels)
}
const RED: (u8, u8, u8) = (220, 30, 30);
const GREEN: (u8, u8, u8) = (30, 220, 30);
const BLUE: (u8, u8, u8) = (30, 30, 220);
const YELLOW: (u8, u8, u8) = (220, 220, 30);
const TOL: u8 = 12;
#[test]
fn test_all_transforms_pixel_positions() {
use crate::decode::DecodeConfig;
let (w, h, pixels) = make_quadrant_image();
let jpeg = encode_test_image(w, h, &pixels, None);
let cases: &[(
LosslessTransform,
u32,
u32,
(u8, u8, u8),
(u8, u8, u8),
(u8, u8, u8),
(u8, u8, u8),
)] = &[
(LosslessTransform::None, 16, 16, RED, GREEN, BLUE, YELLOW),
(
LosslessTransform::FlipHorizontal,
16,
16,
GREEN,
RED,
YELLOW,
BLUE,
),
(
LosslessTransform::FlipVertical,
16,
16,
BLUE,
YELLOW,
RED,
GREEN,
),
(
LosslessTransform::Rotate180,
16,
16,
YELLOW,
BLUE,
GREEN,
RED,
),
(
LosslessTransform::Transpose,
16,
16,
RED,
BLUE,
GREEN,
YELLOW,
),
(
LosslessTransform::Rotate90,
16,
16,
BLUE,
RED,
YELLOW,
GREEN,
),
(
LosslessTransform::Rotate270,
16,
16,
GREEN,
YELLOW,
RED,
BLUE,
),
(
LosslessTransform::Transverse,
16,
16,
YELLOW,
GREEN,
BLUE,
RED,
),
];
for &(transform, exp_w, exp_h, tl, tr, bl, br) in cases {
let config = DecodeConfig::new().transform(transform);
let (out_w, out_h, out_pixels) = decode_test(&jpeg, &config);
assert_eq!(out_w, exp_w, "{transform:?}: wrong width");
assert_eq!(out_h, exp_h, "{transform:?}: wrong height");
let label = format!("{transform:?}");
assert_block_near(&out_pixels, out_w as usize, 0, 0, tl, TOL, &label);
assert_block_near(&out_pixels, out_w as usize, 1, 0, tr, TOL, &label);
assert_block_near(&out_pixels, out_w as usize, 0, 1, bl, TOL, &label);
assert_block_near(&out_pixels, out_w as usize, 1, 1, br, TOL, &label);
}
}
#[test]
fn test_dimension_swap_pixel_positions() {
use crate::decode::DecodeConfig;
let (w, h, pixels) = make_wide_image();
let jpeg = encode_test_image(w, h, &pixels, None);
let config = DecodeConfig::new().transform(LosslessTransform::Transpose);
let (out_w, out_h, out_pixels) = decode_test(&jpeg, &config);
assert_eq!((out_w, out_h), (8, 16), "Transpose should swap 16x8 → 8x16");
assert_block_near(&out_pixels, out_w as usize, 0, 0, RED, TOL, "Transpose-TL");
assert_block_near(
&out_pixels,
out_w as usize,
0,
1,
GREEN,
TOL,
"Transpose-BL",
);
let config = DecodeConfig::new().transform(LosslessTransform::Rotate90);
let (out_w, out_h, _) = decode_test(&jpeg, &config);
assert_eq!((out_w, out_h), (8, 16), "Rotate90 should swap 16x8 → 8x16");
let config = DecodeConfig::new().transform(LosslessTransform::Rotate270);
let (out_w, out_h, _) = decode_test(&jpeg, &config);
assert_eq!((out_w, out_h), (8, 16), "Rotate270 should swap 16x8 → 8x16");
}
#[test]
fn test_auto_orient_all_orientations() {
use crate::decode::DecodeConfig;
use crate::encoder::Orientation;
let (w, h, pixels) = make_quadrant_image();
let cases: &[(
Orientation,
(u8, u8, u8),
(u8, u8, u8),
(u8, u8, u8),
(u8, u8, u8),
)] = &[
(Orientation::Normal, RED, GREEN, BLUE, YELLOW),
(Orientation::FlipHorizontal, GREEN, RED, YELLOW, BLUE),
(Orientation::Rotate180, YELLOW, BLUE, GREEN, RED),
(Orientation::FlipVertical, BLUE, YELLOW, RED, GREEN),
(Orientation::Transpose, RED, BLUE, GREEN, YELLOW),
(Orientation::Rotate90, BLUE, RED, YELLOW, GREEN),
(Orientation::Transverse, YELLOW, GREEN, BLUE, RED),
(Orientation::Rotate270, GREEN, YELLOW, RED, BLUE),
];
for &(orient, tl, tr, bl, br) in cases {
let jpeg = encode_test_image(w, h, &pixels, Some(orient));
let config = DecodeConfig::new().auto_orient(true);
let (out_w, out_h, out_pixels) = decode_test(&jpeg, &config);
let label = format!("auto_orient({orient:?})");
assert_eq!(out_w, 16, "{label}: wrong width");
assert_eq!(out_h, 16, "{label}: wrong height");
assert_block_near(&out_pixels, out_w as usize, 0, 0, tl, TOL, &label);
assert_block_near(&out_pixels, out_w as usize, 1, 0, tr, TOL, &label);
assert_block_near(&out_pixels, out_w as usize, 0, 1, bl, TOL, &label);
assert_block_near(&out_pixels, out_w as usize, 1, 1, br, TOL, &label);
}
}
#[test]
fn test_scanline_matches_decode_all_transforms() {
use crate::decode::DecodeConfig;
let (w, h, pixels) = make_quadrant_image();
let jpeg = encode_test_image(w, h, &pixels, None);
for &transform in &LosslessTransform::ALL {
let config = DecodeConfig::new().transform(transform);
let (dw, dh, decode_pixels) = decode_test(&jpeg, &config);
let (sw, sh, scanline_pixels) = scanline_decode_test(&jpeg, &config);
assert_eq!((dw, dh), (sw, sh), "{transform:?}: dimension mismatch");
assert_eq!(
decode_pixels, scanline_pixels,
"{transform:?}: scanline pixels differ from buffered decode"
);
}
}
#[test]
fn test_scanline_matches_decode_all_orientations() {
use crate::decode::DecodeConfig;
use crate::encoder::Orientation;
let (w, h, pixels) = make_quadrant_image();
let orientations = [
Orientation::Normal,
Orientation::FlipHorizontal,
Orientation::Rotate180,
Orientation::FlipVertical,
Orientation::Transpose,
Orientation::Rotate90,
Orientation::Transverse,
Orientation::Rotate270,
];
for orient in orientations {
let jpeg = encode_test_image(w, h, &pixels, Some(orient));
let config = DecodeConfig::new().auto_orient(true);
let (dw, dh, decode_pixels) = decode_test(&jpeg, &config);
let (sw, sh, scanline_pixels) = scanline_decode_test(&jpeg, &config);
assert_eq!(
(dw, dh),
(sw, sh),
"auto_orient({orient:?}): dimension mismatch"
);
assert_eq!(
decode_pixels, scanline_pixels,
"auto_orient({orient:?}): scanline pixels differ from buffered decode"
);
}
}
#[test]
fn test_transform_420_subsampling() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use enough::Unstoppable;
let (w, h) = (32u32, 32u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
let (r, g, b) = match (x >= 16, y >= 16) {
(false, false) => (220, 30, 30), (true, false) => (30, 220, 30), (false, true) => (30, 30, 220), (true, true) => (220, 220, 30), };
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
let config = EncoderConfig::ycbcr(97, ChromaSubsampling::Quarter);
let mut enc = config
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let tol_420: u8 = 25;
for &transform in &LosslessTransform::ALL {
let config = DecodeConfig::new().transform(transform);
let (dw, dh, decode_pixels) = decode_test(&jpeg, &config);
let (sw, sh, _) = scanline_decode_test(&jpeg, &config);
assert_eq!(
(dw, dh),
(sw, sh),
"{transform:?} 4:2:0: dimension mismatch"
);
assert_eq!(
(dw, dh),
(32, 32),
"{transform:?} 4:2:0: square stays square"
);
if transform == LosslessTransform::None {
assert_block_near(
&decode_pixels,
dw as usize,
0,
0,
RED,
tol_420,
"420-None-TL",
);
assert_block_near(
&decode_pixels,
dw as usize,
2,
0,
GREEN,
tol_420,
"420-None-TR",
);
assert_block_near(
&decode_pixels,
dw as usize,
0,
2,
BLUE,
tol_420,
"420-None-BL",
);
assert_block_near(
&decode_pixels,
dw as usize,
2,
2,
YELLOW,
tol_420,
"420-None-BR",
);
}
}
}
#[test]
fn test_transform_grayscale() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use enough::Unstoppable;
let (w, h) = (16u32, 8u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
let v = if x < 8 { 40u8 } else { 210u8 };
pixels[idx] = v;
pixels[idx + 1] = v;
pixels[idx + 2] = v;
}
}
let config = EncoderConfig::ycbcr(97, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
let config = DecodeConfig::new().transform(LosslessTransform::Rotate90);
let (out_w, out_h, _) = decode_test(&jpeg, &config);
assert_eq!((out_w, out_h), (8, 16), "Rotate90 on 16x8 should give 8x16");
let config = DecodeConfig::new().transform(LosslessTransform::FlipHorizontal);
let (out_w, out_h, out_pixels) = decode_test(&jpeg, &config);
assert_eq!((out_w, out_h), (16, 8), "FlipH preserves dimensions");
let left_avg = block_avg(&out_pixels, out_w as usize, 0, 0);
let right_avg = block_avg(&out_pixels, out_w as usize, 1, 0);
assert!(
left_avg.0 > 180 && right_avg.0 < 80,
"FlipH should swap dark/bright: left={left_avg:?}, right={right_avg:?}"
);
for &transform in &LosslessTransform::ALL {
let config = DecodeConfig::new().transform(transform);
let (dw, dh, decode_pixels) = decode_test(&jpeg, &config);
let (sw, sh, scanline_pixels) = scanline_decode_test(&jpeg, &config);
assert_eq!(
(dw, dh),
(sw, sh),
"{transform:?} grayscale: dimension mismatch"
);
assert_eq!(
decode_pixels, scanline_pixels,
"{transform:?} grayscale: scanline differs from decode"
);
}
}
#[test]
fn test_no_transform_unchanged() {
use crate::decode::DecodeConfig;
let (w, h, pixels) = make_quadrant_image();
let jpeg = encode_test_image(w, h, &pixels, None);
let baseline = DecodeConfig::new();
let (bw, bh, baseline_pixels) = decode_test(&jpeg, &baseline);
let config = DecodeConfig::new().transform(LosslessTransform::None);
let (tw, th, transform_pixels) = decode_test(&jpeg, &config);
assert_eq!((bw, bh), (tw, th));
assert_eq!(
baseline_pixels, transform_pixels,
"None transform should be identical to no transform"
);
let config = DecodeConfig::new().auto_orient(true);
let (aw, ah, orient_pixels) = decode_test(&jpeg, &config);
assert_eq!((bw, bh), (aw, ah));
assert_eq!(
baseline_pixels, orient_pixels,
"auto_orient with no EXIF should be identical"
);
}
#[test]
fn test_composed_orientation_and_transform() {
use crate::decode::DecodeConfig;
use crate::encoder::Orientation;
let (w, h, pixels) = make_quadrant_image();
let jpeg = encode_test_image(w, h, &pixels, Some(Orientation::Rotate90));
let config = DecodeConfig::new().auto_orient(true);
let (_, _, orient_only) = decode_test(&jpeg, &config);
let config = DecodeConfig::new()
.auto_orient(true)
.transform(LosslessTransform::Rotate270);
let (cw, ch, composed_pixels) = decode_test(&jpeg, &config);
assert_eq!((cw, ch), (16, 16));
let baseline = DecodeConfig::new().auto_orient(false);
let (_, _, baseline_pixels) = decode_test(&jpeg, &baseline);
assert_eq!(
composed_pixels, baseline_pixels,
"Rotate90 + Rotate270 should be identity"
);
assert_ne!(
orient_only, baseline_pixels,
"auto_orient(Rotate90) should change the pixels"
);
}
#[test]
fn test_non_mcu_aligned_all_transforms() {
use crate::decode::DecodeConfig;
let (w, h) = (12u32, 20u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
let (r, g, b) = match (x >= 6, y >= 10) {
(false, false) => (220, 30, 30), (true, false) => (30, 220, 30), (false, true) => (30, 30, 220), (true, true) => (220, 220, 30), };
pixels[idx] = r;
pixels[idx + 1] = g;
pixels[idx + 2] = b;
}
}
let jpeg = encode_test_image(w, h, &pixels, None);
for &transform in &LosslessTransform::ALL {
let config = DecodeConfig::new().transform(transform);
let (out_w, out_h, out_pixels) = decode_test(&jpeg, &config);
let label = format!("{transform:?}");
if transform.swaps_dimensions() {
assert_eq!(
(out_w, out_h),
(20, 12),
"{label}: non-aligned swapped dimensions"
);
} else {
assert_eq!(
(out_w, out_h),
(12, 20),
"{label}: non-aligned preserved dimensions"
);
}
let check_pixel = |px: usize, py: usize, label: &str| -> (u8, u8, u8) {
let idx = (py * out_w as usize + px) * 3;
assert!(
idx + 2 < out_pixels.len(),
"{label}: pixel ({px},{py}) out of bounds (image {out_w}x{out_h})"
);
(out_pixels[idx], out_pixels[idx + 1], out_pixels[idx + 2])
};
let tl = check_pixel(2, 2, &label);
assert!(
tl.0 > 10 || tl.1 > 10 || tl.2 > 10,
"{label}: top-left pixel is black (likely padding): {tl:?}"
);
let br = check_pixel(out_w as usize - 3, out_h as usize - 3, &label);
assert!(
br.0 > 10 || br.1 > 10 || br.2 > 10,
"{label}: bottom-right pixel is black (likely padding): {br:?}"
);
let (sw, sh, scanline_pixels) = scanline_decode_test(&jpeg, &config);
assert_eq!(
(out_w, out_h),
(sw, sh),
"{label}: scanline dimension mismatch"
);
assert_eq!(
out_pixels, scanline_pixels,
"{label}: scanline pixels differ from decode"
);
}
}
#[test]
fn test_non_mcu_aligned_auto_orient() {
use crate::decode::DecodeConfig;
use crate::encoder::Orientation;
let (w, h) = (12u32, 20u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
let v = if x < 6 { 200u8 } else { 50u8 };
pixels[idx] = v;
pixels[idx + 1] = v;
pixels[idx + 2] = v;
}
}
let jpeg = encode_test_image(w, h, &pixels, Some(Orientation::Rotate90));
let config = DecodeConfig::new().auto_orient(true);
let (out_w, out_h, out_pixels) = decode_test(&jpeg, &config);
assert_eq!(
(out_w, out_h),
(20, 12),
"auto_orient Rotate90 on 12x20 should give 20x12"
);
let corners = [
(1, 1, "TL"),
(out_w as usize - 2, 1, "TR"),
(1, out_h as usize - 2, "BL"),
(out_w as usize - 2, out_h as usize - 2, "BR"),
];
for (px, py, name) in corners {
let idx = (py * out_w as usize + px) * 3;
let pixel = (out_pixels[idx], out_pixels[idx + 1], out_pixels[idx + 2]);
assert!(
pixel.0 > 10 || pixel.1 > 10 || pixel.2 > 10,
"auto_orient non-aligned {name} pixel is black (padding?): {pixel:?}"
);
}
let (sw, sh, scanline_pixels) = scanline_decode_test(&jpeg, &config);
assert_eq!((out_w, out_h), (sw, sh));
assert_eq!(
out_pixels, scanline_pixels,
"non-aligned auto_orient: scanline differs"
);
}
#[test]
fn test_non_mcu_aligned_420_transform() {
use crate::decode::DecodeConfig;
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
use enough::Unstoppable;
let (w, h) = (12u32, 20u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
let v = if y < 10 { 200u8 } else { 50u8 };
pixels[idx] = v;
pixels[idx + 1] = v / 2;
pixels[idx + 2] = 100;
}
}
let config = EncoderConfig::ycbcr(97, ChromaSubsampling::Quarter);
let mut enc = config
.encode_from_bytes(w, h, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
let jpeg = enc.finish().unwrap();
for &transform in &LosslessTransform::ALL {
let config = DecodeConfig::new().transform(transform);
let (out_w, out_h, out_pixels) = decode_test(&jpeg, &config);
let label = format!("{transform:?} 4:2:0 non-aligned");
if transform.swaps_dimensions() {
assert_eq!((out_w, out_h), (20, 12), "{label}: wrong dimensions");
} else {
assert_eq!((out_w, out_h), (12, 20), "{label}: wrong dimensions");
}
let corners = [
(1, 1),
(out_w as usize - 2, 1),
(1, out_h as usize - 2),
(out_w as usize - 2, out_h as usize - 2),
];
for (px, py) in corners {
let idx = (py * out_w as usize + px) * 3;
let pixel = (out_pixels[idx], out_pixels[idx + 1], out_pixels[idx + 2]);
assert!(
pixel.0 > 10 || pixel.1 > 10 || pixel.2 > 10,
"{label}: corner ({px},{py}) is black (padding?): {pixel:?}"
);
}
}
}
fn pixel_transform(
pixels: &[u8],
w: usize,
h: usize,
transform: LosslessTransform,
) -> (usize, usize, Vec<u8>) {
let (out_w, out_h) = if transform.swaps_dimensions() {
(h, w)
} else {
(w, h)
};
let mut out = vec![0u8; out_w * out_h * 3];
for sy in 0..h {
for sx in 0..w {
let (dx, dy) = match transform {
LosslessTransform::None => (sx, sy),
LosslessTransform::FlipHorizontal => (w - 1 - sx, sy),
LosslessTransform::FlipVertical => (sx, h - 1 - sy),
LosslessTransform::Rotate180 => (w - 1 - sx, h - 1 - sy),
LosslessTransform::Transpose => (sy, sx),
LosslessTransform::Rotate90 => (h - 1 - sy, sx),
LosslessTransform::Rotate270 => (sy, w - 1 - sx),
LosslessTransform::Transverse => (h - 1 - sy, w - 1 - sx),
};
let si = (sy * w + sx) * 3;
let di = (dy * out_w + dx) * 3;
out[di..di + 3].copy_from_slice(&pixels[si..si + 3]);
}
}
(out_w, out_h, out)
}
#[test]
fn test_15x17_border_pixels_lossless() {
use crate::decode::DecodeConfig;
let (w, h) = (15u32, 17u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
pixels[idx] = (x * 17 % 256) as u8;
pixels[idx + 1] = (y * 15 % 256) as u8;
pixels[idx + 2] = ((x * 7 + y * 11) % 256) as u8;
}
}
let jpeg = encode_test_image(w, h, &pixels, None);
let config_none = DecodeConfig::new();
let (ref_w, ref_h, ref_pixels_i16) = decode_test(&jpeg, &config_none);
assert_eq!((ref_w, ref_h), (w, h));
let mut config_f32 = DecodeConfig::new();
config_f32.force_f32_idct = true;
let (_, _, ref_pixels_f32) = decode_test(&jpeg, &config_f32);
let border_positions: Vec<(usize, usize)> = {
let mut positions = Vec::new();
for x in 0..w as usize {
positions.push((x, 0)); positions.push((x, h as usize - 1)); }
for y in 1..h as usize - 1 {
positions.push((0, y)); positions.push((w as usize - 1, y)); }
positions
};
for &transform in &LosslessTransform::ALL {
let label = format!("{transform:?}");
let config = DecodeConfig::new().transform(transform);
let (dct_w, dct_h, dct_pixels) = decode_test(&jpeg, &config);
let ref_pixels = if transform.swaps_dimensions() {
&ref_pixels_f32
} else {
&ref_pixels_i16
};
let (px_w, px_h, px_pixels) =
pixel_transform(ref_pixels, ref_w as usize, ref_h as usize, transform);
assert_eq!(
(dct_w, dct_h),
(px_w as u32, px_h as u32),
"{label}: dimension mismatch"
);
let ow = ref_w as usize;
let oh = ref_h as usize;
let tw = dct_w as usize;
let mut max_diff = 0u8;
let mut worst_pos = (0, 0);
let mut mismatches = 0;
for &(sx, sy) in &border_positions {
let (dx, dy) = match transform {
LosslessTransform::None => (sx, sy),
LosslessTransform::FlipHorizontal => (ow - 1 - sx, sy),
LosslessTransform::FlipVertical => (sx, oh - 1 - sy),
LosslessTransform::Rotate180 => (ow - 1 - sx, oh - 1 - sy),
LosslessTransform::Transpose => (sy, sx),
LosslessTransform::Rotate90 => (oh - 1 - sy, sx),
LosslessTransform::Rotate270 => (sy, ow - 1 - sx),
LosslessTransform::Transverse => (oh - 1 - sy, ow - 1 - sx),
};
let idx = (dy * tw + dx) * 3;
for c in 0..3 {
let diff =
(dct_pixels[idx + c] as i16 - px_pixels[idx + c] as i16).unsigned_abs() as u8;
if diff > max_diff {
max_diff = diff;
worst_pos = (dx, dy);
}
if diff > 0 {
mismatches += 1;
}
}
}
assert_eq!(
max_diff,
0,
"{label}: border pixel mismatch! max_diff={max_diff} at ({},{}) \
mismatches={mismatches}/{} channels",
worst_pos.0,
worst_pos.1,
border_positions.len() * 3
);
}
}
#[test]
fn test_15x17_border_pixels_scanline() {
use crate::decode::DecodeConfig;
let (w, h) = (15u32, 17u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
pixels[idx] = (x * 17 % 256) as u8;
pixels[idx + 1] = (y * 15 % 256) as u8;
pixels[idx + 2] = ((x * 7 + y * 11) % 256) as u8;
}
}
let jpeg = encode_test_image(w, h, &pixels, None);
for &transform in &LosslessTransform::ALL {
let label = format!("{transform:?}");
let config = DecodeConfig::new().transform(transform);
let (buf_w, buf_h, buf_pixels) = decode_test(&jpeg, &config);
let (scan_w, scan_h, scan_pixels) = scanline_decode_test(&jpeg, &config);
assert_eq!(
(buf_w, buf_h),
(scan_w, scan_h),
"{label}: scanline dimensions mismatch"
);
let mut max_diff = 0u8;
let mut worst_pos = (0, 0);
for y in 0..buf_h as usize {
for x in 0..buf_w as usize {
let idx = (y * buf_w as usize + x) * 3;
for c in 0..3 {
let diff = (buf_pixels[idx + c] as i16 - scan_pixels[idx + c] as i16)
.unsigned_abs() as u8;
if diff > max_diff {
max_diff = diff;
worst_pos = (x, y);
}
}
}
}
assert_eq!(
max_diff, 0,
"{label}: scanline vs buffered mismatch! max_diff={max_diff} at ({},{})",
worst_pos.0, worst_pos.1
);
}
}
#[test]
fn test_15x17_all_pixels_lossless() {
let _lock = archmage::testing::lock_token_testing();
use crate::decode::DecodeConfig;
let (w, h) = (15u32, 17u32);
let mut pixels = vec![0u8; (w * h * 3) as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * 3) as usize;
pixels[idx] = ((x * 37 + y * 53) % 200 + 30) as u8;
pixels[idx + 1] = ((x * 61 + y * 23) % 180 + 40) as u8;
pixels[idx + 2] = ((x * 43 + y * 71) % 160 + 50) as u8;
}
}
let jpeg = encode_test_image(w, h, &pixels, None);
let config_none = DecodeConfig::new();
let (ref_w, ref_h, ref_pixels_i16) = decode_test(&jpeg, &config_none);
assert_eq!((ref_w, ref_h), (w, h));
let mut config_f32 = DecodeConfig::new();
config_f32.force_f32_idct = true;
let (_, _, ref_pixels_f32) = decode_test(&jpeg, &config_f32);
for &transform in &LosslessTransform::ALL {
if transform == LosslessTransform::None {
continue;
}
let label = format!("{transform:?}");
let config = DecodeConfig::new().transform(transform);
let (dct_w, dct_h, dct_pixels) = decode_test(&jpeg, &config);
let ref_pixels = if transform.swaps_dimensions() {
&ref_pixels_f32
} else {
&ref_pixels_i16
};
let (px_w, px_h, px_pixels) =
pixel_transform(ref_pixels, ref_w as usize, ref_h as usize, transform);
assert_eq!(
(dct_w as usize, dct_h as usize),
(px_w, px_h),
"{label}: dimension mismatch"
);
let tw = dct_w as usize;
let th = dct_h as usize;
let mut max_diff = 0u8;
let mut worst_pos = (0, 0);
let mut total_diff = 0u64;
let mut mismatches = 0usize;
for y in 0..th {
for x in 0..tw {
let idx = (y * tw + x) * 3;
for c in 0..3 {
let diff = (dct_pixels[idx + c] as i16 - px_pixels[idx + c] as i16)
.unsigned_abs() as u8;
if diff > max_diff {
max_diff = diff;
worst_pos = (x, y);
}
if diff > 0 {
mismatches += 1;
total_diff += diff as u64;
}
}
}
}
assert_eq!(
max_diff,
0,
"{label}: DCT vs pixel transform mismatch! \
max_diff={max_diff} at ({},{}) mismatches={mismatches}/{} mean_diff={:.2}",
worst_pos.0,
worst_pos.1,
tw * th * 3,
if mismatches > 0 {
total_diff as f64 / mismatches as f64
} else {
0.0
}
);
}
}
mod restructure_tests {
use crate::decode::DecodeConfig;
use crate::lossless::{
EdgeHandling, LosslessTransform, OutputMode, RestartInterval, RestructureConfig,
TransformConfig, restructure,
};
use enough::Unstoppable;
fn create_test_jpeg(width: u32, height: u32) -> Vec<u8> {
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
let mut pixels = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
pixels.push(((x * 255 / width) & 0xFF) as u8);
pixels.push(((y * 255 / height) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::None);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
fn create_test_jpeg_420(width: u32, height: u32) -> Vec<u8> {
use crate::encoder::{ChromaSubsampling, EncoderConfig, PixelLayout};
let mut pixels = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
pixels.push(((x * 255 / width) & 0xFF) as u8);
pixels.push(((y * 255 / height) & 0xFF) as u8);
pixels.push(128u8);
}
}
let config = EncoderConfig::ycbcr(90, ChromaSubsampling::Quarter);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
fn create_test_jpeg_gray(width: u32, height: u32) -> Vec<u8> {
use crate::encoder::{EncoderConfig, PixelLayout};
let mut pixels = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
pixels.push(((x * 255 / width + y * 64 / height) & 0xFF) as u8);
}
}
let config = EncoderConfig::grayscale(90);
let mut enc = config
.encode_from_bytes(width, height, PixelLayout::Gray8Srgb)
.unwrap();
enc.push_packed(&pixels, Unstoppable).unwrap();
enc.finish().unwrap()
}
fn compare_coefficients(a: &[u8], b: &[u8]) -> (usize, i16) {
let decoder = DecodeConfig::new();
let ca = decoder.decode_coefficients(a, Unstoppable).unwrap();
let cb = decoder.decode_coefficients(b, Unstoppable).unwrap();
assert_eq!(
ca.components.len(),
cb.components.len(),
"component count mismatch"
);
let mut total_diffs = 0;
let mut max_diff = 0i16;
for (c1, c2) in ca.components.iter().zip(&cb.components) {
assert_eq!(c1.blocks_wide, c2.blocks_wide, "blocks_wide mismatch");
assert_eq!(c1.blocks_high, c2.blocks_high, "blocks_high mismatch");
let num_blocks = c1.num_blocks().min(c2.num_blocks());
for block_idx in 0..num_blocks {
let b1 = c1.block(block_idx);
let b2 = c2.block(block_idx);
for i in 0..64 {
let d = (b1[i] as i32 - b2[i] as i32).abs() as i16;
if d != 0 {
total_diffs += 1;
max_diff = max_diff.max(d);
}
}
}
}
(total_diffs, max_diff)
}
fn is_progressive_jpeg(data: &[u8]) -> bool {
for i in 0..data.len().saturating_sub(1) {
if data[i] == 0xFF && data[i + 1] == 0xC2 {
return true;
}
}
false
}
fn has_dri_marker(data: &[u8]) -> bool {
for i in 0..data.len().saturating_sub(1) {
if data[i] == 0xFF && data[i + 1] == 0xDD {
return true;
}
}
false
}
#[test]
fn test_restructure_sequential_identity() {
let jpeg = create_test_jpeg(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Sequential,
restart_interval: RestartInterval::None,
transform: None,
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(!is_progressive_jpeg(&result));
assert!(!has_dri_marker(&result));
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"identity restructure should preserve all coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_sequential_with_restart_mcus() {
let jpeg = create_test_jpeg(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Sequential,
restart_interval: RestartInterval::EveryMcus(10),
transform: None,
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(!is_progressive_jpeg(&result));
assert!(has_dri_marker(&result), "output should contain DRI marker");
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"restart markers should not affect coefficients (max_diff={max_diff})"
);
let decoder = DecodeConfig::new();
let decoded = decoder.decode(&result, Unstoppable).unwrap();
assert_eq!(decoded.width(), 64);
assert_eq!(decoded.height(), 64);
}
#[test]
fn test_restructure_sequential_with_restart_mcu_rows() {
let jpeg = create_test_jpeg(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Sequential,
restart_interval: RestartInterval::EveryMcuRows(1),
transform: None,
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(has_dri_marker(&result));
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"MCU row restart should preserve coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_progressive_roundtrip() {
let jpeg = create_test_jpeg(64, 64);
let prog_config = RestructureConfig {
output_mode: OutputMode::Progressive,
restart_interval: RestartInterval::None,
transform: None,
};
let progressive = restructure(&jpeg, &prog_config, Unstoppable).unwrap();
assert!(
is_progressive_jpeg(&progressive),
"output should be progressive"
);
let (diffs, max_diff) = compare_coefficients(&jpeg, &progressive);
assert_eq!(
diffs, 0,
"progressive roundtrip should preserve all coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_progressive_to_sequential() {
let jpeg = create_test_jpeg(64, 64);
let prog_config = RestructureConfig {
output_mode: OutputMode::Progressive,
..Default::default()
};
let progressive = restructure(&jpeg, &prog_config, Unstoppable).unwrap();
assert!(is_progressive_jpeg(&progressive));
let seq_config = RestructureConfig {
output_mode: OutputMode::Sequential,
..Default::default()
};
let sequential = restructure(&progressive, &seq_config, Unstoppable).unwrap();
assert!(!is_progressive_jpeg(&sequential));
let (diffs, max_diff) = compare_coefficients(&jpeg, &sequential);
assert_eq!(
diffs, 0,
"progressive->sequential roundtrip should preserve coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_progressive_ignores_restart() {
let jpeg = create_test_jpeg(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Progressive,
restart_interval: RestartInterval::EveryMcus(8),
transform: None,
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(is_progressive_jpeg(&result));
assert!(!has_dri_marker(&result));
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"progressive should preserve coefficients (max_diff={max_diff})"
);
let decoder = DecodeConfig::new();
let decoded = decoder.decode(&result, Unstoppable).unwrap();
assert_eq!(decoded.width(), 64);
}
#[test]
fn test_restructure_with_rotation() {
let jpeg = create_test_jpeg(64, 48);
let config = RestructureConfig {
output_mode: OutputMode::Progressive,
restart_interval: RestartInterval::None,
transform: Some(TransformConfig {
transform: LosslessTransform::Rotate90,
edge_handling: EdgeHandling::TrimPartialBlocks,
}),
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(is_progressive_jpeg(&result));
let decoder = DecodeConfig::new();
let decoded = decoder.decode(&result, Unstoppable).unwrap();
assert_eq!(decoded.width(), 48);
assert_eq!(decoded.height(), 64);
}
#[test]
fn test_restructure_preserves_metadata() {
let jpeg = create_test_jpeg(64, 64);
let decoder = DecodeConfig::new().preserve(crate::decode::PreserveConfig::all());
let (_, orig_extras) = decoder
.decode_coefficients_with_extras(&jpeg, Unstoppable)
.unwrap();
let config = RestructureConfig {
output_mode: OutputMode::Progressive,
..Default::default()
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
let (_, result_extras) = decoder
.decode_coefficients_with_extras(&result, Unstoppable)
.unwrap();
let orig_segments = orig_extras
.as_ref()
.map(|e| e.segments().len())
.unwrap_or(0);
let result_segments = result_extras
.as_ref()
.map(|e| e.segments().len())
.unwrap_or(0);
assert_eq!(
orig_segments, result_segments,
"metadata segment count should be preserved"
);
}
#[test]
fn test_restructure_420_sequential() {
let jpeg = create_test_jpeg_420(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Sequential,
restart_interval: RestartInterval::EveryMcus(4),
..Default::default()
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"4:2:0 sequential restructure should preserve coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_420_progressive() {
let jpeg = create_test_jpeg_420(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Progressive,
restart_interval: RestartInterval::None,
..Default::default()
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(is_progressive_jpeg(&result));
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"4:2:0 progressive restructure should preserve coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_grayscale_sequential() {
let jpeg = create_test_jpeg_gray(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Sequential,
restart_interval: RestartInterval::EveryMcus(8),
..Default::default()
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"grayscale sequential restructure should preserve coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_grayscale_progressive() {
let jpeg = create_test_jpeg_gray(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Progressive,
..Default::default()
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(is_progressive_jpeg(&result));
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"grayscale progressive restructure should preserve coefficients (max_diff={max_diff})"
);
}
#[test]
fn test_restructure_mcu_row_restart_420() {
let jpeg = create_test_jpeg_420(64, 64);
let config = RestructureConfig {
output_mode: OutputMode::Sequential,
restart_interval: RestartInterval::EveryMcuRows(1),
..Default::default()
};
let result = restructure(&jpeg, &config, Unstoppable).unwrap();
assert!(has_dri_marker(&result));
let (diffs, max_diff) = compare_coefficients(&jpeg, &result);
assert_eq!(
diffs, 0,
"4:2:0 MCU row restart should preserve coefficients (max_diff={max_diff})"
);
}
}