Skip to main content

pdfboss_core/
extension.rs

1//! The developer extensions the catalog declares (ISO 32000-1 §7.12): the
2//! `/Extensions` dictionary, one developer extensions dictionary per
3//! registered developer prefix, each naming the PDF version it extends and
4//! the developer's extension level.
5
6use crate::object::Dict;
7use crate::source::AsyncObjectSource;
8use crate::tree::resolved_dict;
9
10/// A developer-defined extension to the standard the document uses (ISO
11/// 32000-1 §7.12.2, Table 50): one entry of the catalog's `/Extensions`.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct DeveloperExtension {
14    /// The developer's registered prefix (Annex E), the entry's key.
15    pub prefix: String,
16    /// `/BaseVersion`: the PDF version the extension applies to, in the
17    /// catalog `/Version` syntax (`1.7`), as written (§7.12.3).
18    pub base_version: String,
19    /// `/ExtensionLevel`: the developer's number for the extension, higher
20    /// for a later extension to the same base version (§7.12.4).
21    pub extension_level: i64,
22}
23
24impl DeveloperExtension {
25    /// The base version as the two integers §7.12.3 reads it as, not as a
26    /// real number (`1.7` is `(1, 7)`); `None` when it is not two integers
27    /// around a period.
28    ///
29    /// Covers ISO 32000-1 §7.12.3.
30    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
36/// The developer extensions the catalog's `/Extensions` dictionary declares,
37/// sorted by prefix since a dictionary keeps no order: every entry whose
38/// value is a dictionary with a `/BaseVersion` name and an
39/// `/ExtensionLevel` integer. The dictionary's own `/Type` entry and an
40/// entry missing either value are skipped; indirect objects are followed
41/// although the clause asks for direct ones.
42///
43/// Covers ISO 32000-1 §7.12.2, §7.12.3 and §7.12.4.
44pub 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    /// The clause's EXAMPLE 3 with a `/Type` entry, an entry that is no
113    /// dictionary and one missing its level, all skipped, and an indirect
114    /// developer extensions dictionary, followed; a catalog without the
115    /// dictionary has no extensions.
116    // Covers ISO 32000-1 §7.12.2, §7.12.3 and §7.12.4.
117    #[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    /// A base version is two integers around a period, not a real number.
139    // Covers ISO 32000-1 §7.12.3.
140    #[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}