pdfboss_core/
output_intent.rs1use crate::object::{Dict, ObjRef, Object};
5use crate::source::AsyncObjectSource;
6use crate::tree::{resolved_dict, Entries};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct OutputIntent {
13 pub subtype: String,
16 pub output_condition: Option<String>,
18 pub output_condition_identifier: Option<String>,
22 pub registry_name: Option<String>,
25 pub info: Option<String>,
27 pub destination_profile: Option<ObjRef>,
31}
32
33pub 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 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 #[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 #[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}