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.
This commit is contained in:
aiongg committed 2026-10-20 13:17:31 +00:00
1 parent 0fe6156614
commit 8fbfea8840
3 files changed
+50 -21

No files matched your search

+4 -4
View File
@@ -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`:
+31 -17
View File
@@ -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<SerializeSettings, WriteError> {
pub fn cmyk_blending(profile: &[u8]) -> Result<SerializeSettings, WriteError> {
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<SerializeSettings, WriteError> {
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
})
}
+15
View File
@@ -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() {