use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BTreeSet};
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_font::{Font, FontCache};
use pdfrum_object::{Dict, Name, ObjRef, Object, Resolve};
use pdfrum_page::Op;
use crate::doc::EditDoc;
use crate::names;
const MAX_TREE_DEPTH: u32 = 64;
#[derive(Debug)]
pub(crate) struct Candidate {
pub(crate) root_font: ObjRef,
pub(crate) cid_font: ObjRef,
pub(crate) descriptor: ObjRef,
pub(crate) base_name: Vec<u8>,
pub(crate) used_gids: BTreeSet<u16>,
pub(crate) cid_to_gid: BTreeMap<u16, u16>,
}
pub(crate) fn candidates(
doc: &EditDoc<'_>,
new_nums: &[u32],
limits: &Limits,
) -> BTreeMap<u32, Candidate> {
let mut found = BTreeMap::new();
if new_nums.is_empty() {
return found;
}
let cache = FontCache::new();
for page in pages(doc) {
scan_page(doc, &page, new_nums, limits, &cache, &mut found);
}
found
}
fn pages(doc: &EditDoc<'_>) -> Vec<Dict> {
let Some(root) = doc.base().trailer().reference(names::ROOT) else {
return Vec::new();
};
let Some(catalog) = fetch_dict(doc, root) else {
return Vec::new();
};
let Some(tree) = catalog.reference(names::PAGES) else {
return Vec::new();
};
let mut out = Vec::new();
let mut seen = BTreeSet::new();
descend(doc, tree, 0, &mut seen, &mut out);
out
}
fn descend(
doc: &EditDoc<'_>,
node: ObjRef,
depth: u32,
seen: &mut BTreeSet<u32>,
out: &mut Vec<Dict>,
) {
if depth > MAX_TREE_DEPTH || !seen.insert(node.num) {
return;
}
let Some(dict) = fetch_dict(doc, node) else {
return;
};
let Some(Object::Array(kids)) = dict
.get(names::KIDS, doc)
.map(pdfrum_object::Resolved::into_owned)
else {
out.push(dict);
return;
};
for kid in kids.iter() {
if let Object::Ref(r) = kid {
descend(doc, *r, depth.saturating_add(1), seen, out);
}
}
}
fn fetch_dict(doc: &EditDoc<'_>, reference: ObjRef) -> Option<Dict> {
doc.fetch(reference).ok()?.as_dict().cloned()
}
fn scan_page(
doc: &EditDoc<'_>,
page: &Dict,
new_nums: &[u32],
limits: &Limits,
cache: &FontCache,
found: &mut BTreeMap<u32, Candidate>,
) {
let Some(fonts) = inherited_fonts(doc, page) else {
return;
};
let bytes = content_bytes(doc, page);
if bytes.is_empty() {
return;
}
let mut diags = Diagnostics::default();
let ops = pdfrum_page::parse_content(&bytes, limits, &mut diags);
let mut loaded: BTreeMap<Name, Option<Selected>> = BTreeMap::new();
let mut current: Option<Selected> = None;
for op in &ops {
match op {
Op::SetFont(name, _) => {
if !loaded.contains_key(name) {
let hit = admit(doc, &fonts, name, new_nums, limits, cache, found);
loaded.insert(name.clone(), hit);
}
current = loaded.get(name).and_then(Clone::clone);
}
Op::ShowText(s) | Op::NextLineShowText(s) | Op::SetSpacingShowText(_, _, s) => {
add_used(current.as_ref(), &s.bytes, found);
}
Op::ShowTextAdjusted(array) => {
for item in &array.items {
if let pdfrum_page::TextItem::Show(codes) = item {
add_used(current.as_ref(), codes, found);
}
}
}
_ => {}
}
}
}
#[derive(Clone)]
struct Selected {
program: u32,
font: std::sync::Arc<Font>,
}
fn admit(
doc: &EditDoc<'_>,
fonts: &Dict,
name: &Name,
new_nums: &[u32],
limits: &Limits,
cache: &FontCache,
found: &mut BTreeMap<u32, Candidate>,
) -> Option<Selected> {
let root_ref = fonts.reference(name)?;
let is_new = |reference: ObjRef| new_nums.binary_search(&reference.num).is_ok();
if !is_new(root_ref) {
return None;
}
let root = fetch_dict(doc, root_ref)?;
if root.name(names::SUBTYPE)?.as_bytes() != b"Type0" {
return None;
}
let descendants = root.array(names::DESCENDANT_FONTS, doc)?;
let cid_ref = descendants.reference_at(0)?;
let cid_font = fetch_dict(doc, cid_ref)?;
if cid_font.name(names::SUBTYPE)?.as_bytes() != b"CIDFontType2" {
return None;
}
let descriptor_ref = cid_font.reference(names::FONT_DESCRIPTOR)?;
let descriptor = fetch_dict(doc, descriptor_ref)?;
let program = descriptor.reference(names::FONT_FILE2)?;
if !is_new(cid_ref) || !is_new(descriptor_ref) || !is_new(program) {
return None;
}
let mut diags = Diagnostics::default();
let font = pdfrum_font::load(&root, doc, cache, limits, &mut diags)?;
if !matches!(font, Font::Type0(_)) {
return None;
}
if let Entry::Vacant(slot) = found.entry(program.num) {
slot.insert(Candidate {
root_font: root_ref,
cid_font: cid_ref,
descriptor: descriptor_ref,
base_name: root
.name(names::BASE_FONT)
.map(|n| n.as_bytes().to_vec())
.unwrap_or_default(),
used_gids: BTreeSet::new(),
cid_to_gid: BTreeMap::new(),
});
}
Some(Selected {
program: program.num,
font: std::sync::Arc::new(font),
})
}
fn add_used(current: Option<&Selected>, codes: &[u8], found: &mut BTreeMap<u32, Candidate>) {
let Some(selected) = current else {
return;
};
let Some(candidate) = found.get_mut(&selected.program) else {
return;
};
for item in selected.font.decode(codes) {
if let (Some(gid), Some(cid)) = (item.gid, item.cid) {
candidate.used_gids.insert(gid.0);
candidate.cid_to_gid.insert(cid.0, gid.0);
}
}
}
fn inherited_fonts(doc: &EditDoc<'_>, page: &Dict) -> Option<Dict> {
let mut node = page.clone();
for _ in 0..MAX_TREE_DEPTH {
if let Some(resources) = node.dict(names::RESOURCES, doc) {
return resources.dict(names::FONT, doc);
}
node = node.dict(names::PARENT, doc)?;
}
None
}
fn content_bytes(doc: &EditDoc<'_>, page: &Dict) -> Vec<u8> {
let mut out = Vec::new();
match page
.get(names::CONTENTS, doc)
.map(pdfrum_object::Resolved::into_owned)
{
Some(Object::Stream(s)) => {
out.extend_from_slice(&decode(doc, &s));
}
Some(Object::Array(a)) => {
for element in a.iter() {
if let Some(s) = element
.resolve(doc)
.ok()
.and_then(|r| r.as_stream().cloned())
{
out.extend_from_slice(&decode(doc, &s));
out.push(b' ');
}
}
}
_ => {}
}
out
}
fn decode(doc: &EditDoc<'_>, stream: &pdfrum_object::Stream) -> Vec<u8> {
let limits = Limits::default();
let mut diags = Diagnostics::default();
pdfrum_filters::decode_chain(stream, 0, doc, &limits, &mut diags).data
}