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