1use 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
15const 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
24pub 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
38pub 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
48pub 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
66pub struct TextFont {
68 pub id: ObjectId,
69 embedded: Option<Box<Embedded>>,
71 builtin: Style,
73 stand_ins: Vec<TextFont>,
75}
76
77fn 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
119struct Embedded {
121 data: Vec<u8>,
122 index: u32,
123 scale: f64,
125 subset: HashMap<u16, (u16, f64)>,
127 nominal: HashMap<char, u16>,
129 reads: HashMap<u16, String>,
131}
132
133struct Shaped {
135 glyph: u16,
137 advance: f64,
139 natural: f64,
141 dx: f64,
143 dy: f64,
144 stated: Option<(String, usize)>,
149}
150
151struct RawGlyph {
153 glyph: u16,
155 cluster: std::ops::Range<usize>,
157 advance: f64,
158 dx: f64,
159 dy: f64,
160}
161
162fn 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 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 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 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 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 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 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#[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
319const 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn is_embedded(&self) -> bool {
593 self.embedded.is_some()
594 }
595
596 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 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 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
646fn 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
658const 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
671fn 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 (true, family) => format!("{family}Mono"),
693 (false, family) => family.to_string(),
694 };
695 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
712fn 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
762fn 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
783fn 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
854fn 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 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
900fn system_font(wanted: &[char]) -> Result<(Vec<u8>, u32)> {
902 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
932fn 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 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 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 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 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 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
1143fn 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 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 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 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 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 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 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 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 let glyphs = embedded.layout(word);
1285 let alone = embedded.subset[&embedded.nominal[&'\u{633}']].0;
1286 assert_ne!(glyphs[2].glyph, alone);
1287 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 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 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 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 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 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 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 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 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 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}