From 8fbfea8840f0a0f5a22df18901e9db324894b2ce Mon Sep 17 00:00:00 2001 From: aiongg Date: Tue, 20 Oct 2026 13:17:31 +0000 Subject: [PATCH] pdf: CMYK blending without an output intent for plain print PDFs print::cmyk_blending(profile) blends transparency in CMYK and writes no output intent or /Trapped, as a non-PDF/X print PDF does. print::settings builds on it and adds the GTS_PDFX output intent and /Trapped /False. --- crates/pdf/README.md | 8 +++--- crates/pdf/src/write/print.rs | 48 +++++++++++++++++++++------------ crates/pdf/tests/print_color.rs | 15 +++++++++++ 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/crates/pdf/README.md b/crates/pdf/README.md index c8efee17..f9942979 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -122,10 +122,10 @@ serialization uses the same backend version as before extraction. It provides: ### Print color -`write::print::settings` returns `SerializeSettings` for CMYK print: DeviceCMYK color, -transparency blending in CMYK (`GroupColorSpace::Cmyk`), the document's CMYK profile as a -`GTS_PDFX` output intent and `/Trapped /False`. The profile is the document's working space; -print export always carries it. craft-pdf takes the profile bytes and does not depend on +`write::print::cmyk_blending(profile)` returns `SerializeSettings` for a plain CMYK print +PDF: DeviceCMYK color and transparency blending in CMYK (`GroupColorSpace::Cmyk`), with no +output intent. `write::print::settings` adds the document's CMYK profile as a `GTS_PDFX` +output intent and `/Trapped /False`, for PDF/X. The profile is the document's working space. craft-pdf takes the profile bytes and does not depend on craft-cms. Its CC0 `builtin::COATED_FOGRA39_ICC` is the default working space, described by `PrintColor::fogra39`: diff --git a/crates/pdf/src/write/print.rs b/crates/pdf/src/write/print.rs index bceef66d..951ce5a8 100644 --- a/crates/pdf/src/write/print.rs +++ b/crates/pdf/src/write/print.rs @@ -1,8 +1,9 @@ //! Serialization settings for CMYK print output. //! -//! The document's CMYK profile, its working space, describes the printing condition: the -//! settings write it as the `GTS_PDFX` output intent, and DeviceCMYK content and the CMYK -//! blending groups are interpreted through it. This module takes the profile bytes and has no +//! The document's CMYK profile, its working space, describes the printing condition. +//! [`cmyk_blending`] blends transparency in CMYK and writes no output intent, like a plain +//! print PDF; [`settings`] also writes the profile as the `GTS_PDFX` output intent, through +//! which DeviceCMYK content and the CMYK blending groups are interpreted, as PDF/X needs. This module takes the profile bytes and has no //! color-management dependency; craft-cms's `builtin::COATED_FOGRA39_ICC` is a suitable //! default profile, described by [`PrintColor::fogra39`]. //! @@ -42,28 +43,41 @@ impl<'a> PrintColor<'a> { } } -/// Settings for CMYK print output: DeviceCMYK color, CMYK blending groups, the profile as -/// the `GTS_PDFX` output intent (and as `cmyk_profile`) and `/Trapped /False`. Other fields -/// keep their defaults; override them with struct update syntax. +/// Settings for a CMYK print PDF without an output intent: DeviceCMYK color and CMYK +/// blending groups, with `profile` as `cmyk_profile`. Other fields keep their defaults; +/// override them with struct update syntax. /// /// Fails with [`WriteError::InvalidProfile`] when `profile` is not a readable CMYK ICC /// profile. -pub fn settings(color: PrintColor<'_>) -> Result { +pub fn cmyk_blending(profile: &[u8]) -> Result { guard(|| { - let profile = ICCProfile::<4>::new(color.profile).ok_or(WriteError::InvalidProfile)?; + let profile = ICCProfile::<4>::new(profile).ok_or(WriteError::InvalidProfile)?; Ok(SerializeSettings { no_device_cs: false, - cmyk_profile: Some(profile.clone()), + cmyk_profile: Some(profile), group_color_space: GroupColorSpace::Cmyk, - output_intent: Some(OutputIntent { - profile, - condition_identifier: color.condition_identifier.to_owned(), - condition: color.condition.map(str::to_owned), - registry_name: color.registry_name.map(str::to_owned), - info: color.info.map(str::to_owned), - }), - trapped: Some(Trapped::False), ..SerializeSettings::default() }) }) } + +/// [`cmyk_blending`] plus the profile as the `GTS_PDFX` output intent and `/Trapped /False`, +/// for PDF/X export. +/// +/// Fails with [`WriteError::InvalidProfile`] when `profile` is not a readable CMYK ICC +/// profile. +pub fn settings(color: PrintColor<'_>) -> Result { + let settings = cmyk_blending(color.profile)?; + let profile = settings.cmyk_profile.clone().ok_or(WriteError::InvalidProfile)?; + Ok(SerializeSettings { + output_intent: Some(OutputIntent { + profile, + condition_identifier: color.condition_identifier.to_owned(), + condition: color.condition.map(str::to_owned), + registry_name: color.registry_name.map(str::to_owned), + info: color.info.map(str::to_owned), + }), + trapped: Some(Trapped::False), + ..settings + }) +} diff --git a/crates/pdf/tests/print_color.rs b/crates/pdf/tests/print_color.rs index c70cc975..037d9d8f 100644 --- a/crates/pdf/tests/print_color.rs +++ b/crates/pdf/tests/print_color.rs @@ -987,6 +987,21 @@ fn print_settings_blend_in_cmyk_and_name_the_profile_as_the_output_intent() { assert!(!text.contains("GTS_PDFXVersion")); } +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), test)] +fn cmyk_blending_writes_no_output_intent_or_trapped() { + let settings = print::cmyk_blending(FOGRA39).unwrap(); + assert_eq!(settings.group_color_space, GroupColorSpace::Cmyk); + assert!(settings.output_intent.is_none() && settings.trapped.is_none()); + + let bytes = sample(settings.clone(), placed(&settings).as_deref()).unwrap(); + let cmyk_group = Some(Some("DeviceCMYK".to_owned())); + assert_eq!(groups(&bytes).pages, vec![cmyk_group.clone(), None, cmyk_group]); + assert!(output_intents(&bytes).is_empty()); + assert!(!String::from_utf8_lossy(&bytes).contains("Trapped")); + assert!(matches!(print::cmyk_blending(b"not a profile"), Err(WriteError::InvalidProfile))); +} + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), test)] fn print_settings_reject_a_profile_that_is_not_cmyk() {