use std::collections::BTreeMap;
use pdfrum_object::{ByteSpan, Dict, Name, ObjRef, Object, Resolve, Stream};
use crate::doc::EditDoc;
use crate::font::collect::Candidate;
use crate::font::{is_opentype_cff, subset, subset_name, subset_tag};
use crate::names;
use crate::write::id::IdSource;
pub(crate) type Overrides = BTreeMap<u32, Object>;
pub(crate) fn build(
doc: &EditDoc<'_>,
new_nums: &[u32],
id_source: IdSource,
next_number: &mut u32,
) -> Overrides {
let mut overrides = Overrides::new();
let limits = pdfrum_common::Limits::default();
for (program, candidate) in crate::font::collect::candidates(doc, new_nums, &limits) {
one(
doc,
program,
&candidate,
id_source,
next_number,
&mut overrides,
);
}
overrides
}
fn one(
doc: &EditDoc<'_>,
program: u32,
candidate: &Candidate,
id_source: IdSource,
next_number: &mut u32,
overrides: &mut Overrides,
) {
if candidate.used_gids.is_empty() {
return;
}
let Some(original) = program_bytes(doc, program) else {
return;
};
if is_opentype_cff(&original) {
return;
}
let gids: Vec<u16> = candidate.used_gids.iter().copied().collect();
let Ok(subsetted) = subset(&original, &gids) else {
return;
};
if subsetted.bytes.len() >= original.len() {
return;
}
let (Some(root), Some(cid_font), Some(descriptor)) = (
fetch_dict(doc, candidate.root_font),
fetch_dict(doc, candidate.cid_font),
fetch_dict(doc, candidate.descriptor),
) else {
return;
};
let name = Name::from(&subset_name(&candidate.base_name, subset_tag(id_source))[..]);
let table = cid_to_gid_table(candidate, &subsetted.gid_map);
let map_ref = ObjRef::new(*next_number, 0);
*next_number = next_number.saturating_add(1);
overrides.insert(map_ref.num, stream(Dict::new(), table));
let length1 = i64::try_from(subsetted.bytes.len()).unwrap_or(i64::MAX);
overrides.insert(
program,
stream(
Dict::from_pairs([(names::LENGTH1.clone(), Object::Int(length1))]),
subsetted.bytes,
),
);
overrides.insert(
candidate.root_font.num,
Object::Dict(set(&root, names::BASE_FONT, Object::Name(name.clone()))),
);
let cid_font = set(&cid_font, names::BASE_FONT, Object::Name(name.clone()));
let cid_font = set(&cid_font, names::CID_TO_GID_MAP, Object::Ref(map_ref));
overrides.insert(candidate.cid_font.num, Object::Dict(cid_font));
overrides.insert(
candidate.descriptor.num,
Object::Dict(set(&descriptor, names::FONT_NAME, Object::Name(name))),
);
}
fn cid_to_gid_table(candidate: &Candidate, map: &super::GidMap) -> Vec<u8> {
let highest = candidate.cid_to_gid.keys().copied().max().unwrap_or(0);
let len = usize::from(highest).saturating_add(1).saturating_mul(2);
let mut table = vec![0u8; len];
for (cid, old_gid) in &candidate.cid_to_gid {
let Some(new_gid) = map.get(*old_gid) else {
continue;
};
let at = usize::from(*cid).saturating_mul(2);
if let Some(slot) = table.get_mut(at..at.saturating_add(2)) {
slot.copy_from_slice(&new_gid.to_be_bytes());
}
}
table
}
fn stream(dict: Dict, bytes: Vec<u8>) -> Object {
let length = i64::try_from(bytes.len()).unwrap_or(i64::MAX);
let mut dict = dict;
dict.push(names::LENGTH.clone(), Object::Int(length));
Object::Stream(Box::new(Stream::new(dict, ByteSpan::from(bytes))))
}
fn set(dict: &Dict, key: &Name, value: Object) -> Dict {
let mut out = Dict::new();
let mut placed = false;
for (k, v) in dict.iter() {
if k == key {
if !placed {
out.push(k.clone(), value.clone());
placed = true;
}
continue;
}
out.push(k.clone(), v.clone());
}
if !placed {
out.push(key.clone(), value);
}
out
}
fn fetch_dict(doc: &EditDoc<'_>, reference: ObjRef) -> Option<Dict> {
doc.fetch(reference).ok()?.as_dict().cloned()
}
fn program_bytes(doc: &EditDoc<'_>, program: u32) -> Option<Vec<u8>> {
let object = doc.fetch(ObjRef::new(program, 0)).ok()?;
let stream = object.as_stream()?;
let limits = pdfrum_common::Limits::default();
let mut diags = pdfrum_common::Diagnostics::default();
let data = pdfrum_filters::decode_chain(stream, 0, doc, &limits, &mut diags).data;
(!data.is_empty()).then_some(data)
}
#[cfg(test)]
mod tests {
use super::{cid_to_gid_table, set};
use crate::font::GidMap;
use crate::font::collect::Candidate;
use crate::names;
use pdfrum_object::{Dict, Name, ObjRef, Object};
use std::collections::{BTreeMap, BTreeSet};
fn candidate(pairs: &[(u16, u16)]) -> Candidate {
Candidate {
root_font: ObjRef::new(1, 0),
cid_font: ObjRef::new(2, 0),
descriptor: ObjRef::new(3, 0),
base_name: b"Test".to_vec(),
used_gids: pairs.iter().map(|(_, gid)| *gid).collect::<BTreeSet<_>>(),
cid_to_gid: pairs.iter().copied().collect::<BTreeMap<_, _>>(),
}
}
fn glyph_at(table: &[u8], cid: u16) -> Option<u16> {
let at = usize::from(cid) * 2;
let pair = table.get(at..at + 2)?;
Some(u16::from_be_bytes([*pair.first()?, *pair.get(1)?]))
}
#[test]
fn the_table_sends_each_cid_to_its_new_glyph() {
let map = GidMap::from_pairs([(0, 0), (12, 1), (40, 2)]);
let table = cid_to_gid_table(&candidate(&[(5, 40), (9, 12)]), &map);
assert_eq!(glyph_at(&table, 5), Some(2));
assert_eq!(glyph_at(&table, 9), Some(1));
}
#[test]
fn a_cid_the_page_never_showed_reads_as_notdef() {
let map = GidMap::from_pairs([(0, 0), (40, 1)]);
let table = cid_to_gid_table(&candidate(&[(5, 40)]), &map);
assert_eq!(glyph_at(&table, 4), Some(0));
assert_eq!(glyph_at(&table, 6), None);
}
#[test]
fn the_table_is_two_bytes_per_cid_up_to_the_highest_one_drawn() {
let map = GidMap::from_pairs([(0, 0), (40, 1)]);
let table = cid_to_gid_table(&candidate(&[(9, 40)]), &map);
assert_eq!(table.len(), 20, "CIDs 0 through 9, two bytes each");
}
#[test]
fn a_dropped_glyph_leaves_its_cid_at_notdef() {
let map = GidMap::from_pairs([(0, 0)]);
let table = cid_to_gid_table(&candidate(&[(3, 40)]), &map);
assert_eq!(glyph_at(&table, 3), Some(0));
}
#[test]
fn setting_a_key_that_is_already_there_replaces_it_in_place() {
let dict = Dict::from_pairs([
(names::TYPE.clone(), Object::Name(Name::from("Font"))),
(names::BASE_FONT.clone(), Object::Name(Name::from("Old"))),
(names::SUBTYPE.clone(), Object::Name(Name::from("Type0"))),
]);
let out = set(
&dict,
names::BASE_FONT,
Object::Name(Name::from("ABCDEF+Old")),
);
let keys: Vec<&Name> = out.keys().collect();
assert_eq!(keys, vec![names::TYPE, names::BASE_FONT, names::SUBTYPE]);
assert_eq!(
out.name(names::BASE_FONT).map(Name::as_bytes),
Some(&b"ABCDEF+Old"[..])
);
}
#[test]
fn setting_a_key_that_is_absent_appends_it() {
let dict = Dict::from_pairs([(names::TYPE.clone(), Object::Name(Name::from("Font")))]);
let out = set(&dict, names::CID_TO_GID_MAP, Object::Ref(ObjRef::new(9, 0)));
assert_eq!(out.len(), 2);
assert_eq!(
out.reference(names::CID_TO_GID_MAP),
Some(ObjRef::new(9, 0))
);
}
#[test]
fn a_duplicated_key_collapses_to_one() {
let dict = Dict::from_pairs([
(names::BASE_FONT.clone(), Object::Name(Name::from("First"))),
(names::TYPE.clone(), Object::Name(Name::from("Font"))),
(names::BASE_FONT.clone(), Object::Name(Name::from("Second"))),
]);
let out = set(&dict, names::BASE_FONT, Object::Name(Name::from("Tagged")));
assert_eq!(out.len(), 2);
assert_eq!(
out.name(names::BASE_FONT).map(Name::as_bytes),
Some(&b"Tagged"[..])
);
}
}