use super::*;
fn export_jpeg_baseline_icc_tiff_for_test(
work_dir: &std::path::Path,
jpeg: Vec<u8>,
icc_profile_policy: IccProfilePolicy,
) -> Result<ExportReport, Error> {
let source = work_dir.join("source.svs");
write_tiled_jpeg_tiff(&source, 8, 8, 8, 8, std::slice::from_ref(&jpeg));
export_dicom(ExportRequest {
source_path: source,
output_dir: work_dir.join("out"),
options: ExportOptions {
tile_size: 8,
transfer_syntax: TransferSyntax::JpegBaseline8Bit,
icc_profile_policy,
..ExportOptions::default()
},
metadata: MetadataSource::ResearchPlaceholder,
level_filter: None,
})
}
#[test]
fn export_dicom_defaults_missing_icc_to_assumed_srgb() {
let tmp = tempfile::tempdir().unwrap();
let jpeg = encode_test_jpeg(8, 8, [160, 20, 40]);
let report =
export_jpeg_baseline_icc_tiff_for_test(tmp.path(), jpeg, IccProfilePolicy::FallbackSrgb)
.unwrap();
assert_eq!(
report.instances[0].icc_profile_source,
IccProfileSource::SynthesizedSrgb
);
assert_generated_icc_profile_eq(
dicom_instance_icc_profile(&report.instances[0].path),
synthetic_srgb_icc_profile_for_test(),
);
}
#[test]
fn export_dicom_can_assume_display_p3_when_icc_is_missing() {
let tmp = tempfile::tempdir().unwrap();
let jpeg = encode_test_jpeg(8, 8, [160, 20, 40]);
let report = export_jpeg_baseline_icc_tiff_for_test(
tmp.path(),
jpeg,
IccProfilePolicy::FallbackDisplayP3,
)
.unwrap();
assert_eq!(
report.instances[0].icc_profile_source,
IccProfileSource::SynthesizedDisplayP3
);
assert_generated_icc_profile_eq(
dicom_instance_icc_profile(&report.instances[0].path),
synthetic_display_p3_icc_profile_for_test(),
);
}
#[test]
fn export_dicom_strict_icc_fails_when_source_has_no_icc() {
let tmp = tempfile::tempdir().unwrap();
let jpeg = encode_test_jpeg(8, 8, [160, 20, 40]);
let err = export_jpeg_baseline_icc_tiff_for_test(tmp.path(), jpeg, IccProfilePolicy::Strict)
.unwrap_err();
assert!(err.to_string().contains("ICC"), "unexpected error: {err}");
}
#[test]
fn export_dicom_can_omit_missing_icc_when_requested() {
let tmp = tempfile::tempdir().unwrap();
let jpeg = encode_test_jpeg(8, 8, [160, 20, 40]);
let report =
export_jpeg_baseline_icc_tiff_for_test(tmp.path(), jpeg, IccProfilePolicy::OmitIfMissing)
.unwrap();
assert_eq!(
report.instances[0].icc_profile_source,
IccProfileSource::OmittedMissing
);
assert!(!dicom_instance_has_icc_profile(&report.instances[0].path));
}
#[test]
fn export_dicom_uses_embedded_jpeg_icc_when_available() {
let tmp = tempfile::tempdir().unwrap();
let icc_profile = synthetic_display_p3_icc_profile_for_test();
let jpeg = jpeg_with_icc_profile(encode_test_jpeg(8, 8, [160, 20, 40]), &icc_profile);
let report =
export_jpeg_baseline_icc_tiff_for_test(tmp.path(), jpeg, IccProfilePolicy::FallbackSrgb)
.unwrap();
assert_eq!(
report.instances[0].icc_profile_source,
IccProfileSource::SourceJpeg
);
assert_eq!(
dicom_instance_icc_profile(&report.instances[0].path),
icc_profile
);
}
fn jpeg_with_icc_profile(jpeg: Vec<u8>, icc_profile: &[u8]) -> Vec<u8> {
assert!(jpeg.starts_with(&[0xFF, 0xD8]));
let payload_len = 14 + icc_profile.len();
let segment_len = u16::try_from(payload_len + 2).expect("ICC APP2 segment fits in JPEG");
let mut out = Vec::with_capacity(jpeg.len() + payload_len + 4);
out.extend_from_slice(&jpeg[..2]);
out.extend_from_slice(&[0xFF, 0xE2]);
out.extend_from_slice(&segment_len.to_be_bytes());
out.extend_from_slice(b"ICC_PROFILE\0");
out.extend_from_slice(&[1, 1]);
out.extend_from_slice(icc_profile);
out.extend_from_slice(&jpeg[2..]);
out
}
fn dicom_instance_has_icc_profile(path: &std::path::Path) -> bool {
let object = dicom_object::open_file(path).unwrap();
let optical_path = object
.element(tags::OPTICAL_PATH_SEQUENCE)
.unwrap()
.items()
.unwrap();
optical_path[0].element(tags::ICC_PROFILE).is_ok()
}
fn dicom_instance_icc_profile(path: &std::path::Path) -> Vec<u8> {
let object = dicom_object::open_file(path).unwrap();
let optical_path = object
.element(tags::OPTICAL_PATH_SEQUENCE)
.unwrap()
.items()
.unwrap();
optical_path[0]
.element(tags::ICC_PROFILE)
.unwrap()
.to_bytes()
.unwrap()
.into_owned()
}
fn synthetic_srgb_icc_profile_for_test() -> Vec<u8> {
moxcms::ColorProfile::new_srgb().encode().unwrap()
}
fn synthetic_display_p3_icc_profile_for_test() -> Vec<u8> {
moxcms::ColorProfile::new_display_p3().encode().unwrap()
}
fn assert_generated_icc_profile_eq(mut actual: Vec<u8>, mut expected: Vec<u8>) {
normalize_icc_profile_creation_datetime(&mut actual);
normalize_icc_profile_creation_datetime(&mut expected);
assert_eq!(actual, expected);
}
fn normalize_icc_profile_creation_datetime(profile: &mut [u8]) {
const ICC_CREATION_DATETIME: std::ops::Range<usize> = 24..36;
if let Some(created_at) = profile.get_mut(ICC_CREATION_DATETIME) {
created_at.fill(0);
}
}