Skip to main content

pdfops/
font.rs

1//! Fonts for text that pdfops draws itself (stamps and form field appearances).
2//!
3//! Latin-1 text uses the built-in Helvetica, which costs no space. Anything
4//! else needs real glyphs, so a font file is subsetted to the characters used
5//! and embedded as a composite font with a ToUnicode map. Where no one font has
6//! all the characters, several share the text between them.
7
8use std::collections::{BTreeMap, HashMap, HashSet};
9use std::ops::Range;
10use std::path::Path;
11
12use anyhow::{Context, Result, anyhow, bail};
13use lopdf::{Dictionary, Document, Object, ObjectId, Stream};
14
15/// Helvetica advance widths for ASCII 32..=126, in 1/1000 em (Adobe AFM).
16const HELVETICA: [u16; 95] = [
17    278, 278, 355, 556, 556, 889, 667, 191, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
18    556, 556, 556, 556, 556, 556, 556, 278, 278, 584, 584, 584, 556, 1015, 667, 667, 722, 722, 667,
19    611, 778, 722, 278, 500, 667, 556, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
20    667, 611, 278, 278, 278, 469, 556, 333, 556, 556, 500, 556, 556, 278, 556, 556, 222, 222, 500,
21    222, 833, 556, 556, 556, 556, 333, 500, 278, 556, 500, 722, 500, 500, 500, 334, 260, 334, 584,
22];
23
24/// Width of WinAnsi `bytes` set in Helvetica at `size` points.
25pub fn helvetica_width(bytes: &[u8], size: f64) -> f64 {
26    let units: u32 = bytes
27        .iter()
28        .map(|&b| {
29            HELVETICA
30                .get((b as usize).wrapping_sub(32))
31                .copied()
32                .unwrap_or(556) as u32
33        })
34        .sum();
35    units as f64 * size / 1000.0
36}
37
38/// Encodes text for a WinAnsi font, if every character is Latin-1.
39pub fn winansi(text: &str) -> Option<Vec<u8>> {
40    text.chars()
41        .map(|c| match c as u32 {
42            0x20..=0x7e | 0xa0..=0xff => Some(c as u8),
43            _ => None,
44        })
45        .collect()
46}
47
48/// A PDF literal string, escaped.
49pub fn literal(bytes: &[u8]) -> String {
50    let mut s = String::with_capacity(bytes.len() + 2);
51    s.push('(');
52    for &b in bytes {
53        match b {
54            b'(' | b')' | b'\\' => {
55                s.push('\\');
56                s.push(b as char);
57            }
58            0x20..=0x7e => s.push(b as char),
59            _ => s.push_str(&format!("\\{b:03o}")),
60        }
61    }
62    s.push(')');
63    s
64}
65
66/// A font object in a document, able to encode and measure text for it.
67pub struct TextFont {
68    pub id: ObjectId,
69    /// The embedded font's data for laying text out; `None` for a built-in font.
70    embedded: Option<Box<Embedded>>,
71    /// Which built-in metrics apply when nothing is embedded.
72    builtin: Style,
73    /// The fonts that stand in where this one has no glyph, in the order they are tried.
74    stand_ins: Vec<TextFont>,
75}
76
77/// Shares `text` out among `fonts` fonts, of which `has` tells whether one can draw a
78/// character. The pieces come in the order they are drawn, from left to right.
79///
80/// A character goes to the first font that has it. Within a word, and for what is
81/// not a letter, the font in use carries on while it can: an accent stays with its
82/// letter, a comma with the word before it.
83fn share(
84    text: &str,
85    fonts: usize,
86    has: &dyn Fn(usize, char) -> bool,
87) -> Vec<(usize, Range<usize>)> {
88    if fonts <= 1 {
89        return vec![(0, 0..text.len())];
90    }
91    let mut out = Vec::new();
92    let bidi = unicode_bidi::BidiInfo::new(text, None);
93    for paragraph in &bidi.paragraphs {
94        let (levels, runs) = bidi.visual_runs(paragraph, paragraph.range.clone());
95        for run in runs {
96            let mut pieces: Vec<(usize, Range<usize>)> = Vec::new();
97            let (mut current, mut in_word) = (0, false);
98            for (at, c) in text[run.clone()].char_indices() {
99                let at = run.start + at;
100                let carries_on = has(current, c) && (in_word || !c.is_alphabetic());
101                if !carries_on {
102                    current = (0..fonts).find(|&k| has(k, c)).unwrap_or(current);
103                }
104                in_word = c.is_alphabetic();
105                match pieces.last_mut() {
106                    Some((font, range)) if *font == current => range.end = at + c.len_utf8(),
107                    _ => pieces.push((current, at..at + c.len_utf8())),
108                }
109            }
110            if levels[run.start].is_rtl() {
111                pieces.reverse();
112            }
113            out.extend(pieces);
114        }
115    }
116    out
117}
118
119/// What it takes to lay text out in an embedded font.
120struct Embedded {
121    data: Vec<u8>,
122    index: u32,
123    /// Font units to thousandths of an em.
124    scale: f64,
125    /// Glyph id in the font file to (glyph id in the subset, its advance in 1/1000 em).
126    subset: HashMap<u16, (u16, f64)>,
127    /// The glyph a character has before any shaping, as a glyph id in the font file.
128    nominal: HashMap<char, u16>,
129    /// What each glyph of the subset reads as in the font's map back to text.
130    reads: HashMap<u16, String>,
131}
132
133/// One glyph of laid out text, in the order it is drawn. Distances are in 1/1000 em.
134struct Shaped {
135    /// Glyph id in the subset.
136    glyph: u16,
137    /// How far the pen moves on, which kerning and shaping may have changed.
138    advance: f64,
139    /// The advance the font states for the glyph alone, which a viewer applies.
140    natural: f64,
141    /// Where the glyph sits relative to the pen: marks are placed this way.
142    dx: f64,
143    dy: f64,
144    /// On the first glyph of a cluster that the map back to text may not give back as
145    /// it was written: the text, and how many glyphs draw it. A glyph has one reading
146    /// in that map, while shaping reorders glyphs, splits a character over several,
147    /// stacks them and uses one glyph for different characters.
148    stated: Option<(String, usize)>,
149}
150
151/// A glyph as the shaper returns it, with the text it stands for.
152struct RawGlyph {
153    /// Glyph id in the font file.
154    glyph: u16,
155    /// Byte range in the text of the cluster the glyph belongs to.
156    cluster: std::ops::Range<usize>,
157    advance: f64,
158    dx: f64,
159    dy: f64,
160}
161
162/// Lays `text` out in `face`: bidirectional reordering, then shaping run by run.
163///
164/// Glyphs come back left to right as they are drawn, whatever the direction of the
165/// script, with ligatures, contextual forms, kerning and mark positions applied.
166fn shape(face: &rustybuzz::Face<'_>, text: &str) -> Vec<RawGlyph> {
167    let mut out = Vec::new();
168    let bidi = unicode_bidi::BidiInfo::new(text, None);
169    for paragraph in &bidi.paragraphs {
170        let (levels, runs) = bidi.visual_runs(paragraph, paragraph.range.clone());
171        for run in runs {
172            let piece = &text[run.clone()];
173            let mut buffer = rustybuzz::UnicodeBuffer::new();
174            buffer.push_str(piece);
175            buffer.set_direction(if levels[run.start].is_rtl() {
176                rustybuzz::Direction::RightToLeft
177            } else {
178                rustybuzz::Direction::LeftToRight
179            });
180            // Script and language follow from the text itself.
181            buffer.guess_segment_properties();
182            let shaped = rustybuzz::shape(face, &[], buffer);
183            let mut starts: Vec<usize> = shaped
184                .glyph_infos()
185                .iter()
186                .map(|g| g.cluster as usize)
187                .collect();
188            starts.sort_unstable();
189            starts.dedup();
190            for (info, position) in shaped.glyph_infos().iter().zip(shaped.glyph_positions()) {
191                let from = info.cluster as usize;
192                let to = starts
193                    .iter()
194                    .copied()
195                    .find(|&s| s > from)
196                    .unwrap_or(piece.len());
197                // Line breaks and the like have no place in a line of text.
198                if piece[from..to].chars().all(char::is_control) {
199                    continue;
200                }
201                out.push(RawGlyph {
202                    glyph: info.glyph_id as u16,
203                    cluster: run.start + from..run.start + to,
204                    advance: position.x_advance as f64,
205                    dx: position.x_offset as f64,
206                    dy: position.y_offset as f64,
207                });
208            }
209        }
210    }
211    out
212}
213
214impl Embedded {
215    /// `text` as the glyphs of the subset that draw it.
216    ///
217    /// The subset holds the glyphs that shaping the font's sample text selected. Where
218    /// other text needs a glyph beyond those, the characters concerned are drawn with
219    /// their plain glyphs instead, unshaped but legible.
220    fn layout(&self, text: &str) -> Vec<Shaped> {
221        let Some(face) = rustybuzz::Face::from_slice(&self.data, self.index) else {
222            return Vec::new();
223        };
224        let raw = shape(&face, text);
225        let mut out = Vec::with_capacity(raw.len());
226        let mut i = 0;
227        while i < raw.len() {
228            let mut j = i;
229            while j < raw.len() && raw[j].cluster == raw[i].cluster {
230                j += 1;
231            }
232            let cluster = &raw[i..j];
233            let from = out.len();
234            if cluster.iter().all(|g| self.subset.contains_key(&g.glyph)) {
235                out.extend(cluster.iter().map(|g| {
236                    let (glyph, natural) = self.subset[&g.glyph];
237                    Shaped {
238                        glyph,
239                        advance: g.advance * self.scale,
240                        natural,
241                        dx: g.dx * self.scale,
242                        dy: g.dy * self.scale,
243                        stated: None,
244                    }
245                }));
246            } else {
247                for c in text[cluster[0].cluster.clone()].chars() {
248                    let plain = self.nominal.get(&c).and_then(|g| self.subset.get(g));
249                    let (glyph, natural) = plain.copied().unwrap_or((0, 0.0));
250                    out.push(Shaped {
251                        glyph,
252                        advance: natural,
253                        natural,
254                        dx: 0.0,
255                        dy: 0.0,
256                        stated: None,
257                    });
258                }
259            }
260            let written = &text[cluster[0].cluster.clone()];
261            if out.len() - from > 1 {
262                // Within a cluster the order of drawing is free, and a reader takes text
263                // to begin where its first glyph does and to end where its last one
264                // does, by the width the font states for it. So the glyphs that move the
265                // pen on come first, as they stand, and carry the text of the cluster:
266                // they begin and end where it does. The marks set over, under or into
267                // them follow and are stated to read as nothing. Drawn anywhere else, a
268                // mark would begin or end the cluster where it stands itself, and what
269                // is next to it would seem joined on, or a word apart.
270                let total: f64 = out[from..].iter().map(|g| g.advance).sum();
271                let mut pen = 0.0;
272                let placed: Vec<(f64, bool, Shaped)> = out
273                    .drain(from..)
274                    .map(|g| {
275                        let at = pen + g.dx;
276                        pen += g.advance;
277                        (at, g.advance > 0.5, g)
278                    })
279                    .collect();
280                let moving = placed.iter().filter(|g| g.1).count();
281                // Nothing moves the pen in a cluster of marks alone: they carry its text.
282                let carried = if moving == 0 { placed.len() } else { moving };
283                let (first, rest): (Vec<_>, Vec<_>) =
284                    placed.into_iter().partition(|g| g.1 || moving == 0);
285                let (mut pen, last) = (0.0, first.len() + rest.len() - 1);
286                for (k, (at, _, mut glyph)) in first.into_iter().chain(rest).enumerate() {
287                    glyph.dx = at - pen;
288                    pen = at + glyph.natural;
289                    let rest = if k == last { total - pen } else { 0.0 };
290                    glyph.advance = glyph.dx + glyph.natural + rest;
291                    glyph.stated = match k {
292                        0 => Some((written.to_string(), carried)),
293                        k if k == carried => Some((String::new(), last + 1 - carried)),
294                        _ => None,
295                    };
296                    out.push(glyph);
297                }
298            } else if let Some(only) = out.get_mut(from) {
299                // One glyph that the font's map reads as something else than was written.
300                let read = self.reads.get(&only.glyph).map_or("", String::as_str);
301                if read != written {
302                    only.stated = Some((written.to_string(), 1));
303                }
304            }
305            i = j;
306        }
307        out
308    }
309}
310
311/// The variant of a typeface.
312#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
313pub struct Style {
314    pub bold: bool,
315    pub italic: bool,
316    pub mono: bool,
317}
318
319/// Helvetica-Bold advance widths for ASCII 32..=126, in 1/1000 em (Adobe AFM).
320const HELVETICA_BOLD: [u16; 95] = [
321    278, 333, 474, 556, 556, 889, 722, 238, 333, 333, 389, 584, 278, 333, 278, 278, 556, 556, 556,
322    556, 556, 556, 556, 556, 556, 556, 333, 333, 584, 584, 584, 611, 975, 722, 722, 722, 722, 667,
323    611, 778, 722, 278, 556, 722, 611, 833, 722, 778, 667, 778, 722, 667, 611, 722, 667, 944, 667,
324    667, 611, 333, 278, 333, 584, 556, 333, 556, 611, 556, 611, 556, 333, 611, 611, 278, 278, 556,
325    278, 889, 611, 611, 611, 611, 389, 556, 333, 611, 556, 778, 556, 556, 500, 389, 280, 389, 584,
326];
327
328impl TextFont {
329    /// Adds a font able to draw `text` to the document.
330    ///
331    /// With `file`, that font is embedded. Otherwise Helvetica is used when it
332    /// suffices, and an installed font covering the text is embedded when not.
333    /// `text` should be what will be drawn, not just its characters: an embedded
334    /// font carries the ligatures and joined forms that this text calls for.
335    pub fn new(doc: &mut Document, text: &str, file: Option<&Path>) -> Result<TextFont> {
336        let mut wanted: Vec<char> = text.chars().filter(|c| !c.is_control()).collect();
337        if file.is_none() && winansi(&wanted.iter().collect::<String>()).is_some() {
338            return Ok(TextFont {
339                id: doc.add_object(helvetica()),
340                embedded: None,
341                builtin: Style::default(),
342                stand_ins: Vec::new(),
343            });
344        }
345        wanted.sort_unstable();
346        wanted.dedup();
347        let fonts = match file {
348            Some(path) => {
349                let data = std::fs::read(path)
350                    .with_context(|| format!("cannot read font {}", path.display()))?;
351                let missing = missing(&data, 0, &wanted)
352                    .ok_or_else(|| anyhow!("{} is not a usable font", path.display()))?;
353                // What the given font lacks, installed ones draw.
354                let mut fonts = vec![(data, 0)];
355                if !missing.is_empty() {
356                    fonts.extend(system_fonts(&missing, Style::default()).with_context(|| {
357                        format!(
358                            "font {} has no glyph for: {}",
359                            path.display(),
360                            missing.iter().collect::<String>()
361                        )
362                    })?);
363                }
364                fonts
365            }
366            None => system_fonts(&wanted, Style::default())?,
367        };
368        embed_all(doc, fonts, &wanted, text)
369    }
370
371    /// Adds a font of the given style for `text`: a built-in one (Helvetica or Courier)
372    /// when the text is Latin-1, otherwise an installed font of that style, embedded.
373    pub fn styled(doc: &mut Document, text: &str, style: Style) -> Result<TextFont> {
374        let mut wanted: Vec<char> = text.chars().filter(|c| !c.is_control()).collect();
375        if winansi(&wanted.iter().collect::<String>()).is_some() {
376            let family = if style.mono { "Courier" } else { "Helvetica" };
377            let variant = match (style.bold, style.italic) {
378                (false, false) => "",
379                (true, false) => "-Bold",
380                (false, true) => "-Oblique",
381                (true, true) => "-BoldOblique",
382            };
383            let mut font = helvetica();
384            font.set(
385                "BaseFont",
386                Object::Name(format!("{family}{variant}").into_bytes()),
387            );
388            return Ok(TextFont {
389                id: doc.add_object(font),
390                embedded: None,
391                builtin: style,
392                stand_ins: Vec::new(),
393            });
394        }
395        wanted.sort_unstable();
396        wanted.dedup();
397        let fonts = system_fonts(&wanted, style)?;
398        embed_all(doc, fonts, &wanted, text)
399    }
400
401    /// The font objects that draw this font's text: its own, then those standing in
402    /// for it. `show_named` wants a resource name for each.
403    pub fn ids(&self) -> Vec<ObjectId> {
404        let mut ids = vec![self.id];
405        ids.extend(self.stand_ins.iter().map(|font| font.id));
406        ids
407    }
408
409    fn has(&self, c: char) -> bool {
410        match &self.embedded {
411            Some(embedded) => embedded.nominal.contains_key(&c),
412            None => winansi(c.encode_utf8(&mut [0; 4])).is_some(),
413        }
414    }
415
416    /// `text` shared out among this font and those standing in for it: for each piece,
417    /// in the order they are drawn, which font of `ids` draws it. Each font is to be
418    /// used through `show`, `elements` and `own_width`, which speak for it alone.
419    pub fn pieces<'a>(&'a self, text: &'a str) -> Vec<(usize, &'a TextFont, &'a str)> {
420        let font = |k: usize| match k {
421            0 => self,
422            k => &self.stand_ins[k - 1],
423        };
424        share(text, 1 + self.stand_ins.len(), &|k, c| font(k).has(c))
425            .into_iter()
426            .map(|(k, range)| (k, font(k), &text[range]))
427            .collect()
428    }
429
430    /// The operators that show `text` at `size` points, each piece of it in the font
431    /// that has its glyphs. `names` are the resource names of `ids`, in that order.
432    pub fn show_named(&self, names: &[String], text: &str, size: f64) -> String {
433        self.pieces(text)
434            .into_iter()
435            .map(|(k, font, piece)| {
436                format!("/{} {size:.2} Tf\n{}", names[k], font.show(piece, size))
437            })
438            .collect::<Vec<_>>()
439            .join("\n")
440    }
441
442    /// The elements of the `TJ` arrays showing `text` in this font alone: strings of
443    /// glyphs, and between them the movements that kerning and shaping ask for. Each
444    /// array comes with the text to state around it, where its glyphs would not read
445    /// back as what was written; see `Shaped::stated`.
446    ///
447    /// A `TJ` array cannot raise or lower a glyph, so marks that the font places above
448    /// or below their base stay on the baseline here; `show` places them.
449    pub fn elements(&self, text: &str) -> Vec<(Option<String>, Vec<Object>)> {
450        let Some(embedded) = &self.embedded else {
451            return vec![(
452                None,
453                vec![Object::String(
454                    winansi(text).unwrap_or_default(),
455                    lopdf::StringFormat::Literal,
456                )],
457            )];
458        };
459        let mut out: Vec<(Option<String>, Vec<Object>)> = vec![(None, Vec::new())];
460        let mut run: Vec<u8> = Vec::new();
461        // How far the pen is from where the next glyph belongs.
462        let mut owed = 0.0f64;
463        let mut stating = 0usize;
464        let close = |run: &mut Vec<u8>, out: &mut Vec<(Option<String>, Vec<Object>)>| {
465            if !run.is_empty() {
466                let glyphs = Object::String(std::mem::take(run), lopdf::StringFormat::Hexadecimal);
467                out.last_mut().expect("never empty").1.push(glyphs);
468            }
469        };
470        for glyph in embedded.layout(text) {
471            if let Some((written, glyphs)) = glyph.stated {
472                close(&mut run, &mut out);
473                out.push((Some(written), Vec::new()));
474                stating = glyphs;
475            }
476            owed += glyph.dx;
477            if owed.abs() >= 0.5 {
478                close(&mut run, &mut out);
479                // A positive number in the array moves the pen back.
480                let back = Object::Real(-owed as f32);
481                out.last_mut().expect("never empty").1.push(back);
482                owed = 0.0;
483            }
484            run.extend(glyph.glyph.to_be_bytes());
485            owed += glyph.advance - glyph.dx - glyph.natural;
486            if stating > 0 {
487                stating -= 1;
488                if stating == 0 {
489                    close(&mut run, &mut out);
490                    out.push((None, Vec::new()));
491                }
492            }
493        }
494        close(&mut run, &mut out);
495        if owed.abs() >= 0.5 {
496            let back = Object::Real(-owed as f32);
497            out.last_mut().expect("never empty").1.push(back);
498        }
499        out.retain(|(_, shown)| !shown.is_empty());
500        out
501    }
502
503    /// The operators that show `text` at `size` points in this font alone: one `Tj` for
504    /// plain text, a `TJ` with kerning where the font has any, and changes of text rise
505    /// around marks that sit above or below their base.
506    pub fn show(&self, text: &str, size: f64) -> String {
507        let Some(embedded) = &self.embedded else {
508            return format!("{} Tj", literal(&winansi(text).unwrap_or_default()));
509        };
510        let hex = |bytes: &[u8]| -> String { bytes.iter().map(|b| format!("{b:02X}")).collect() };
511        let mut out: Vec<String> = Vec::new();
512        // The array being built: finished elements, and the glyphs of the current string.
513        let mut array: Vec<String> = Vec::new();
514        let mut run: Vec<u8> = Vec::new();
515        let mut owed = 0.0f64;
516        let mut rise = 0.0f64;
517        let flush = |array: &mut Vec<String>, run: &mut Vec<u8>, out: &mut Vec<String>| {
518            if !run.is_empty() {
519                array.push(format!("<{}>", hex(run)));
520                run.clear();
521            }
522            match array.as_slice() {
523                [] => {}
524                [only] if only.starts_with('<') => out.push(format!("{only} Tj")),
525                many => out.push(format!("[{}] TJ", many.join(" "))),
526            }
527            array.clear();
528        };
529        // How many glyphs are still to come in the cluster whose text is stated.
530        let mut stating = 0usize;
531        for glyph in embedded.layout(text) {
532            if let Some((written, glyphs)) = &glyph.stated {
533                flush(&mut array, &mut run, &mut out);
534                let utf16: String = written
535                    .encode_utf16()
536                    .map(|unit| format!("{unit:04X}"))
537                    .collect();
538                out.push(format!("/Span <</ActualText <FEFF{utf16}>>> BDC"));
539                stating = *glyphs;
540            }
541            if (glyph.dy - rise).abs() >= 0.5 {
542                // The movement owed so far belongs before the rise changes.
543                if owed.abs() >= 0.5 {
544                    if !run.is_empty() {
545                        array.push(format!("<{}>", hex(&run)));
546                        run.clear();
547                    }
548                    array.push(format!("{:.0}", -owed));
549                    owed = 0.0;
550                }
551                flush(&mut array, &mut run, &mut out);
552                rise = glyph.dy;
553                out.push(format!("{:.2} Ts", rise * size / 1000.0));
554            }
555            owed += glyph.dx;
556            if owed.abs() >= 0.5 {
557                if !run.is_empty() {
558                    array.push(format!("<{}>", hex(&run)));
559                    run.clear();
560                }
561                array.push(format!("{:.0}", -owed));
562                owed = 0.0;
563            }
564            run.extend(glyph.glyph.to_be_bytes());
565            owed += glyph.advance - glyph.dx - glyph.natural;
566            if stating > 0 {
567                stating -= 1;
568                if stating == 0 {
569                    flush(&mut array, &mut run, &mut out);
570                    out.push("EMC".to_string());
571                }
572            }
573        }
574        if owed.abs() >= 0.5 {
575            if !run.is_empty() {
576                array.push(format!("<{}>", hex(&run)));
577                run.clear();
578            }
579            array.push(format!("{:.0}", -owed));
580        }
581        flush(&mut array, &mut run, &mut out);
582        if rise != 0.0 {
583            out.push("0 Ts".to_string());
584        }
585        if out.is_empty() {
586            return "<> Tj".to_string();
587        }
588        out.join("\n")
589    }
590
591    /// Whether this is an embedded font rather than the built-in Helvetica.
592    pub fn is_embedded(&self) -> bool {
593        self.embedded.is_some()
594    }
595
596    /// How wide `text` is at `size` points, each piece of it in the font that draws it.
597    pub fn width(&self, text: &str, size: f64) -> f64 {
598        if self.stand_ins.is_empty() {
599            return self.own_width(text, size);
600        }
601        self.pieces(text)
602            .into_iter()
603            .map(|(_, font, piece)| font.own_width(piece, size))
604            .sum()
605    }
606
607    /// How wide `text` is at `size` points in this font alone.
608    pub fn own_width(&self, text: &str, size: f64) -> f64 {
609        match &self.embedded {
610            None => {
611                let bytes = winansi(text).unwrap_or_default();
612                if self.builtin.mono {
613                    // Every Courier glyph is 600 units wide.
614                    bytes.len() as f64 * 0.6 * size
615                } else if self.builtin.bold {
616                    let units: u32 = bytes
617                        .iter()
618                        .map(|&b| {
619                            HELVETICA_BOLD
620                                .get((b as usize).wrapping_sub(32))
621                                .copied()
622                                .unwrap_or(556) as u32
623                        })
624                        .sum();
625                    units as f64 * size / 1000.0
626                } else {
627                    helvetica_width(&bytes, size)
628                }
629            }
630            Some(embedded) => {
631                embedded.layout(text).iter().map(|g| g.advance).sum::<f64>() * size / 1000.0
632            }
633        }
634    }
635}
636
637pub fn helvetica() -> Dictionary {
638    let mut font = Dictionary::new();
639    font.set("Type", Object::Name(b"Font".to_vec()));
640    font.set("Subtype", Object::Name(b"Type1".to_vec()));
641    font.set("BaseFont", Object::Name(b"Helvetica".to_vec()));
642    font.set("Encoding", Object::Name(b"WinAnsiEncoding".to_vec()));
643    font
644}
645
646/// Characters of `wanted` the face cannot draw, or `None` if it does not parse.
647fn missing(data: &[u8], index: u32, wanted: &[char]) -> Option<Vec<char>> {
648    let face = ttf_parser::Face::parse(data, index).ok()?;
649    Some(
650        wanted
651            .iter()
652            .copied()
653            .filter(|&c| face.glyph_index(c).is_none())
654            .collect(),
655    )
656}
657
658/// Where common distributions and systems keep a broad-coverage sans-serif.
659const USUAL_FONTS: [&str; 9] = [
660    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
661    "/usr/share/fonts/TTF/DejaVuSans.ttf",
662    "/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf",
663    "/usr/share/fonts/noto/NotoSans-Regular.ttf",
664    "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
665    "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
666    "/usr/share/fonts/liberation/LiberationSans-Regular.ttf",
667    "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
668    "C:\\Windows\\Fonts\\arial.ttf",
669];
670
671/// Where the usual fonts would be in the given style. The usual families name their
672/// variants predictably, which avoids indexing all fonts.
673fn usual_fonts(style: Style) -> Vec<std::path::PathBuf> {
674    if style == Style::default() {
675        return USUAL_FONTS.iter().map(Into::into).collect();
676    }
677    let variant = match (style.bold, style.italic) {
678        (false, false) => ["", "-Regular"],
679        (true, false) => ["-Bold", "-Bold"],
680        (false, true) => ["-Oblique", "-Italic"],
681        (true, true) => ["-BoldOblique", "-BoldItalic"],
682    };
683    let mut out = Vec::new();
684    for path in USUAL_FONTS {
685        let path = Path::new(path);
686        let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
687            continue;
688        };
689        let family = match (style.mono, stem.trim_end_matches("-Regular")) {
690            (true, "LiberationSans") => "LiberationMono".to_string(),
691            // DejaVuSans has DejaVuSansMono, NotoSans has NotoSansMono.
692            (true, family) => format!("{family}Mono"),
693            (false, family) => family.to_string(),
694        };
695        // Windows names its Arial variants by letter.
696        let short = match (stem, style.bold, style.italic, style.mono) {
697            ("arial", true, false, false) => Some("arialbd"),
698            ("arial", false, true, false) => Some("ariali"),
699            ("arial", true, true, false) => Some("arialbi"),
700            _ => None,
701        };
702        out.extend(short.map(|name| path.with_file_name(format!("{name}.ttf"))));
703        out.extend(
704            variant
705                .iter()
706                .map(|suffix| path.with_file_name(format!("{family}{suffix}.ttf"))),
707        );
708    }
709    out
710}
711
712/// Finds an installed font of the given style covering `wanted`.
713///
714/// Falls back to any covering font: wrong emphasis beats missing letters.
715fn styled_system_font(wanted: &[char], style: Style) -> Result<(Vec<u8>, u32)> {
716    if style == Style::default() {
717        return system_font(wanted);
718    }
719    let covers =
720        |data: &[u8], index: u32| missing(data, index, wanted).is_some_and(|m| m.is_empty());
721    for candidate in usual_fonts(style) {
722        if let Ok(data) = std::fs::read(&candidate)
723            && covers(&data, 0)
724        {
725            return Ok((data, 0));
726        }
727    }
728    let mut db = fontdb::Database::new();
729    db.load_system_fonts();
730    let query = fontdb::Query {
731        families: &[if style.mono {
732            fontdb::Family::Monospace
733        } else {
734            fontdb::Family::SansSerif
735        }],
736        weight: if style.bold {
737            fontdb::Weight::BOLD
738        } else {
739            fontdb::Weight::NORMAL
740        },
741        style: if style.italic {
742            fontdb::Style::Italic
743        } else {
744            fontdb::Style::Normal
745        },
746        ..Default::default()
747    };
748    let found = db
749        .query(&query)
750        .and_then(|id| {
751            db.with_face_data(id, |data, index| {
752                covers(data, index).then(|| (data.to_vec(), index))
753            })
754        })
755        .flatten();
756    match found {
757        Some(hit) => Ok(hit),
758        None => system_font(wanted),
759    }
760}
761
762/// Installed fonts in the order they are preferred: a regular sans-serif first, then
763/// plain upright text faces, so that a stamp does not come out bold, italic or monospaced.
764fn by_preference(db: &fontdb::Database) -> Vec<&fontdb::FaceInfo> {
765    let preferred = db.query(&fontdb::Query {
766        families: &[fontdb::Family::SansSerif],
767        ..Default::default()
768    });
769    let mut faces: Vec<&fontdb::FaceInfo> = db.faces().collect();
770    faces.sort_by_key(|f| {
771        (
772            Some(f.id) != preferred,
773            f.style != fontdb::Style::Normal,
774            f.weight != fontdb::Weight::NORMAL,
775            f.stretch != fontdb::Stretch::Normal,
776            f.monospaced,
777            f.post_script_name.clone(),
778        )
779    });
780    faces
781}
782
783/// Finds installed fonts that between them cover `wanted`.
784///
785/// A usual font of the given style that has everything is the whole answer. One that
786/// has the letters of some of the text draws those, and others stand in for the rest:
787/// Latin next to Chinese stays in the face a reader expects, not in the Latin of a
788/// Chinese font. Failing both, any one font that has everything will do, and then as
789/// few as it takes, each chosen for covering the most of what is still missing.
790fn system_fonts(wanted: &[char], style: Style) -> Result<Vec<(Vec<u8>, u32)>> {
791    let mut usual: Option<(usize, Vec<u8>)> = None;
792    for path in usual_fonts(style) {
793        let Ok(data) = std::fs::read(&path) else {
794            continue;
795        };
796        let Some(missing) = missing(&data, 0, wanted) else {
797            continue;
798        };
799        if missing.is_empty() {
800            return Ok(vec![(data, 0)]);
801        }
802        let letters = wanted
803            .iter()
804            .filter(|c| c.is_alphabetic() && !missing.contains(c))
805            .count();
806        if letters > usual.as_ref().map_or(0, |u| u.0) {
807            usual = Some((letters, data));
808        }
809    }
810    let mut out = Vec::new();
811    let mut left = wanted.to_vec();
812    match usual {
813        Some((_, data)) => {
814            left = missing(&data, 0, wanted).unwrap_or_default();
815            out.push((data, 0));
816        }
817        None => {
818            if let Ok(one) = styled_system_font(wanted, style) {
819                return Ok(vec![one]);
820            }
821        }
822    }
823    let mut db = fontdb::Database::new();
824    db.load_system_fonts();
825    let faces = by_preference(&db);
826    while !left.is_empty() {
827        let mut best: Option<(usize, fontdb::ID)> = None;
828        for face in &faces {
829            let covered = db
830                .with_face_data(face.id, |data, index| missing(data, index, &left))
831                .flatten()
832                .map_or(0, |missing| left.len() - missing.len());
833            if covered > best.map_or(0, |b| b.0) {
834                best = Some((covered, face.id));
835                if covered == left.len() {
836                    break;
837                }
838            }
839        }
840        let found =
841            best.and_then(|(_, id)| db.with_face_data(id, |data, index| (data.to_vec(), index)));
842        let Some((data, index)) = found else {
843            bail!(
844                "no installed font can draw '{}'; pass a font file that can",
845                left.iter().collect::<String>()
846            );
847        };
848        left = missing(&data, index, &left).unwrap_or_default();
849        out.push((data, index));
850    }
851    Ok(out)
852}
853
854/// Embeds fonts that between them draw `text`: the first as the font itself, the
855/// others standing in where it has no glyph. Each carries what it is going to draw.
856fn embed_all(
857    doc: &mut Document,
858    mut fonts: Vec<(Vec<u8>, u32)>,
859    wanted: &[char],
860    text: &str,
861) -> Result<TextFont> {
862    if fonts.len() == 1 {
863        let (data, index) = fonts.remove(0);
864        return embed(doc, data, index, wanted, text);
865    }
866    let has: Vec<HashSet<char>> = fonts
867        .iter()
868        .map(|(data, index)| {
869            let missing = missing(data, *index, wanted).unwrap_or_else(|| wanted.to_vec());
870            wanted
871                .iter()
872                .copied()
873                .filter(|c| !missing.contains(c))
874                .collect()
875        })
876        .collect();
877    // What each font draws of the text, a piece to the line: shaping never reaches
878    // from one piece into the next.
879    let mut drawn = vec![String::new(); fonts.len()];
880    for line in text.split(char::is_control) {
881        for (k, range) in share(line, fonts.len(), &|k, c| has[k].contains(&c)) {
882            drawn[k] += &line[range];
883            drawn[k].push('\n');
884        }
885    }
886    let mut embedded = Vec::new();
887    for (k, ((data, index), drawn)) in fonts.into_iter().zip(drawn).enumerate() {
888        let mut chars: Vec<char> = drawn.chars().filter(|c| !c.is_control()).collect();
889        chars.sort_unstable();
890        chars.dedup();
891        if k == 0 || !chars.is_empty() {
892            embedded.push(embed(doc, data, index, &chars, &drawn)?);
893        }
894    }
895    let mut font = embedded.remove(0);
896    font.stand_ins = embedded;
897    Ok(font)
898}
899
900/// Finds an installed font covering `wanted`, preferring a regular sans-serif.
901fn system_font(wanted: &[char]) -> Result<(Vec<u8>, u32)> {
902    // Indexing every installed font takes seconds on a cold cache, so the usual
903    // candidates are tried first; the full search below remains the fallback.
904    for path in USUAL_FONTS {
905        if let Ok(data) = std::fs::read(path)
906            && missing(&data, 0, wanted).is_some_and(|m| m.is_empty())
907        {
908            return Ok((data, 0));
909        }
910    }
911    let mut db = fontdb::Database::new();
912    db.load_system_fonts();
913    for face in by_preference(&db) {
914        let found = db.with_face_data(face.id, |data, index| {
915            missing(data, index, wanted)
916                .is_some_and(|m| m.is_empty())
917                .then(|| (data.to_vec(), index))
918        });
919        if let Some(Some(hit)) = found {
920            return Ok(hit);
921        }
922    }
923    bail!(
924        "no installed font can draw '{}'; pass a font file that can",
925        wanted
926            .iter()
927            .filter(|c| winansi(&c.to_string()).is_none())
928            .collect::<String>()
929    )
930}
931
932/// Embeds the glyphs that `text` needs as a Type0 font with an identity encoding.
933///
934/// Those are the plain glyph of every character in `wanted`, and whatever shaping
935/// `text` selects on top: ligatures, joined and contextual forms, positioned marks.
936fn embed(
937    doc: &mut Document,
938    data: Vec<u8>,
939    index: u32,
940    wanted: &[char],
941    text: &str,
942) -> Result<TextFont> {
943    let face =
944        ttf_parser::Face::parse(&data, index).map_err(|e| anyhow!("cannot parse font: {e}"))?;
945    let scale = 1000.0 / face.units_per_em() as f64;
946    let mut remapper = subsetter::GlyphRemapper::new();
947    let advance = |glyph: u16| {
948        face.glyph_hor_advance(ttf_parser::GlyphId(glyph))
949            .unwrap_or(0) as f64
950            * scale
951    };
952    let mut subset: HashMap<u16, (u16, f64)> = HashMap::new();
953    let mut nominal = HashMap::new();
954    // What each glyph of the subset reads as, for the map back to text.
955    let mut reads: BTreeMap<u16, String> = BTreeMap::new();
956    for &c in wanted {
957        if let Some(glyph) = face.glyph_index(c) {
958            let new = remapper.remap(glyph.0);
959            subset.insert(glyph.0, (new, advance(glyph.0)));
960            nominal.insert(c, glyph.0);
961            reads.entry(new).or_insert_with(|| c.to_string());
962        }
963    }
964    if let Some(shaper) = rustybuzz::Face::from_slice(&data, index) {
965        // Shaped line by line: a line break never takes part in a ligature.
966        for line in text.split(char::is_control) {
967            let shaped = shape(&shaper, line);
968            let mut i = 0;
969            while i < shaped.len() {
970                let mut j = i;
971                while j < shaped.len() && shaped[j].cluster == shaped[i].cluster {
972                    j += 1;
973                }
974                // One cluster: the glyphs that together draw a stretch of characters.
975                // A glyph that is the plain glyph of one of those characters reads as
976                // it. The other characters are shared out, in order, among the glyphs
977                // shaping put in: a ligature reads as all it replaced. Read in drawing
978                // order, the cluster then gives back its characters.
979                let mut rest: Vec<char> = line[shaped[i].cluster.clone()].chars().collect();
980                let mut put_in = Vec::new();
981                for glyph in &shaped[i..j] {
982                    let new = remapper.remap(glyph.glyph);
983                    subset.insert(glyph.glyph, (new, advance(glyph.glyph)));
984                    match rest
985                        .iter()
986                        .position(|c| nominal.get(c) == Some(&glyph.glyph))
987                    {
988                        Some(at) => {
989                            rest.remove(at);
990                        }
991                        None => put_in.push(new),
992                    }
993                }
994                for (k, new) in put_in.iter().enumerate() {
995                    let to = if k + 1 == put_in.len() {
996                        rest.len()
997                    } else {
998                        (k + 1).min(rest.len())
999                    };
1000                    let part: String = rest[k.min(rest.len())..to].iter().collect();
1001                    if !part.is_empty() {
1002                        reads.entry(*new).or_insert(part);
1003                    }
1004                }
1005                i = j;
1006            }
1007        }
1008    }
1009    let program = subsetter::subset(&data, index, &remapper)
1010        .map_err(|e| anyhow!("cannot subset font: {e}"))?;
1011
1012    // CFF outlines are embedded as a bare CFF table, TrueType outlines as a font file.
1013    let cff_tag = ttf_parser::Tag::from_bytes(b"CFF ");
1014    let is_cff = face.raw_face().table(cff_tag).is_some();
1015    let program = if is_cff {
1016        let raw = ttf_parser::RawFace::parse(&program, 0)
1017            .map_err(|e| anyhow!("cannot read font subset: {e}"))?;
1018        raw.table(cff_tag)
1019            .ok_or_else(|| anyhow!("font subset has no CFF table"))?
1020            .to_vec()
1021    } else {
1022        program
1023    };
1024
1025    let base = face
1026        .names()
1027        .into_iter()
1028        .find(|n| n.name_id == ttf_parser::name_id::POST_SCRIPT_NAME)
1029        .and_then(|n| n.to_string())
1030        .map(|n| {
1031            n.chars()
1032                .filter(|c| c.is_ascii_alphanumeric() || *c == '-')
1033                .collect::<String>()
1034        })
1035        .filter(|n| !n.is_empty())
1036        .unwrap_or_else(|| "Font".to_string());
1037    // Subset fonts carry a six letter tag, derived here from the glyph set so output is reproducible.
1038    let mut glyph_set: Vec<u16> = subset.keys().copied().collect();
1039    glyph_set.sort_unstable();
1040    let hash = glyph_set.iter().fold(0xcbf29ce484222325u64, |h, &g| {
1041        (h ^ g as u64).wrapping_mul(0x100000001b3)
1042    });
1043    let tag: String = (0..6)
1044        .map(|i| (b'A' + ((hash >> (i * 8)) % 26) as u8) as char)
1045        .collect();
1046    let name = Object::Name(format!("{tag}+{base}").into_bytes());
1047
1048    let mut file = Dictionary::new();
1049    if is_cff {
1050        file.set("Subtype", Object::Name(b"CIDFontType0C".to_vec()));
1051    }
1052    let mut file = Stream::new(file, program);
1053    let _ = file.compress();
1054    let file = doc.add_object(file);
1055
1056    let bbox = face.global_bounding_box();
1057    let mut descriptor = Dictionary::new();
1058    descriptor.set("Type", Object::Name(b"FontDescriptor".to_vec()));
1059    descriptor.set("FontName", name.clone());
1060    descriptor.set("Flags", 4);
1061    descriptor.set(
1062        "FontBBox",
1063        [bbox.x_min, bbox.y_min, bbox.x_max, bbox.y_max]
1064            .map(|v| Object::Real((v as f64 * scale) as f32))
1065            .to_vec(),
1066    );
1067    descriptor.set("ItalicAngle", 0);
1068    descriptor.set(
1069        "Ascent",
1070        Object::Real((face.ascender() as f64 * scale) as f32),
1071    );
1072    descriptor.set(
1073        "Descent",
1074        Object::Real((face.descender() as f64 * scale) as f32),
1075    );
1076    descriptor.set(
1077        "CapHeight",
1078        Object::Real((face.capital_height().unwrap_or(face.ascender()) as f64 * scale) as f32),
1079    );
1080    descriptor.set("StemV", 80);
1081    descriptor.set(if is_cff { "FontFile3" } else { "FontFile2" }, file);
1082    let descriptor = doc.add_object(descriptor);
1083
1084    let mut by_glyph: Vec<(u16, f64)> = subset.values().copied().collect();
1085    by_glyph.sort_by_key(|g| g.0);
1086    let mut widths = Vec::with_capacity(by_glyph.len() * 2);
1087    for &(glyph, advance) in &by_glyph {
1088        widths.push(Object::Integer(glyph as i64));
1089        widths.push(Object::Array(vec![Object::Real(advance as f32)]));
1090    }
1091
1092    let mut system = Dictionary::new();
1093    system.set("Registry", Object::string_literal("Adobe"));
1094    system.set("Ordering", Object::string_literal("Identity"));
1095    system.set("Supplement", 0);
1096    let mut cid = Dictionary::new();
1097    cid.set("Type", Object::Name(b"Font".to_vec()));
1098    cid.set(
1099        "Subtype",
1100        Object::Name(if is_cff {
1101            b"CIDFontType0".to_vec()
1102        } else {
1103            b"CIDFontType2".to_vec()
1104        }),
1105    );
1106    cid.set("BaseFont", name.clone());
1107    cid.set("CIDSystemInfo", system);
1108    cid.set("FontDescriptor", descriptor);
1109    cid.set("DW", 1000);
1110    cid.set("W", widths);
1111    if !is_cff {
1112        cid.set("CIDToGIDMap", Object::Name(b"Identity".to_vec()));
1113    }
1114    let cid = doc.add_object(cid);
1115
1116    let reads: Vec<(u16, String)> = reads.into_iter().collect();
1117    let mut to_unicode = Stream::new(Dictionary::new(), to_unicode(&reads).into_bytes());
1118    let _ = to_unicode.compress();
1119    let to_unicode = doc.add_object(to_unicode);
1120
1121    let mut font = Dictionary::new();
1122    font.set("Type", Object::Name(b"Font".to_vec()));
1123    font.set("Subtype", Object::Name(b"Type0".to_vec()));
1124    font.set("BaseFont", name);
1125    font.set("Encoding", Object::Name(b"Identity-H".to_vec()));
1126    font.set("DescendantFonts", vec![Object::Reference(cid)]);
1127    font.set("ToUnicode", to_unicode);
1128    Ok(TextFont {
1129        id: doc.add_object(font),
1130        embedded: Some(Box::new(Embedded {
1131            data,
1132            index,
1133            scale,
1134            subset,
1135            nominal,
1136            reads: reads.into_iter().collect(),
1137        })),
1138        builtin: Style::default(),
1139        stand_ins: Vec::new(),
1140    })
1141}
1142
1143/// A CMap from glyph ids back to text, so stamped text stays searchable.
1144///
1145/// A glyph may read as several characters: a ligature does.
1146fn to_unicode(glyphs: &[(u16, String)]) -> String {
1147    let mut cmap = String::from(
1148        "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\
1149         /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
1150         /CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n\
1151         1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n",
1152    );
1153    // A bfchar block holds at most 100 entries.
1154    for chunk in glyphs.chunks(100) {
1155        cmap += &format!("{} beginbfchar\n", chunk.len());
1156        for (glyph, text) in chunk {
1157            let utf16: String = text.encode_utf16().map(|u| format!("{u:04X}")).collect();
1158            cmap += &format!("<{glyph:04X}> <{utf16}>\n");
1159        }
1160        cmap += "endbfchar\n";
1161    }
1162    cmap + "endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n"
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167    use super::*;
1168
1169    #[test]
1170    fn latin1_text_uses_helvetica() {
1171        let mut doc = Document::with_version("1.7");
1172        let font = TextFont::new(&mut doc, "Caf\u{e9} (1)", None).unwrap();
1173        assert_eq!(font.show("a(b)\u{e9}", 10.0), "(a\\(b\\)\\351) Tj");
1174        // "Hi": 722 + 222 thousandths of an em.
1175        assert!((font.width("Hi", 10.0) - 9.44).abs() < 1e-9);
1176        assert_eq!(
1177            doc.get_dictionary(font.id)
1178                .unwrap()
1179                .get(b"BaseFont")
1180                .unwrap()
1181                .as_name()
1182                .unwrap(),
1183            b"Helvetica"
1184        );
1185    }
1186
1187    #[test]
1188    fn to_unicode_maps_glyphs_including_astral_characters_and_ligatures() {
1189        let cmap = to_unicode(&[
1190            (1, "ż".to_string()),
1191            (2, "😀".to_string()),
1192            (3, "fi".to_string()),
1193        ]);
1194        assert!(cmap.contains(
1195            "3 beginbfchar\n<0001> <017C>\n<0002> <D83DDE00>\n<0003> <00660069>\nendbfchar"
1196        ));
1197    }
1198
1199    /// A font embedded for `text`, or nothing where no installed font can draw it.
1200    fn embedded(text: &str) -> Option<(Document, TextFont)> {
1201        let mut doc = Document::with_version("1.7");
1202        match TextFont::new(&mut doc, text, None) {
1203            Ok(font) => Some((doc, font)),
1204            Err(e) => {
1205                assert!(e.to_string().contains("no installed font"), "{e:#}");
1206                eprintln!("skipped: {e}");
1207                None
1208            }
1209        }
1210    }
1211
1212    /// What each glyph of `text` reads as, in the order the glyphs are drawn.
1213    fn drawn(doc: &Document, font: &TextFont, text: &str) -> Vec<String> {
1214        let map = doc
1215            .get_dictionary(font.id)
1216            .unwrap()
1217            .get(b"ToUnicode")
1218            .unwrap();
1219        let map = doc
1220            .get_object(map.as_reference().unwrap())
1221            .unwrap()
1222            .as_stream()
1223            .unwrap();
1224        let map = String::from_utf8(map.decompressed_content().unwrap()).unwrap();
1225        font.embedded
1226            .as_ref()
1227            .unwrap()
1228            .layout(text)
1229            .iter()
1230            .map(|g| {
1231                let line = map
1232                    .lines()
1233                    .find(|l| l.starts_with(&format!("<{:04X}> ", g.glyph)));
1234                let hex = line.map_or("", |l| l[8..].trim_matches(|c| c == '<' || c == '>'));
1235                let units: Vec<u16> = (0..hex.len() / 4)
1236                    .map(|i| u16::from_str_radix(&hex[i * 4..i * 4 + 4], 16).unwrap())
1237                    .collect();
1238                String::from_utf16(&units).unwrap()
1239            })
1240            .collect()
1241    }
1242
1243    #[test]
1244    fn right_to_left_text_is_drawn_from_its_right_end() {
1245        // Hebrew does not join, so each letter keeps its glyph and only the order changes.
1246        let Some((doc, font)) = embedded("\u{5e9}\u{5dc}\u{5d5}\u{5dd} abc") else {
1247            return;
1248        };
1249        assert_eq!(
1250            drawn(&doc, &font, "\u{5e9}\u{5dc}\u{5d5}\u{5dd}"),
1251            ["\u{5dd}", "\u{5d5}", "\u{5dc}", "\u{5e9}"]
1252        );
1253        // Latin text in the same line keeps its own direction.
1254        assert_eq!(
1255            drawn(&doc, &font, "abc \u{5e9}\u{5dc}\u{5d5}\u{5dd}"),
1256            [
1257                "a", "b", "c", " ", "\u{5dd}", "\u{5d5}", "\u{5dc}", "\u{5e9}"
1258            ]
1259        );
1260    }
1261
1262    #[test]
1263    fn arabic_is_joined_and_its_ligature_reads_as_both_letters() {
1264        // seen, lam, alef, meem: the lam and alef form one ligature in every Arabic font.
1265        let word = "\u{633}\u{644}\u{627}\u{645}";
1266        let Some((doc, font)) = embedded(word) else {
1267            return;
1268        };
1269        let embedded = font.embedded.as_ref().unwrap();
1270        let face = ttf_parser::Face::parse(&embedded.data, embedded.index).unwrap();
1271        if face
1272            .raw_face()
1273            .table(ttf_parser::Tag::from_bytes(b"GSUB"))
1274            .is_none()
1275        {
1276            eprintln!("skipped: the installed font has no shaping tables");
1277            return;
1278        }
1279        assert_eq!(
1280            drawn(&doc, &font, word),
1281            ["\u{645}", "\u{644}\u{627}", "\u{633}"]
1282        );
1283        // The seen is drawn in the form that joins to what follows, not as it stands alone.
1284        let glyphs = embedded.layout(word);
1285        let alone = embedded.subset[&embedded.nominal[&'\u{633}']].0;
1286        assert_ne!(glyphs[2].glyph, alone);
1287        // Text the font was not made for falls back to plain glyphs rather than to nothing.
1288        assert_eq!(embedded.layout("\u{645}\u{633}").len(), 2);
1289    }
1290
1291    #[test]
1292    fn kerning_narrows_text_and_is_written_as_movements() {
1293        let Some((_, font)) = embedded("\u{17c} AVAVAV") else {
1294            return;
1295        };
1296        let apart = 3.0 * (font.width("A", 100.0) + font.width("V", 100.0));
1297        let together = font.width("AVAVAV", 100.0);
1298        if (apart - together).abs() < 1e-6 {
1299            eprintln!("skipped: the installed font does not kern A and V");
1300            return;
1301        }
1302        assert!(together < apart, "{together} {apart}");
1303        let shown = font.show("AVAVAV", 100.0);
1304        assert!(shown.starts_with('[') && shown.ends_with("] TJ"), "{shown}");
1305        // The movements written add up to the difference in width.
1306        let moved: f64 = shown
1307            .trim_matches(|c| c == '[' || c == ']' || c == 'T' || c == 'J' || c == ' ')
1308            .split(' ')
1309            .filter_map(|part| part.parse::<f64>().ok())
1310            .sum();
1311        assert!(
1312            (moved / 10.0 - (apart - together)).abs() < 0.6,
1313            "{moved} {apart} {together}"
1314        );
1315        // Plain letters read back as they are: one array, with nothing stated around it.
1316        let arrays = font.elements("AVAVAV");
1317        assert!(arrays.len() == 1 && arrays[0].0.is_none());
1318        assert_eq!(arrays[0].1.len(), shown.split(' ').count() - 1);
1319    }
1320
1321    #[test]
1322    fn text_is_shared_out_among_fonts_by_what_each_can_draw() {
1323        let pieces = |text: &str, has: &dyn Fn(usize, char) -> bool| -> Vec<(usize, String)> {
1324            share(text, 2, has)
1325                .into_iter()
1326                .map(|(font, range)| (font, text[range].to_string()))
1327                .collect()
1328        };
1329        let own = |pieces: &[(usize, &str)]| -> Vec<(usize, String)> {
1330            pieces.iter().map(|(k, s)| (*k, s.to_string())).collect()
1331        };
1332        // The second font takes over where the first has nothing, keeps the comma and
1333        // the space that follow, and hands back at the next word the first can draw.
1334        let latin_first = |font: usize, c: char| font == 1 || c.is_ascii();
1335        assert_eq!(
1336            pieces("ab \u{4f60}\u{597d}, cd", &latin_first),
1337            own(&[(0, "ab "), (1, "\u{4f60}\u{597d}, "), (0, "cd")])
1338        );
1339        // An accent stays in the font of its letter, though the first font has it too.
1340        let accent = |font: usize, c: char| match font {
1341            0 => c.is_ascii() || c == '\u{301}',
1342            _ => c == '\u{436}' || c == '\u{301}',
1343        };
1344        assert_eq!(
1345            pieces("\u{436}\u{301}a", &accent),
1346            own(&[(1, "\u{436}\u{301}"), (0, "a")])
1347        );
1348        // Pieces come as they are drawn: in a line that runs from the right, the
1349        // piece that is read first comes last.
1350        let scripts = |font: usize, c: char| match font {
1351            0 => c == ' ' || ('\u{5d0}'..='\u{5ea}').contains(&c),
1352            _ => ('\u{600}'..='\u{6ff}').contains(&c),
1353        };
1354        assert_eq!(
1355            pieces(
1356                "\u{5e9}\u{5dc}\u{5d5}\u{5dd} \u{633}\u{644}\u{627}\u{645}",
1357                &scripts
1358            ),
1359            own(&[
1360                (1, "\u{633}\u{644}\u{627}\u{645}"),
1361                (0, "\u{5e9}\u{5dc}\u{5d5}\u{5dd} ")
1362            ])
1363        );
1364        // One font takes everything, whatever it has.
1365        assert_eq!(share("a\u{4f60}", 1, &|_, _| false), [(0, 0..4)]);
1366    }
1367
1368    #[test]
1369    fn several_fonts_draw_what_no_single_one_has() {
1370        // Polish, Hebrew, Chinese and Devanagari: installed fonts rarely have all four.
1371        let text = "Za\u{17c}\u{f3}\u{142}\u{107} \u{5e9}\u{5dc}\u{5d5}\u{5dd} \u{4f60}\u{597d} \u{928}\u{92e}\u{938}\u{94d}\u{924}\u{947}";
1372        let Some((doc, font)) = embedded(text) else {
1373            return;
1374        };
1375        let ids = font.ids();
1376        let names: Vec<String> = (0..ids.len()).map(|k| format!("F{k}")).collect();
1377        let pieces = font.pieces(text);
1378        // Every character is drawn by a font that has it, and every font is put to use.
1379        for (_, piece_font, piece) in &pieces {
1380            assert!(
1381                piece.chars().all(|c| piece_font.has(c)),
1382                "{piece} is not covered"
1383            );
1384        }
1385        let used: HashSet<usize> = pieces.iter().map(|p| p.0).collect();
1386        assert_eq!(used.len(), ids.len());
1387        assert!(ids.iter().all(|id| doc.get_dictionary(*id).is_ok()));
1388        // The width is the sum of the pieces, and each piece is shown in its own font.
1389        let sum: f64 = pieces
1390            .iter()
1391            .map(|(_, piece_font, piece)| piece_font.own_width(piece, 10.0))
1392            .sum();
1393        assert!((font.width(text, 10.0) - sum).abs() < 1e-9);
1394        let shown = font.show_named(&names, text, 10.0);
1395        assert_eq!(shown.matches(" Tf").count(), pieces.len(), "{shown}");
1396        eprintln!("{} fonts share the sample", ids.len());
1397    }
1398}