use crate::names;
use pdfrum_object::{Dict, Name, Object, Resolve};
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Resources {
pub chosen: Option<Dict>,
pub page: Option<Dict>,
}
impl Resources {
#[must_use]
pub fn choose(own: Option<Dict>, parent: Option<Dict>, page: Option<Dict>) -> Self {
let chosen = own.or(parent).or_else(|| page.clone());
Self { chosen, page }
}
#[must_use]
pub fn for_page(page: Option<Dict>) -> Self {
Self {
chosen: page.clone(),
page,
}
}
#[must_use]
pub fn holder<R: Resolve>(&self, category: &Name, r: &R) -> Option<Dict> {
if let Some(chosen) = &self.chosen
&& let Some(found) = chosen.dict(category, r)
{
return Some(found);
}
let page = self.page.as_ref()?;
if self.chosen.as_ref() == Some(page) {
return None;
}
page.dict(category, r)
}
#[must_use]
pub fn find<R: Resolve>(&self, category: &Name, name: &Name, r: &R) -> Option<Object> {
let holder = self.holder(category, r)?;
Some(holder.get(name, r)?.into_owned())
}
#[must_use]
pub fn find_ref<R: Resolve>(
&self,
category: &Name,
name: &Name,
r: &R,
) -> Option<pdfrum_object::ObjRef> {
match self.holder(category, r)?.raw(name)? {
Object::Ref(reference) => Some(*reference),
_ => None,
}
}
#[must_use]
pub fn color_spaces<R: Resolve>(&self, r: &R) -> Option<Dict> {
self.holder(names::COLOR_SPACE, r)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::Resources;
use crate::names;
use pdfrum_object::{Dict, Name, NoResolve, Object};
fn category(key: &str, entry: &str) -> Dict {
Dict::from_pairs([(
Name::from(key),
Object::Dict(Dict::from_pairs([(
Name::from(entry),
Object::Name(Name::from(entry)),
)])),
)])
}
#[test]
fn the_first_non_null_dictionary_wins_whole() {
let own = category("Font", "F1");
let page = category("XObject", "X1");
let res = Resources::choose(Some(own), None, Some(page));
assert!(
res.find(names::FONT, &Name::from("F1"), &NoResolve)
.is_some()
);
assert!(
res.find(names::XOBJECT, &Name::from("X1"), &NoResolve)
.is_some()
);
}
#[test]
fn a_present_category_lacking_the_name_does_not_fall_back() {
let mut own = category("Font", "F1");
own.push(
Name::from("XObject"),
Object::Dict(Dict::from_pairs([(
Name::from("Other"),
Object::Name(Name::from("Other")),
)])),
);
let page = category("XObject", "X1");
let res = Resources::choose(Some(own), None, Some(page));
assert!(
res.find(names::XOBJECT, &Name::from("X1"), &NoResolve)
.is_none(),
"a present-but-incomplete category must not fall back"
);
}
#[test]
fn the_parent_is_consulted_before_the_page() {
let parent = category("Font", "PARENT");
let page = category("Font", "PAGE");
let res = Resources::choose(None, Some(parent), Some(page));
assert!(
res.find(names::FONT, &Name::from("PARENT"), &NoResolve)
.is_some()
);
assert!(
res.find(names::FONT, &Name::from("PAGE"), &NoResolve)
.is_none()
);
}
#[test]
fn a_page_looks_up_only_in_itself() {
let page = category("Font", "F1");
let res = Resources::for_page(Some(page));
assert!(
res.find(names::FONT, &Name::from("F1"), &NoResolve)
.is_some()
);
assert!(
res.find(names::XOBJECT, &Name::from("X1"), &NoResolve)
.is_none()
);
}
#[test]
fn no_resources_at_all_finds_nothing() {
let res = Resources::default();
assert!(
res.find(names::FONT, &Name::from("F1"), &NoResolve)
.is_none()
);
assert!(res.color_spaces(&NoResolve).is_none());
}
}