Browse / github.com/storytold/designcraft / patch
PDF/X: the output condition identifier the profile names (#1475)
PDF/X: the output condition identifier the profile names (#1475) Fixes the lookup half of #1475. A loaded CMYK working space exported `/OutputConditionIdentifier (Custom)` whatever the profile was — only the built-in Coated FOGRA39 got a registered identifier. A print provider's preflight matches on that field, so `Custom` loses the one thing it reads: ``` before /S /GTS_PDFX /OutputConditionIdentifier (Custom) /OutputCondition (eciCMYK v2) … after /S /GTS_PDFX /OutputConditionIdentifier (FOGRA59) /OutputCondition (eciCMYK v2) … ``` ### Read, not guessed `registered_identifier` takes the characterisation from the profile's **own description** and f
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Hamza0101 (imported from pull request #1557, open)
- Base
3ce3a24f08d03935bc8cf7cb23755aca8065558c- Size
- +88 −9 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
5bda6bc227a20b205d4d7415
How it went
No other operator has reported using it yet.
Diff
raw · crates/color/src/cms/mod.rs crates/color/src/cms/tests.rs crates/pdf/src/pdfx.rs
@@ -160,11 +160,41 @@ impl ColorSettings { pub struct OutputIntent { pub icc: Cow<'static, [u8]>, /// `OutputConditionIdentifier`: a registered characterisation, or `Custom`.- pub identifier: &'static str,+ pub identifier: Cow<'static, str>, /// `OutputCondition` / `Info`: the profile's name. pub condition: String, } +/// The registered characterisation identifier a profile names in its own description, spelled as+/// the ICC registry spells it.+///+/// A print provider's preflight matches on the identifier, so `Custom` is only right when the+/// profile names no characterisation: `PSOuncoated_v3_FOGRA52` is FOGRA52 and+/// `eciCMYK v2 (FOGRA59)` is FOGRA59, and saying `Custom` for those loses the one field the+/// provider reads (#1475). Nothing is guessed from a profile's other properties — the identifier+/// has to be in the description, which is where these profiles put it.+pub fn registered_identifier(description: &str) -> Option<String> {+ // `to_ascii_uppercase` keeps every byte's position, so an offset into `upper` is an offset+ // into `description` as well. The digits are read from `upper` at that same offset: reading+ // them from a `Vec<char>` indexed by a byte offset went wrong as soon as anything before the+ // name was multi-byte -- "Coated (R) FOGRA39" gave FOGRA9 (review of #1475).+ let upper: String = description.to_ascii_uppercase();+ let bytes = upper.as_bytes();+ // `FOGRA` or `CGATS21_CRPC` followed by digits, and not inside a longer word.+ for prefix in ["CGATS21_CRPC", "FOGRA"] {+ let mut from = 0;+ while let Some(at) = upper.get(from..).and_then(|s| s.find(prefix)).map(|i| i + from) {+ from = at + prefix.len();+ let before_is_word = at > 0 && bytes.get(at - 1).is_some_and(|b| b.is_ascii_alphanumeric());+ let digits: String = upper.get(from..).unwrap_or("").chars().take_while(char::is_ascii_digit).collect();+ if !before_is_word && !digits.is_empty() {+ return Some(format!("{prefix}{digits}"));+ }+ }+ }+ None+}+ /// Target colour model for [`Cms::convert`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Model {@@ -332,12 +362,18 @@ impl Cms { /// described by the Generic CMYK one. pub fn output_intent(&self) -> Result<OutputIntent, CmsError> { Ok(match &self.cmyk {- CmykSpace::Icc(p) if p.name == COATED_FOGRA39 => {- OutputIntent { icc: Cow::Borrowed(craft_cms::builtin::COATED_FOGRA39_ICC), identifier: "FOGRA39", condition: COATED_FOGRA39.into() }- }- CmykSpace::Icc(p) => OutputIntent { icc: Cow::Owned(p.profile().to_bytes().to_vec()), identifier: "Custom", condition: p.name.clone() },+ CmykSpace::Icc(p) if p.name == COATED_FOGRA39 => OutputIntent {+ icc: Cow::Borrowed(craft_cms::builtin::COATED_FOGRA39_ICC),+ identifier: "FOGRA39".into(),+ condition: COATED_FOGRA39.into(),+ },+ CmykSpace::Icc(p) => OutputIntent {+ icc: Cow::Owned(p.profile().to_bytes().to_vec()),+ identifier: registered_identifier(&p.name).map_or(Cow::Borrowed("Custom"), Cow::Owned),+ condition: p.name.clone(),+ }, CmykSpace::Generic(_) | CmykSpace::Device => {- OutputIntent { icc: Cow::Borrowed(icc_out::generic_cmyk_icc()?), identifier: "Custom", condition: GENERIC_CMYK.into() }+ OutputIntent { icc: Cow::Borrowed(icc_out::generic_cmyk_icc()?), identifier: "Custom".into(), condition: GENERIC_CMYK.into() } } }) }@@ -81,13 +81,56 @@ fn saved_cmyk_names_still_load() { #[test] fn output_intent_follows_the_working_cmyk_space() { let fogra = cms().output_intent().unwrap();- assert_eq!((fogra.identifier, fogra.condition.as_str()), ("FOGRA39", COATED_FOGRA39));+ assert_eq!((&*fogra.identifier, fogra.condition.as_str()), ("FOGRA39", COATED_FOGRA39)); assert_eq!(&*fogra.icc, craft_cms::builtin::COATED_FOGRA39_ICC); for name in [GENERIC_CMYK, DEVICE_CMYK] { let g = Cms::new(&ColorSettings { cmyk: name.into(), ..Default::default() }).unwrap().output_intent().unwrap();- assert_eq!((g.identifier, g.condition.as_str()), ("Custom", GENERIC_CMYK), "{name}");+ assert_eq!((&*g.identifier, g.condition.as_str()), ("Custom", GENERIC_CMYK), "{name}"); assert_eq!(&*g.icc, icc_out::generic_cmyk_icc().unwrap()); }+ // A loaded profile whose description names its characterisation: the registered identifier,+ // not `Custom`, which is the one field a print provider's preflight matches on (#1475).+ let bytes = craft_cms::Builtin::CoatedCmyk.profile().to_bytes().to_vec();+ for (name, want) in [+ ("PSOuncoated_v3_FOGRA52", "FOGRA52"),+ ("eciCMYK v2 (FOGRA59)", "FOGRA59"),+ ("Coated FOGRA51 (ISO 12647-2:2013)", "FOGRA51"),+ ("CGATS21_CRPC6", "CGATS21_CRPC6"),+ ("Our House Press", "Custom"),+ ] {+ super::register_icc(&bytes, Some(name.to_string())).unwrap_or_else(|e| panic!("{name}: {e}"));+ let c = Cms::new(&ColorSettings { cmyk: name.into(), ..Default::default() }).unwrap_or_else(|e| panic!("{name}: {e}"));+ let intent = c.output_intent().unwrap();+ assert_eq!((&*intent.identifier, intent.condition.as_str()), (want, name), "{name}");+ }+}++/// The identifier is read off the profile's own description, never guessed from anything else.+#[test]+fn a_registered_characterisation_is_the_one_the_profile_names() {+ use super::registered_identifier;+ for (description, want) in [+ ("PSOuncoated_v3_FOGRA52", Some("FOGRA52")),+ ("eciCMYK v2 (FOGRA59)", Some("FOGRA59")),+ ("Coated FOGRA39 (ISO 12647-2:2004)", Some("FOGRA39")),+ ("fogra51 lowercase", Some("FOGRA51")),+ ("CGATS21_CRPC6", Some("CGATS21_CRPC6")),+ ("GRACoL2013_CRPC6", None),+ ("Our House Press", None),+ ("FOGRA", None),+ ("XFOGRA39", None),+ ("", None),+ // A multi-byte character before the name used to shift the digits by one, so this gave+ // FOGRA9 and a print provider's preflight read the wrong characterisation (review of+ // #1475). The offset and the digits now come from the same string.+ ("Coated \u{AE} FOGRA39 (ISO 12647-2:2004)", Some("FOGRA39")),+ ("\u{201C}eciCMYK v2\u{201D} (FOGRA59)", Some("FOGRA59")),+ ("\u{30B3}\u{30FC}\u{30C8} FOGRA51", Some("FOGRA51")),+ ("Papier m\u{E4}t\u{E9} CGATS21_CRPC6", Some("CGATS21_CRPC6")),+ ("", None),+ ] {+ assert_eq!(registered_identifier(description).as_deref(), want, "{description:?}");+ } } #[test]@@ -60,7 +60,7 @@ pub fn make_pdfx4(pdf: &[u8], title: &str, date: Option<i64>) -> Option<Vec<u8>> oi_id, format!( "<</Type/OutputIntent/S/GTS_PDFX/OutputConditionIdentifier{}/OutputCondition{}/Info{}/RegistryName(http://www.color.org)/DestOutputProfile {icc_id} 0 R>>",- lit(intent.identifier),+ lit(&intent.identifier), lit(cond), lit(cond) )