Skip to main content

pdfrum_edit/font/
glyph.rs

1//! Faces for glyph runs: fonts a layout engine has already shaped with, drawn
2//! by glyph ID rather than by character.
3//!
4//! [`EditDoc::embed_font`] turns *text* into codes through the program's own
5//! cmap, which cannot reach a glyph the cmap does not name β€” a ligature, a
6//! contextual form, a glyph two code points share. A shaper hands over glyph
7//! IDs, so a glyph font takes those directly. Each glyph is given a CID the
8//! first time it is drawn (CID 0 is `.notdef`), and the file's font objects
9//! are written from what was drawn once a drawing call returns:
10//!
11//! - the program, **subset to the drawn glyphs in CID order**, so the
12//!   subsetter's new glyph IDs *are* the CIDs and no `/CIDToGIDMap` is needed
13//!   (a CFF result is a `CIDFontType0`, where CID is GID by definition);
14//! - at the face's **variable instance**, which the subsetter bakes into the
15//!   program (feature `variable-fonts`);
16//! - `/W` from the advances the runs were positioned with, so a viewer's pen
17//!   and the layout's agree to the unit;
18//! - `/ToUnicode` from the **text each glyph was drawn for**, as the caller
19//!   gave it β€” a ligature maps to all of its letters; a glyph drawn for two
20//!   different texts keeps its first, and the other run carries
21//!   `/ActualText` instead (see [`Canvas::glyphs`](crate::Canvas::glyphs)).
22
23use 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/// A face embedded for glyph runs, ready to draw with
45/// [`Canvas::glyphs`](crate::Canvas::glyphs).
46///
47/// A handle into the session that embedded it: the font objects are written
48/// from what that session drew, each time a drawing call returns.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct GlyphFont {
51    slot: usize,
52    font: ObjRef,
53}
54
55impl GlyphFont {
56    /// The `/Type0` font dictionary a page's resources name.
57    #[must_use]
58    pub fn object(&self) -> ObjRef {
59        self.font
60    }
61}
62
63/// How a glyph's text stands in the font's `/ToUnicode`.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub(crate) enum Claim {
66    /// The map says this text for the glyph (or the text is empty and says
67    /// nothing).
68    Mapped,
69    /// The map already says something else: the run must carry the text as
70    /// `/ActualText`.
71    Taken,
72}
73
74/// One glyph font's state in a session: the face, and what has been drawn.
75#[derive(Debug, Clone)]
76pub(crate) struct GlyphFace {
77    program: ByteSpan,
78    index: u32,
79    instance: FontInstance,
80    /// The `/Type0` dictionary, reserved at embedding and written by
81    /// [`finish`].
82    font: ObjRef,
83    /// The face's PostScript name, untagged.
84    name: Vec<u8>,
85    /// The glyph each CID draws; CID 0 is `.notdef`.
86    gids: Vec<u16>,
87    cids: HashMap<u16, u16>,
88    /// Each CID's advance in thousandths of an em, at the instance.
89    widths: Vec<u32>,
90    /// Each CID's text, once a run has claimed it.
91    texts: Vec<Option<String>>,
92    /// Whether anything changed since the objects were last written.
93    stale: bool,
94}
95
96impl GlyphFace {
97    /// The CIDs of `gids` and their advances, giving a CID to each glyph not
98    /// drawn before.
99    ///
100    /// # Errors
101    ///
102    /// [`Error::TooManyGlyphs`] past 65 535 distinct glyphs, which is all an
103    /// Identity-H code can name.
104    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    /// Record that `cid` was drawn for `text`.
135    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    /// Whether `font` names this face.
152    pub(crate) fn is(&self, font: &GlyphFont) -> bool {
153        self.font == font.font
154    }
155
156    /// Advances of `gids` at the instance, in thousandths of an em, rounded:
157    /// the numbers `/W` will carry, so a run positioned with them lands where
158    /// a viewer's pen does.
159    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    /// Embed face `face` of `program` at `instance`, to draw shaped glyph
188    /// runs with.
189    ///
190    /// `program` is a TrueType or OpenType font or a collection (`.ttc`), of
191    /// which `face` picks one (0 for a single font). It is shared, not copied:
192    /// a 30 MB collection costs nothing until a save writes the subset of it
193    /// the pages drew. Nothing of the font reaches the file until a drawing
194    /// call that used it returns.
195    ///
196    /// # Errors
197    ///
198    /// [`Error::UnrecognisedFontProgram`] when `face` of `program` cannot be
199    /// read, [`Error::EmptyFontProgram`] when it declares no glyphs, and
200    /// [`Error::VariableFontsDisabled`] for a CFF2 face or a non-default
201    /// instance without the `variable-fonts` feature.
202    ///
203    /// ```
204    /// use pdfrum_edit::{EditDoc, FontInstance, Size, blank_document};
205    /// use pdfrum_object::ByteSpan;
206    ///
207    /// let base = blank_document(&[Size::new(200.0, 100.0)])?;
208    /// let mut edit = EditDoc::new(&base);
209    /// let program = ByteSpan::from(include_bytes!("../../tests/files/tiny.ttf").to_vec());
210    /// let font = edit.embed_glyph_font(program, 0, FontInstance::Default)?;
211    /// assert_ne!(font.object().num, 0);
212    /// # Ok::<(), pdfrum_edit::Error>(())
213    /// ```
214    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    /// The state of the glyph font `font` names, if this session embedded it.
249    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
256/// The face's PostScript name (`name` ID 6), or `Untitled`.
257fn 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
269/// Write every glyph font that changed: subset program, descriptor, `/W`,
270/// `/ToUnicode` and the descendant, under the `/Type0` reserved for it.
271///
272/// # Errors
273///
274/// [`Error::Subset`] when the subsetter refuses the face.
275pub(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
337/// The face's own name, or the instance's when the subset is not the
338/// default one: `/BaseFont` and `/FontName` then say the weight the glyphs
339/// were cut at, not the default instance's.
340fn 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
350/// The program subset to the drawn glyphs in CID order, at the instance.
351fn 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
382/// Sixteen bytes that change whenever the subset does: the name and the
383/// drawn glyphs, so a fixed input gives a fixed subset tag.
384fn 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
398/// The `/ToUnicode` CMap: one `bfchar` per CID with text, the text as
399/// UTF-16BE of any length (a ligature's letters, a grapheme's marks).
400fn 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}