use crate::object::Dict;
use crate::source::AsyncObjectSource;
use crate::tree::resolved_dict;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeveloperExtension {
pub prefix: String,
pub base_version: String,
pub extension_level: i64,
}
impl DeveloperExtension {
pub fn base_version_numbers(&self) -> Option<(u32, u32)> {
let (major, minor) = self.base_version.split_once('.')?;
Some((major.parse().ok()?, minor.parse().ok()?))
}
}
pub async fn extensions_with<S: AsyncObjectSource>(
src: &S,
trailer: &Dict,
) -> Vec<DeveloperExtension> {
let mut found = Vec::new();
let Some(root) = trailer.get("Root") else {
return found;
};
let Some(catalog) = resolved_dict(src, root).await else {
return found;
};
let Some(entry) = catalog.get("Extensions") else {
return found;
};
let Some(extensions) = resolved_dict(src, entry).await else {
return found;
};
for (prefix, value) in extensions.iter() {
if prefix.0 == "Type" {
continue;
}
let Some(dict) = resolved_dict(src, value).await else {
continue;
};
let Some(base_version) = dict.get_name("BaseVersion") else {
continue;
};
let Some(extension_level) = dict.get_int("ExtensionLevel") else {
continue;
};
found.push(DeveloperExtension {
prefix: prefix.0.clone(),
base_version: base_version.0.clone(),
extension_level,
});
}
found.sort_by(|a, b| a.prefix.cmp(&b.prefix));
found
}
#[cfg(test)]
mod tests {
use super::DeveloperExtension;
use crate::Document;
use pdfboss_testkit::PdfBuilder;
fn doc(catalog_extra: &str, objects: &[(u32, &str)]) -> Document {
let mut b = PdfBuilder::new();
b.object(
1,
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
);
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
for (num, body) in objects {
b.object(*num, body);
}
Document::load(b.build(1)).unwrap()
}
fn extension(prefix: &str, base_version: &str, extension_level: i64) -> DeveloperExtension {
DeveloperExtension {
prefix: prefix.to_string(),
base_version: base_version.to_string(),
extension_level,
}
}
#[test]
fn the_catalog_extensions_are_read_sorted_by_prefix() {
let found = doc(
"/Extensions << /Type /Extensions \
/GLGR << /Type /DeveloperExtensions /BaseVersion /1.7 /ExtensionLevel 1002 >> \
/ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> \
/BAD 5 /HALF << /BaseVersion /1.7 >> /INDR 4 0 R >>",
&[(4, "<< /BaseVersion /2.0 /ExtensionLevel 1 >>")],
)
.extensions();
assert_eq!(
found,
vec![
extension("ADBE", "1.7", 3),
extension("GLGR", "1.7", 1002),
extension("INDR", "2.0", 1),
]
);
assert_eq!(doc("", &[]).extensions(), Vec::new());
}
#[test]
fn base_versions_are_two_integers() {
assert_eq!(
extension("ADBE", "1.7", 3).base_version_numbers(),
Some((1, 7))
);
assert_eq!(
extension("ADBE", "1.10", 3).base_version_numbers(),
Some((1, 10))
);
assert_eq!(extension("ADBE", "1.7.1", 3).base_version_numbers(), None);
assert_eq!(extension("ADBE", "one", 3).base_version_numbers(), None);
}
}