pdfboss_core/
extension.rs1use crate::object::Dict;
7use crate::source::AsyncObjectSource;
8use crate::tree::resolved_dict;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct DeveloperExtension {
14 pub prefix: String,
16 pub base_version: String,
19 pub extension_level: i64,
22}
23
24impl DeveloperExtension {
25 pub fn base_version_numbers(&self) -> Option<(u32, u32)> {
31 let (major, minor) = self.base_version.split_once('.')?;
32 Some((major.parse().ok()?, minor.parse().ok()?))
33 }
34}
35
36pub async fn extensions_with<S: AsyncObjectSource>(
45 src: &S,
46 trailer: &Dict,
47) -> Vec<DeveloperExtension> {
48 let mut found = Vec::new();
49 let Some(root) = trailer.get("Root") else {
50 return found;
51 };
52 let Some(catalog) = resolved_dict(src, root).await else {
53 return found;
54 };
55 let Some(entry) = catalog.get("Extensions") else {
56 return found;
57 };
58 let Some(extensions) = resolved_dict(src, entry).await else {
59 return found;
60 };
61 for (prefix, value) in extensions.iter() {
62 if prefix.0 == "Type" {
63 continue;
64 }
65 let Some(dict) = resolved_dict(src, value).await else {
66 continue;
67 };
68 let Some(base_version) = dict.get_name("BaseVersion") else {
69 continue;
70 };
71 let Some(extension_level) = dict.get_int("ExtensionLevel") else {
72 continue;
73 };
74 found.push(DeveloperExtension {
75 prefix: prefix.0.clone(),
76 base_version: base_version.0.clone(),
77 extension_level,
78 });
79 }
80 found.sort_by(|a, b| a.prefix.cmp(&b.prefix));
81 found
82}
83
84#[cfg(test)]
85mod tests {
86 use super::DeveloperExtension;
87 use crate::Document;
88 use pdfboss_testkit::PdfBuilder;
89
90 fn doc(catalog_extra: &str, objects: &[(u32, &str)]) -> Document {
91 let mut b = PdfBuilder::new();
92 b.object(
93 1,
94 &format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
95 );
96 b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
97 b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
98 for (num, body) in objects {
99 b.object(*num, body);
100 }
101 Document::load(b.build(1)).unwrap()
102 }
103
104 fn extension(prefix: &str, base_version: &str, extension_level: i64) -> DeveloperExtension {
105 DeveloperExtension {
106 prefix: prefix.to_string(),
107 base_version: base_version.to_string(),
108 extension_level,
109 }
110 }
111
112 #[test]
118 fn the_catalog_extensions_are_read_sorted_by_prefix() {
119 let found = doc(
120 "/Extensions << /Type /Extensions \
121 /GLGR << /Type /DeveloperExtensions /BaseVersion /1.7 /ExtensionLevel 1002 >> \
122 /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> \
123 /BAD 5 /HALF << /BaseVersion /1.7 >> /INDR 4 0 R >>",
124 &[(4, "<< /BaseVersion /2.0 /ExtensionLevel 1 >>")],
125 )
126 .extensions();
127 assert_eq!(
128 found,
129 vec![
130 extension("ADBE", "1.7", 3),
131 extension("GLGR", "1.7", 1002),
132 extension("INDR", "2.0", 1),
133 ]
134 );
135 assert_eq!(doc("", &[]).extensions(), Vec::new());
136 }
137
138 #[test]
141 fn base_versions_are_two_integers() {
142 assert_eq!(
143 extension("ADBE", "1.7", 3).base_version_numbers(),
144 Some((1, 7))
145 );
146 assert_eq!(
147 extension("ADBE", "1.10", 3).base_version_numbers(),
148 Some((1, 10))
149 );
150 assert_eq!(extension("ADBE", "1.7.1", 3).base_version_numbers(), None);
151 assert_eq!(extension("ADBE", "one", 3).base_version_numbers(), None);
152 }
153}