From 6b9564c60d1ec480c48a73b6b1341ade6c6f12ad Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 10:48:45 -0800 Subject: [PATCH] PDF: PDF/X-1a test covers a tagged gray JPEG The PDF/X-1a case of tagged_tiffs_keep_their_icc_profile also exports a gray JPEG with a gray profile, kept as it is, resampled and compressed with JPEG, and checks that it is written in DeviceGray without the profile. The comments on decoded_image, recode and the test say that PDF/X-1a allows no ICC-based color spaces. --- crates/pdf/src/images.rs | 6 +++--- crates/pdf/src/tests/compression.rs | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/pdf/src/images.rs b/crates/pdf/src/images.rs index eee79ee3bb..0085f07538 100644 --- a/crates/pdf/src/images.rs +++ b/crates/pdf/src/images.rs @@ -144,8 +144,8 @@ pub(crate) fn from_decoded(img: DynamicImage, icc: Option>, interpolate: } /// Image `bytes` decoded within [`limits`], with the ICC profile it carries ([`source_icc`]) when -/// `profiles` allows it (not in PDF/X-1a, which has device colours only), as [`from_decoded`] -/// writes it. `None` when it can't be decoded. +/// `profiles` allows it (not in PDF/X-1a, which allows no ICC-based color spaces), as +/// [`from_decoded`] writes it. `None` when it can't be decoded. pub(crate) fn decoded_image(bytes: &[u8], interpolate: bool, sixteen_bit: bool, profiles: bool) -> Option { from_decoded(decoded(bytes)?, profiles.then(|| source_icc(bytes)).flatten(), interpolate, sixteen_bit) } @@ -371,7 +371,7 @@ fn resample_channels(img: DynamicImage, r: &Rule, (w, h): (u32, u32)) -> Dynamic /// Encoded image `bytes` placed `size` points wide and high, as the Compression settings `c` ask: /// resampled when its resolution is above the threshold, compressed with ZIP or JPEG, its colours /// converted by `convert` (colour images). Pixels that keep their colours keep the ICC profile -/// they carry when `profiles` allows it (not in PDF/X-1a, which has device colours only). +/// they carry when `profiles` allows it (not in PDF/X-1a, which allows no ICC-based color spaces). /// `interpolate` lets viewers smooth it. An image that can't be decoded is kept as it is. pub(crate) fn recode( bytes: &[u8], diff --git a/crates/pdf/src/tests/compression.rs b/crates/pdf/src/tests/compression.rs index 30e9d57e4b..8b88a0731d 100644 --- a/crates/pdf/src/tests/compression.rs +++ b/crates/pdf/src/tests/compression.rs @@ -292,6 +292,11 @@ fn tagged(img: &DynamicImage, icc: &[u8], format: ImageFormat) -> Vec { enc.set_icc_profile(icc.to_vec()).unwrap(); img.write_with_encoder(enc).unwrap(); } + ImageFormat::Jpeg => { + let mut enc = image::codecs::jpeg::JpegEncoder::new(&mut out); + enc.set_icc_profile(icc.to_vec()).unwrap(); + img.write_with_encoder(enc).unwrap(); + } _ => { let mut enc = image::codecs::png::PngEncoder::new(&mut out); enc.set_icc_profile(icc.to_vec()).unwrap(); @@ -423,11 +428,12 @@ fn tagged_tiffs_keep_their_icc_profile() { let spaces = export(&tiff(&colour(), &rgb), json!({"output": {"conversion": "destination", "destination": destination}})); assert!(spaces.len() == 1 && spaces[0].icc.as_deref() != Some(rgb.as_slice()), "converted to {destination}: {spaces:?}"); } - // PDF/X-1a has device colours only: a tagged gray TIFF or PNG is written in DeviceGray, kept as - // it is, resampled or compressed with JPEG. + // PDF/X-1a allows no ICC-based color spaces: a tagged gray TIFF, PNG or JPEG is written in + // DeviceGray, kept as it is, resampled or compressed with JPEG. let compressions = [json!({}), json!({"gray": {"downsample": "bicubic", "ppi": 150, "abovePpi": 225}}), json!({"gray": {"compression": "jpeg"}})]; let png = doc(tagged(&colour().grayscale(), &gray, ImageFormat::Png), "image/png"); - for (format, d) in [("TIFF", tiff(&colour().grayscale(), &gray)), ("PNG", png)] { + let jpeg = doc(tagged(&colour().grayscale(), &gray, ImageFormat::Jpeg), "image/jpeg"); + for (format, d) in [("TIFF", tiff(&colour().grayscale(), &gray)), ("PNG", png), ("JPEG", jpeg)] { for compression in &compressions { let mut settings: PdfSettings = serde_json::from_value(json!({"standard": "pdfX1a", "compatibility": "1.3", "compression": compression.clone()})).unwrap();