1use std::collections::{BTreeMap, HashMap};
24use std::fmt::Write as _;
25
26use pdfrum_font::GlyphSource;
27use pdfrum_object::{ByteSpan, Dict, Name, ObjRef, Object, Stream};
28use sha2::{Digest as _, Sha256};
29use skrifa::instance::{LocationRef, Size};
30use skrifa::raw::TableProvider as _;
31use skrifa::{FontRef, GlyphId, MetadataProvider as _, Tag};
32
33use crate::doc::EditDoc;
34use crate::error::Error;
35use crate::font::embed::{
36 MAX_BF_ENTRIES, ProgramKind, TO_UNICODE_END, TO_UNICODE_START, add_charcode, cid_font_dict,
37 create_widths_array, load_font_desc, type0_font_dict,
38};
39use crate::font::instance::{self, FontInstance};
40use crate::font::{is_opentype_cff, subset_name, subset_tag};
41use crate::names;
42use crate::write::id::IdSource;
43
44#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct GlyphFont {
51 slot: usize,
52 font: ObjRef,
53}
54
55impl GlyphFont {
56 #[must_use]
58 pub fn object(&self) -> ObjRef {
59 self.font
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub(crate) enum Claim {
66 Mapped,
69 Taken,
72}
73
74#[derive(Debug, Clone)]
76pub(crate) struct GlyphFace {
77 program: ByteSpan,
78 index: u32,
79 instance: FontInstance,
80 font: ObjRef,
83 name: Vec<u8>,
85 gids: Vec<u16>,
87 cids: HashMap<u16, u16>,
88 widths: Vec<u32>,
90 texts: Vec<Option<String>>,
92 stale: bool,
94}
95
96impl GlyphFace {
97 pub(crate) fn cids(&mut self, gids: &[u16]) -> Result<Vec<(u16, u32)>, Error> {
105 let fresh: Vec<u16> = gids
106 .iter()
107 .copied()
108 .filter(|gid| !self.cids.contains_key(gid))
109 .collect();
110 if !fresh.is_empty() {
111 let advances = self.advances(&fresh);
112 for (gid, width) in fresh.into_iter().zip(advances) {
113 if self.cids.contains_key(&gid) {
114 continue;
115 }
116 let cid = u16::try_from(self.gids.len()).map_err(|_| Error::TooManyGlyphs)?;
117 self.gids.push(gid);
118 self.cids.insert(gid, cid);
119 self.widths.push(width);
120 self.texts.push(None);
121 self.stale = true;
122 }
123 }
124 Ok(gids
125 .iter()
126 .map(|gid| {
127 let cid = self.cids.get(gid).copied().unwrap_or(0);
128 let width = self.widths.get(usize::from(cid)).copied().unwrap_or(0);
129 (cid, width)
130 })
131 .collect())
132 }
133
134 pub(crate) fn claim(&mut self, cid: u16, text: &str) -> Claim {
136 let Some(slot) = self.texts.get_mut(usize::from(cid)) else {
137 return Claim::Taken;
138 };
139 match slot {
140 _ if text.is_empty() => Claim::Mapped,
141 Some(held) if held == text => Claim::Mapped,
142 Some(_) => Claim::Taken,
143 None => {
144 *slot = Some(text.to_owned());
145 self.stale = true;
146 Claim::Mapped
147 }
148 }
149 }
150
151 pub(crate) fn is(&self, font: &GlyphFont) -> bool {
153 self.font == font.font
154 }
155
156 fn advances(&self, gids: &[u16]) -> Vec<u32> {
160 let Ok(font) = FontRef::from_index(&self.program, self.index) else {
161 return vec![500; gids.len()];
162 };
163 let per_em = f32::from(font.head().map_or(1000, |head| head.units_per_em()).max(1));
164 let location = instance::location(&font, &self.instance);
165 let metrics = font.glyph_metrics(Size::unscaled(), LocationRef::from(&location));
166 gids.iter()
167 .map(|&gid| {
168 let advance = metrics
169 .advance_width(GlyphId::new(u32::from(gid)))
170 .unwrap_or(0.0);
171 thousandths(advance * 1000.0 / per_em)
172 })
173 .collect()
174 }
175}
176
177#[expect(
178 clippy::cast_possible_truncation,
179 clippy::cast_sign_loss,
180 reason = "an advance in thousandths of an em, clamped to what /W holds"
181)]
182fn thousandths(value: f32) -> u32 {
183 value.round().clamp(0.0, 65_535.0) as u32
184}
185
186impl EditDoc<'_> {
187 pub fn embed_glyph_font(
215 &mut self,
216 program: ByteSpan,
217 face: u32,
218 instance: FontInstance,
219 ) -> Result<GlyphFont, Error> {
220 let parsed =
221 FontRef::from_index(&program, face).map_err(|_| Error::UnrecognisedFontProgram)?;
222 if parsed.maxp().map_or(0, |maxp| maxp.num_glyphs()) == 0 {
223 return Err(Error::EmptyFontProgram);
224 }
225 let needs_instancing = parsed.table_data(Tag::new(b"CFF2")).is_some()
226 || !instance::user_values(&parsed, &instance).is_empty();
227 if needs_instancing && !cfg!(feature = "variable-fonts") {
228 return Err(Error::VariableFontsDisabled);
229 }
230 let name = postscript_name(&parsed);
231 let font = self.add(Object::Null);
232 let slot = self.glyph_faces.len();
233 self.glyph_faces.push(GlyphFace {
234 program,
235 index: face,
236 instance,
237 font,
238 name,
239 gids: vec![0],
240 cids: HashMap::from([(0, 0)]),
241 widths: vec![0],
242 texts: vec![None],
243 stale: false,
244 });
245 Ok(GlyphFont { slot, font })
246 }
247
248 pub(crate) fn glyph_face(&mut self, font: &GlyphFont) -> Option<&mut GlyphFace> {
250 self.glyph_faces
251 .get_mut(font.slot)
252 .filter(|face| face.is(font))
253 }
254}
255
256fn postscript_name(font: &FontRef<'_>) -> Vec<u8> {
258 font.localized_strings(skrifa::string::StringId::POSTSCRIPT_NAME)
259 .english_or_first()
260 .map(|name| {
261 name.chars()
262 .filter(|c| c.is_ascii_graphic() && !"[](){}<>/%#".contains(*c))
263 .collect::<String>()
264 })
265 .filter(|name| !name.is_empty())
266 .map_or_else(|| b"Untitled".to_vec(), String::into_bytes)
267}
268
269pub(crate) fn finish(doc: &mut EditDoc<'_>) -> Result<(), Error> {
276 let stale: Vec<GlyphFace> = doc
277 .glyph_faces
278 .iter()
279 .filter(|face| face.stale && face.gids.len() > 1)
280 .cloned()
281 .collect();
282 for face in stale {
283 write(doc, &face)?;
284 if let Some(held) = doc
285 .glyph_faces
286 .iter_mut()
287 .find(|held| held.font == face.font)
288 {
289 held.stale = false;
290 }
291 }
292 Ok(())
293}
294
295fn write(doc: &mut EditDoc<'_>, face: &GlyphFace) -> Result<(), Error> {
296 let program = subset_program(face)?;
297 let kind = if is_opentype_cff(&program) {
298 ProgramKind::OpenTypeCff
299 } else {
300 ProgramKind::TrueType
301 };
302 let glyphs =
303 GlyphSource::from_bytes(program.as_slice()).ok_or(Error::UnrecognisedFontProgram)?;
304 let base = instance_base_name(face);
305 let name = subset_name(&base, subset_tag(IdSource::Fixed(fingerprint(face))));
306 let descriptor = load_font_desc(doc, &name, &program, kind, &glyphs);
307 let widths: BTreeMap<u32, u32> = face
308 .widths
309 .iter()
310 .enumerate()
311 .filter_map(|(cid, width)| Some((u32::try_from(cid).ok()?, *width)))
312 .collect();
313 let w = doc.add(Object::Array(create_widths_array(&widths)));
314 let to_unicode = doc.add(Object::Stream(Box::new(Stream::new(
315 Dict::new(),
316 ByteSpan::from(to_unicode_cmap(&face.texts)),
317 ))));
318 let subtype = match kind {
319 ProgramKind::OpenTypeCff | ProgramKind::Type1 => names::CID_FONT_TYPE0.clone(),
320 ProgramKind::TrueType => names::CID_FONT_TYPE2.clone(),
321 };
322 let mut descendant = cid_font_dict(doc, &name, subtype, descriptor, w);
323 if kind == ProgramKind::TrueType {
324 descendant.push(
325 names::CID_TO_GID_MAP.clone(),
326 Object::Name(Name::from("Identity")),
327 );
328 }
329 let descendant = doc.add(Object::Dict(descendant));
330 doc.replace(
331 face.font,
332 Object::Dict(type0_font_dict(&name, descendant, to_unicode)),
333 );
334 Ok(())
335}
336
337fn instance_base_name(face: &GlyphFace) -> Vec<u8> {
341 FontRef::from_index(&face.program, face.index)
342 .ok()
343 .and_then(|font| {
344 let values = instance::user_values(&font, &face.instance);
345 crate::font::instance_name::instance_name(&font, &values)
346 })
347 .unwrap_or_else(|| face.name.clone())
348}
349
350fn subset_program(face: &GlyphFace) -> Result<Vec<u8>, Error> {
352 let mut remapper = subsetter::GlyphRemapper::new();
353 for gid in &face.gids {
354 remapper.remap(*gid);
355 }
356 subset_at_instance(face, &remapper).map_err(|error| Error::Subset(error.to_string()))
357}
358
359#[cfg(feature = "variable-fonts")]
360fn subset_at_instance(
361 face: &GlyphFace,
362 remapper: &subsetter::GlyphRemapper,
363) -> Result<Vec<u8>, subsetter::Error> {
364 let values = FontRef::from_index(&face.program, face.index)
365 .map(|font| instance::user_values(&font, &face.instance))
366 .unwrap_or_default();
367 let coordinates: Vec<(subsetter::Tag, f32)> = values
368 .iter()
369 .map(|axis| (subsetter::Tag::new(&axis.tag), axis.value))
370 .collect();
371 subsetter::subset_with_variations(&face.program, face.index, &coordinates, remapper)
372}
373
374#[cfg(not(feature = "variable-fonts"))]
375fn subset_at_instance(
376 face: &GlyphFace,
377 remapper: &subsetter::GlyphRemapper,
378) -> Result<Vec<u8>, subsetter::Error> {
379 subsetter::subset(&face.program, face.index, remapper)
380}
381
382fn fingerprint(face: &GlyphFace) -> [u8; 16] {
385 let mut hash = Sha256::new();
386 hash.update(&face.name);
387 for gid in &face.gids {
388 hash.update(gid.to_be_bytes());
389 }
390 let digest = hash.finalize();
391 let mut out = [0u8; 16];
392 out.iter_mut()
393 .zip(digest.iter())
394 .for_each(|(slot, byte)| *slot = *byte);
395 out
396}
397
398fn to_unicode_cmap(texts: &[Option<String>]) -> Vec<u8> {
401 let entries: Vec<(u32, &str)> = texts
402 .iter()
403 .enumerate()
404 .filter_map(|(cid, text)| {
405 let text = text.as_deref().filter(|text| !text.is_empty())?;
406 Some((u32::try_from(cid).ok()?, text))
407 })
408 .collect();
409 let mut buf = String::from(TO_UNICODE_START);
410 for chunk in entries.chunks(MAX_BF_ENTRIES) {
411 let _ = writeln!(buf, "{} beginbfchar", chunk.len());
412 for (cid, text) in chunk {
413 add_charcode(&mut buf, *cid);
414 buf.push_str(" <");
415 for unit in text.encode_utf16() {
416 let _ = write!(buf, "{unit:04X}");
417 }
418 buf.push_str(">\n");
419 }
420 buf.push_str("endbfchar\n");
421 }
422 buf.push_str(TO_UNICODE_END);
423 buf.into_bytes()
424}
425
426#[cfg(test)]
427mod tests {
428 use super::to_unicode_cmap;
429
430 #[test]
431 fn a_ligature_maps_to_all_its_letters() {
432 let cmap = String::from_utf8(to_unicode_cmap(&[
433 None,
434 Some("fi".into()),
435 Some("δΈ".into()),
436 ]))
437 .expect("ASCII");
438 assert!(cmap.contains("<0001> <00660069>"), "{cmap}");
439 assert!(cmap.contains("<0002> <4E00>"), "{cmap}");
440 assert!(cmap.contains("2 beginbfchar"), "notdef has no text: {cmap}");
441 }
442
443 #[test]
444 fn a_cid_without_text_is_left_out() {
445 let cmap =
446 String::from_utf8(to_unicode_cmap(&[None, None, Some(String::new())])).expect("ASCII");
447 assert!(!cmap.contains("beginbfchar"), "{cmap}");
448 }
449}