Skip to main content

pdfboss_core/
output_intent.rs

1//! Output intents (ISO 32000-1 §14.11.5): the catalog's `/OutputIntents`
2//! array, read as data. Rendering does not apply a destination profile.
3
4use crate::object::{Dict, ObjRef, Object};
5use crate::source::AsyncObjectSource;
6use crate::tree::{resolved_dict, Entries};
7
8/// An output intent dictionary (ISO 32000-1 §14.11.5, Table 365): the colour
9/// reproduction characteristics of one intended output device or production
10/// condition, as a PDF/X, PDF/A or PDF/E file declares them.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct OutputIntent {
13    /// `/S`: the subtype naming the standard the intent belongs to:
14    /// `GTS_PDFX`, `GTS_PDFA1`, `ISO_PDFE1`, or a name an extension defines.
15    pub subtype: String,
16    /// `/OutputCondition`: the intended output condition, worded for people.
17    pub output_condition: Option<String>,
18    /// `/OutputConditionIdentifier`: the condition's name in a registry such
19    /// as the ICC characterization data registry, `Custom`, or an
20    /// application's own name. Required by the table, `None` when missing.
21    pub output_condition_identifier: Option<String>,
22    /// `/RegistryName`: the registry, conventionally a URI, that defines the
23    /// identifier.
24    pub registry_name: Option<String>,
25    /// `/Info`: further human-readable information about the condition.
26    pub info: Option<String>,
27    /// `/DestOutputProfile`: the ICC profile stream, by reference, that maps
28    /// the document's source colours to the device's colorants; `None` when
29    /// absent or written directly, a stream always being indirect.
30    pub destination_profile: Option<ObjRef>,
31}
32
33/// The output intents the catalog's `/OutputIntents` array declares, in
34/// array order: every entry that is a dictionary carrying the required `/S`
35/// subtype. Other entries are skipped, and a catalog without the array, or
36/// whose entry is no array, has none. Nothing here converts colours: the
37/// clause leaves the data informational, and rendering ignores it.
38///
39/// Covers ISO 32000-1 §14.11.5.
40pub async fn output_intents_with<S: AsyncObjectSource>(
41    src: &S,
42    trailer: &Dict,
43) -> Vec<OutputIntent> {
44    let mut intents = Vec::new();
45    let Some(root) = trailer.get("Root") else {
46        return intents;
47    };
48    let Some(catalog) = resolved_dict(src, root).await else {
49        return intents;
50    };
51    let Some(entry) = catalog.get("OutputIntents") else {
52        return intents;
53    };
54    let Ok(array) = src.resolve(entry).await else {
55        return intents;
56    };
57    let Some(array) = array.as_array() else {
58        return intents;
59    };
60    for value in array {
61        let Some(dict) = resolved_dict(src, value).await else {
62            continue;
63        };
64        let Some(subtype) = dict.get_name("S") else {
65            continue;
66        };
67        let entries = Entries { src, dict: &dict };
68        intents.push(OutputIntent {
69            subtype: subtype.0.clone(),
70            output_condition: entries.text("OutputCondition").await,
71            output_condition_identifier: entries.text("OutputConditionIdentifier").await,
72            registry_name: entries.text("RegistryName").await,
73            info: entries.text("Info").await,
74            destination_profile: dict.get("DestOutputProfile").and_then(Object::as_ref),
75        });
76    }
77    intents
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::document::Document;
84    use crate::object::ObjRef;
85    use pdfboss_testkit::PdfBuilder;
86
87    /// A document whose catalog carries `output_intents` as its
88    /// `/OutputIntents` value, plus the clause's EXAMPLE 1 as object 5.
89    fn with_intents(output_intents: &str) -> Document {
90        let mut b = PdfBuilder::new();
91        b.object(
92            1,
93            &format!("<< /Type /Catalog /Pages 2 0 R /OutputIntents {output_intents} >>"),
94        );
95        b.object(2, "<< /Type /Pages /Kids [] /Count 0 >>");
96        b.object(
97            5,
98            "<< /Type /OutputIntent /S /GTS_PDFX /OutputCondition (CGATS TR 001 (SWOP)) \
99             /OutputConditionIdentifier (CGATS TR 001) /RegistryName (http://www.color.org) \
100             /DestOutputProfile 100 0 R >>",
101        );
102        Document::load(b.build(1)).unwrap()
103    }
104
105    /// The clause's EXAMPLE 1 by reference and a direct PDF/A intent with a
106    /// UTF-16 `/Info`, in array order.
107    // Covers ISO 32000-1 §14.11.5.
108    #[test]
109    fn reads_every_entry_of_the_output_intent_dictionaries() {
110        let doc = with_intents(
111            "[ 5 0 R << /S /GTS_PDFA1 /OutputConditionIdentifier (sRGB IEC61966-2.1) \
112             /Info <FEFF007300520047004200200070007200650076006900650077> >> ]",
113        );
114        assert_eq!(
115            doc.output_intents(),
116            vec![
117                OutputIntent {
118                    subtype: "GTS_PDFX".to_string(),
119                    output_condition: Some("CGATS TR 001 (SWOP)".to_string()),
120                    output_condition_identifier: Some("CGATS TR 001".to_string()),
121                    registry_name: Some("http://www.color.org".to_string()),
122                    info: None,
123                    destination_profile: Some(ObjRef { num: 100, gen: 0 }),
124                },
125                OutputIntent {
126                    subtype: "GTS_PDFA1".to_string(),
127                    output_condition: None,
128                    output_condition_identifier: Some("sRGB IEC61966-2.1".to_string()),
129                    registry_name: None,
130                    info: Some("sRGB preview".to_string()),
131                    destination_profile: None,
132                },
133            ]
134        );
135    }
136
137    /// An intent without the required `/S`, a non-dictionary entry and a
138    /// direct `/DestOutputProfile` (the table asks for a stream, which is
139    /// always indirect); a catalog whose entry is no array has no intents.
140    // Covers ISO 32000-1 §14.11.5.
141    #[test]
142    fn malformed_intents_are_skipped_and_a_direct_profile_is_no_reference() {
143        let doc = with_intents(
144            "[ << /Type /OutputIntent /OutputConditionIdentifier (nameless) >> 42 \
145             << /S /ISO_PDFE1 /DestOutputProfile << /N 3 >> >> ]",
146        );
147        let intents = doc.output_intents();
148        assert_eq!(intents.len(), 1);
149        assert_eq!(intents[0].subtype, "ISO_PDFE1");
150        assert_eq!(intents[0].destination_profile, None);
151        assert_eq!(with_intents("<< /S /GTS_PDFX >>").output_intents(), vec![]);
152        let plain = Document::load(pdfboss_testkit::simple_doc("plain")).unwrap();
153        assert_eq!(plain.output_intents(), vec![]);
154    }
155}