use rawshift_image::core::image::RgbImage;
use rawshift_image::core::metadata::ImageMetadata;
use rawshift_image::formats::export::{
BitDepth, CommonEncodeOptions, EncodeOptions, JpegEncEncodeConfig, LibwebpEncodeConfig,
MetadataEmbedOptions, WebPMode, ZunePngEncodeConfig,
};
use rawshift_image::formats::{encode_rgb_image, encode_rgb_image_to_vec};
use std::fs;
use std::path::PathBuf;
fn synthetic_image() -> RgbImage {
RgbImage::new(4, 4, vec![32768u16; 4 * 4 * 3])
}
fn temp_path(name: &str) -> PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("rawshift_test_{}", name));
path
}
fn common(embed_exif: bool, embed_icc: bool, embed_xmp: bool) -> CommonEncodeOptions {
CommonEncodeOptions {
metadata: MetadataEmbedOptions {
embed_exif,
embed_icc,
embed_xmp,
},
bit_depth: BitDepth::Sixteen,
}
}
fn jpeg_has_exif(data: &[u8]) -> bool {
for i in 0..data.len().saturating_sub(10) {
if data[i] == 0xFF
&& data[i + 1] == 0xE1
&& i + 8 < data.len()
&& &data[i + 4..i + 8] == b"Exif"
{
return true;
}
}
false
}
fn jpeg_has_icc(data: &[u8]) -> bool {
for i in 0..data.len().saturating_sub(16) {
if data[i] == 0xFF
&& data[i + 1] == 0xE2
&& i + 15 < data.len()
&& &data[i + 4..i + 15] == b"ICC_PROFILE"
{
return true;
}
}
false
}
fn webp_has_exif(data: &[u8]) -> bool {
data.windows(4).any(|w| w == b"EXIF")
}
fn webp_has_icc(data: &[u8]) -> bool {
data.windows(4).any(|w| w == b"ICCP")
}
fn webp_has_xmp(data: &[u8]) -> bool {
data.windows(4).any(|w| w == b"XMP ")
}
mod jpeg_tests {
use super::*;
fn jpeg(quality: u8, exif: bool, icc: bool) -> EncodeOptions {
EncodeOptions::JpegJpegEnc(JpegEncEncodeConfig {
quality,
common: common(exif, icc, true),
})
}
#[test]
fn test_jpeg_export_with_exif_enabled() {
let img = synthetic_image();
let path = temp_path("export_with_exif.jpg");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&jpeg(85, true, false),
)
.expect("Export JPEG");
let data = fs::read(&path).expect("Read JPEG");
assert_eq!(&data[0..2], &[0xFF, 0xD8], "Should be valid JPEG");
assert!(jpeg_has_exif(&data), "JPEG should contain EXIF metadata");
fs::remove_file(&path).ok();
}
#[test]
fn test_jpeg_export_with_icc_enabled() {
let img = synthetic_image();
let path = temp_path("export_with_icc.jpg");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&jpeg(85, false, true),
)
.expect("Export JPEG");
let data = fs::read(&path).expect("Read JPEG");
assert!(jpeg_has_icc(&data), "JPEG should contain ICC profile");
fs::remove_file(&path).ok();
}
#[test]
fn test_jpeg_export_with_both_exif_and_icc() {
let img = synthetic_image();
let path = temp_path("export_with_both.jpg");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&jpeg(90, true, true),
)
.expect("Export JPEG");
let data = fs::read(&path).expect("Read JPEG");
assert!(jpeg_has_exif(&data), "JPEG should contain EXIF");
assert!(jpeg_has_icc(&data), "JPEG should contain ICC");
fs::remove_file(&path).ok();
}
#[test]
fn test_jpeg_export_without_metadata() {
let img = synthetic_image();
let path = temp_path("export_no_meta.jpg");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&jpeg(85, false, false),
)
.expect("Export JPEG");
let data = fs::read(&path).expect("Read JPEG");
assert!(!jpeg_has_exif(&data), "JPEG should NOT contain EXIF");
assert!(!jpeg_has_icc(&data), "JPEG should NOT contain ICC");
fs::remove_file(&path).ok();
}
#[test]
fn test_jpeg_export_default_options() {
let img = synthetic_image();
let path = temp_path("export_default.jpg");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&EncodeOptions::jpeg(),
)
.expect("Export JPEG");
let data = fs::read(&path).expect("Read JPEG");
assert!(jpeg_has_exif(&data), "Default JPEG should contain EXIF");
assert!(jpeg_has_icc(&data), "Default JPEG should contain ICC");
fs::remove_file(&path).ok();
}
#[test]
fn test_jpeg_quality_affects_file_size() {
let data: Vec<u16> = (0..64 * 64 * 3)
.map(|i| ((i * 997) % 65536) as u16)
.collect();
let img = RgbImage::new(64, 64, data);
let low = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &jpeg(30, false, false))
.expect("Export low quality");
let high =
encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &jpeg(95, false, false))
.expect("Export high quality");
assert!(
high.len() > low.len(),
"High quality ({}) should be larger than low quality ({})",
high.len(),
low.len()
);
}
}
mod webp_tests {
use super::*;
fn webp(exif: bool, icc: bool, xmp: bool) -> LibwebpEncodeConfig {
LibwebpEncodeConfig {
common: common(exif, icc, xmp),
..LibwebpEncodeConfig::lossy()
}
}
#[test]
fn test_webp_export_lossy_with_exif() {
let img = synthetic_image();
let path = temp_path("export_lossy_exif.webp");
let opts = EncodeOptions::WebpLibwebp(webp(true, false, false));
encode_rgb_image(&img, &ImageMetadata::default(), &path, &opts).expect("Export WebP");
let data = fs::read(&path).expect("Read WebP");
assert_eq!(&data[0..4], b"RIFF", "Should start with RIFF");
assert_eq!(&data[8..12], b"WEBP", "Should have WEBP FourCC");
assert!(webp_has_exif(&data), "WebP should contain EXIF metadata");
fs::remove_file(&path).ok();
}
#[test]
fn test_webp_export_lossless() {
let img = synthetic_image();
let path = temp_path("export_lossless.webp");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&EncodeOptions::webp_lossless(),
)
.expect("Export lossless WebP");
let data = fs::read(&path).expect("Read WebP");
assert_eq!(&data[0..4], b"RIFF", "Should start with RIFF");
assert_eq!(&data[8..12], b"WEBP", "Should have WEBP FourCC");
fs::remove_file(&path).ok();
}
#[test]
fn test_webp_export_without_metadata() {
let img = synthetic_image();
let path = temp_path("export_no_meta.webp");
let opts = EncodeOptions::WebpLibwebp(webp(false, false, false));
encode_rgb_image(&img, &ImageMetadata::default(), &path, &opts).expect("Export WebP");
let data = fs::read(&path).expect("Read WebP");
assert!(!webp_has_exif(&data), "WebP should NOT contain EXIF");
assert!(!webp_has_icc(&data), "WebP should NOT contain ICC");
assert!(!webp_has_xmp(&data), "WebP should NOT contain XMP");
fs::remove_file(&path).ok();
}
#[test]
fn test_webp_export_with_icc() {
let img = synthetic_image();
let path = temp_path("export_icc.webp");
let opts = EncodeOptions::WebpLibwebp(webp(false, true, false));
encode_rgb_image(&img, &ImageMetadata::default(), &path, &opts).expect("Export WebP");
let data = fs::read(&path).expect("Read WebP");
assert!(webp_has_icc(&data), "WebP should contain ICC profile");
fs::remove_file(&path).ok();
}
#[test]
fn test_webp_export_with_xmp() {
let img = synthetic_image();
let path = temp_path("export_xmp.webp");
let meta = ImageMetadata {
xmp: Some(b"<x:xmpmeta>test</x:xmpmeta>".to_vec()),
..Default::default()
};
let opts = EncodeOptions::WebpLibwebp(webp(false, false, true));
encode_rgb_image(&img, &meta, &path, &opts).expect("Export WebP");
let data = fs::read(&path).expect("Read WebP");
assert!(webp_has_xmp(&data), "WebP should contain XMP metadata");
fs::remove_file(&path).ok();
}
#[test]
fn test_webp_export_default_options() {
let img = synthetic_image();
let path = temp_path("export_default.webp");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&EncodeOptions::webp_lossy(),
)
.expect("Export WebP");
let data = fs::read(&path).expect("Read WebP");
assert!(webp_has_exif(&data), "Default WebP should contain EXIF");
assert!(webp_has_icc(&data), "Default WebP should contain ICC");
fs::remove_file(&path).ok();
}
#[test]
fn test_webp_quality_affects_file_size() {
let data: Vec<u16> = (0..64 * 64 * 3)
.map(|i| ((i * 997) % 65536) as u16)
.collect();
let img = RgbImage::new(64, 64, data);
let mut low = webp(false, false, false);
low.quality = 10.0;
let mut high = webp(false, false, false);
high.quality = 95.0;
let low = encode_rgb_image_to_vec(
&img,
&ImageMetadata::default(),
&EncodeOptions::WebpLibwebp(low),
)
.expect("Export low quality");
let high = encode_rgb_image_to_vec(
&img,
&ImageMetadata::default(),
&EncodeOptions::WebpLibwebp(high),
)
.expect("Export high quality");
assert!(
high.len() > low.len(),
"High quality ({}) should be larger than low quality ({})",
high.len(),
low.len()
);
}
}
mod png_tests {
use super::*;
const PNG_SIGNATURE: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
#[test]
fn test_png_export_basic() {
let img = synthetic_image();
let path = temp_path("export_basic.png");
encode_rgb_image(
&img,
&ImageMetadata::default(),
&path,
&EncodeOptions::png(),
)
.expect("Export PNG");
let data = fs::read(&path).expect("Read PNG");
assert_eq!(&data[0..8], &PNG_SIGNATURE, "Should have PNG signature");
fs::remove_file(&path).ok();
}
#[test]
fn test_png_export_8bit() {
let img = synthetic_image();
let opts = EncodeOptions::PngZune(ZunePngEncodeConfig {
common: CommonEncodeOptions {
bit_depth: BitDepth::Eight,
..CommonEncodeOptions::default()
},
});
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &opts)
.expect("Export 8-bit PNG");
assert_eq!(&data[0..8], &PNG_SIGNATURE);
assert_eq!(data[24], 8, "PNG should be 8-bit");
}
#[test]
fn test_png_export_16bit() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &EncodeOptions::png())
.expect("Export 16-bit PNG");
assert_eq!(data[24], 16, "default PNG should be 16-bit");
}
}
#[cfg(feature = "avif-encode")]
fn avif_has_icc(data: &[u8]) -> bool {
data.windows(8)
.any(|w| &w[..4] == b"colr" && (&w[4..8] == b"rICC" || &w[4..8] == b"prof"))
}
#[cfg(feature = "jxl-encode")]
fn jxl_has_icc(data: &[u8]) -> bool {
let mut pos = 0;
while pos + 8 <= data.len() {
let sz = u32::from_be_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
if &data[pos + 4..pos + 8] == b"iccp" {
return true;
}
if sz < 8 {
break;
}
pos += sz;
}
false
}
#[cfg(feature = "jxl-encode")]
fn jxl_is_container(data: &[u8]) -> bool {
data.get(4..8) == Some(b"JXL ")
}
#[cfg(feature = "avif-encode")]
mod avif_tests {
use super::*;
use rawshift_image::formats::export::RavifEncodeConfig;
fn avif(exif: bool, icc: bool) -> EncodeOptions {
EncodeOptions::AvifRavif(RavifEncodeConfig {
common: common(exif, icc, true),
..RavifEncodeConfig::default()
})
}
#[test]
fn test_avif_export_with_icc_enabled() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &avif(false, true))
.expect("Export AVIF");
assert!(avif_has_icc(&data), "AVIF should contain ICC profile");
}
#[test]
fn test_avif_export_with_icc_disabled() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &avif(false, false))
.expect("Export AVIF");
assert!(!avif_has_icc(&data), "AVIF should NOT contain ICC profile");
}
#[test]
fn test_avif_export_with_icc_and_exif() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &avif(true, true))
.expect("Export AVIF");
assert!(
avif_has_icc(&data),
"AVIF should still contain ICC after EXIF embedding"
);
}
#[test]
fn test_avif_default_options_embed_icc() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &EncodeOptions::avif())
.expect("Export AVIF");
assert!(avif_has_icc(&data), "Default AVIF should embed ICC");
}
}
#[cfg(feature = "jxl-encode")]
mod jxl_tests {
use super::*;
use rawshift_image::formats::export::ZuneJxlEncodeConfig;
fn jxl(exif: bool, icc: bool) -> EncodeOptions {
EncodeOptions::JxlZune(ZuneJxlEncodeConfig {
common: common(exif, icc, true),
..ZuneJxlEncodeConfig::default()
})
}
#[test]
fn test_jxl_config_default_has_embed_icc() {
assert!(
ZuneJxlEncodeConfig::default().common.metadata.embed_icc,
"ZuneJxlEncodeConfig default should embed ICC"
);
}
#[test]
fn test_jxl_export_with_icc_enabled() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &jxl(false, true))
.expect("Export JXL");
assert!(jxl_is_container(&data), "JXL should be container format");
assert!(jxl_has_icc(&data), "JXL should contain ICC profile");
}
#[test]
fn test_jxl_export_with_icc_disabled() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &jxl(false, false))
.expect("Export JXL");
assert!(!jxl_has_icc(&data), "JXL should NOT contain ICC profile");
}
#[test]
fn test_jxl_default_options_embed_icc() {
let img = synthetic_image();
let data = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &EncodeOptions::jxl())
.expect("Export JXL");
assert!(jxl_has_icc(&data), "Default JXL should embed ICC");
}
}
mod encode_options_tests {
use super::*;
use rawshift_image::formats::export::OutputFormat;
#[test]
fn test_encode_options_constructors() {
let _ = EncodeOptions::png();
let _ = EncodeOptions::jpeg();
let _ = EncodeOptions::webp_lossy();
let _ = EncodeOptions::webp_lossless();
#[cfg(feature = "avif-encode")]
let _ = EncodeOptions::avif();
#[cfg(feature = "jxl-encode")]
let _ = EncodeOptions::jxl();
#[cfg(feature = "dng-encode")]
let _ = EncodeOptions::dng();
}
#[test]
fn test_jpeg_config_defaults() {
let cfg = JpegEncEncodeConfig::default();
assert_eq!(cfg.quality, 90, "JPEG default quality should be 90");
assert!(
cfg.common.metadata.embed_exif,
"JPEG embeds EXIF by default"
);
assert!(cfg.common.metadata.embed_icc, "JPEG embeds ICC by default");
}
#[test]
fn test_webp_config_defaults() {
let cfg = LibwebpEncodeConfig::default();
assert_eq!(cfg.mode, WebPMode::Lossy, "WebP default mode is Lossy");
assert!((cfg.quality - 75.0).abs() < f32::EPSILON);
assert_eq!(cfg.method, 4);
assert_eq!(cfg.near_lossless, 100);
}
#[test]
fn test_webp_named_constructors() {
assert_eq!(LibwebpEncodeConfig::lossy().mode, WebPMode::Lossy);
assert_eq!(LibwebpEncodeConfig::lossless().mode, WebPMode::Lossless);
}
#[test]
fn test_format_and_codec_id() {
assert_eq!(EncodeOptions::png().format(), OutputFormat::Png);
assert_eq!(EncodeOptions::jpeg().codec_id().id, "jpeg/jpeg-encoder");
}
}
mod in_memory_tests {
use super::*;
use rawshift_image::formats::{
StandardFormat, available_decoders, available_encoders, decode_standard_image,
probe_standard_image,
};
#[test]
fn encode_to_vec_matches_path_for_png() {
let img = synthetic_image();
let meta = ImageMetadata::default();
let path = temp_path("vec_vs_path.png");
let vec_bytes =
encode_rgb_image_to_vec(&img, &meta, &EncodeOptions::png()).expect("encode to vec");
encode_rgb_image(&img, &meta, &path, &EncodeOptions::png()).expect("encode to path");
let path_bytes = fs::read(&path).expect("read back");
assert_eq!(
vec_bytes, path_bytes,
"vec and path output must be identical"
);
fs::remove_file(&path).ok();
}
#[cfg(feature = "avif-encode")]
#[test]
fn avif_encodes_in_memory_without_a_path() {
let img = synthetic_image();
let bytes =
encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &EncodeOptions::avif())
.expect("AVIF to vec");
assert!(!bytes.is_empty());
assert_eq!(
rawshift_image::formats::detect_standard_format(&bytes),
Some(StandardFormat::Avif)
);
let mut sink = Vec::new();
rawshift_image::formats::encode_rgb_image_to_writer(
&img,
&ImageMetadata::default(),
&mut sink,
&EncodeOptions::avif(),
)
.expect("AVIF to writer");
assert_eq!(sink, bytes, "writer output must match vec output");
}
#[cfg(feature = "jxl-encode")]
#[test]
fn jxl_encodes_in_memory_without_a_path() {
let img = synthetic_image();
let bytes = encode_rgb_image_to_vec(&img, &ImageMetadata::default(), &EncodeOptions::jxl())
.expect("JXL to vec");
assert!(!bytes.is_empty());
let mut sink = Vec::new();
rawshift_image::formats::encode_rgb_image_to_writer(
&img,
&ImageMetadata::default(),
&mut sink,
&EncodeOptions::jxl(),
)
.expect("JXL to writer");
assert_eq!(sink, bytes);
}
#[test]
fn registry_lists_compiled_codecs() {
let encoders = available_encoders();
assert!(!encoders.is_empty(), "default build has encoders");
assert!(encoders.iter().all(|c| c.id.id.contains('/')));
let decoders = available_decoders();
assert!(!decoders.is_empty(), "default build has decoders");
}
#[test]
fn decoded_png_is_tagged_srgb() {
use rawshift_image::core::ColorSpace;
let bytes = encode_rgb_image_to_vec(
&synthetic_image(),
&ImageMetadata::default(),
&EncodeOptions::png(),
)
.expect("encode PNG");
let decoded = decode_standard_image(&bytes, StandardFormat::Png).expect("decode PNG");
assert_eq!(decoded.color_space(), ColorSpace::Srgb);
}
#[test]
fn probe_reports_dimensions_without_decoding() {
let bytes = encode_rgb_image_to_vec(
&synthetic_image(),
&ImageMetadata::default(),
&EncodeOptions::png(),
)
.expect("encode PNG");
let probe = probe_standard_image(&bytes).expect("probe");
assert_eq!(probe.format, StandardFormat::Png);
assert_eq!(probe.size.width, 4);
assert_eq!(probe.size.height, 4);
}
#[test]
fn entry_point_types_are_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<RgbImage>();
assert_send_sync::<ImageMetadata>();
assert_send_sync::<EncodeOptions>();
assert_send_sync::<rawshift_image::formats::DecodeOptions>();
assert_send_sync::<rawshift_image::formats::ImageProbe>();
assert_send_sync::<rawshift_image::error::RawError>();
assert_send_sync::<Vec<u8>>();
}
}