1mod cache;
9mod face;
10mod synth;
11
12pub use cache::{GlyphCache, GlyphKey};
13pub use face::{Charmap, CharmapId, Face};
14pub use synth::SynthGlyph;
15
16pub use crate::descriptor::em_adjust;
17pub(crate) use crate::descriptor::normalize_font_metric;
18
19use crate::Gid;
20use crate::ids::GlyphName;
21use pdfrum_common::kurbo::{Affine, BezPath, Rect};
22use pdfrum_common::{Diagnostics, Limits};
23use std::sync::Arc;
24
25#[derive(Debug, Clone, Default)]
31pub enum GlyphSource {
32 Fontations(Face),
34 Type1(Arc<pdfrum_type1::Type1Font>),
36 #[default]
38 None,
39}
40
41const TYPE1_CHARMAPS: [CharmapId; 2] = [CharmapId::UNICODE_SYNTHETIC, CharmapId::ADOBE_CUSTOM];
44
45#[derive(Debug, Clone, Copy, PartialEq, Default)]
52pub(crate) struct GlyphParams {
53 pub dest_width: i32,
56 pub weight: i32,
58 pub skew: i32,
61 pub vertical: bool,
64 pub embolden: f64,
67}
68
69impl GlyphSource {
70 #[must_use]
74 pub fn from_bytes(bytes: impl Into<Arc<[u8]>>) -> Option<Self> {
75 let bytes = bytes.into();
76 if let Some(face) = Face::new(Arc::clone(&bytes), 0) {
77 return Some(Self::Fontations(face));
78 }
79 let mut diags = Diagnostics::default();
80 pdfrum_type1::Type1Font::parse(&bytes, &Limits::default(), &mut diags)
81 .ok()
82 .map(|font| Self::Type1(Arc::new(font)))
83 }
84
85 #[must_use]
87 pub(crate) fn is_some(&self) -> bool {
88 !matches!(self, Self::None)
89 }
90
91 #[must_use]
93 pub fn units_per_em(&self) -> u16 {
94 match self {
95 Self::Fontations(f) => f.units_per_em(),
96 Self::Type1(f) => f.units_per_em(),
97 Self::None => 0,
98 }
99 }
100
101 #[must_use]
103 pub fn num_glyphs(&self) -> u32 {
104 match self {
105 Self::Fontations(f) => f.num_glyphs(),
106 Self::Type1(f) => f.num_glyphs(),
107 Self::None => 0,
108 }
109 }
110
111 #[must_use]
114 pub fn is_truetype(&self) -> bool {
115 match self {
116 Self::Fontations(f) => f.is_truetype(),
117 Self::Type1(_) | Self::None => false,
118 }
119 }
120
121 #[must_use]
127 pub fn char_index(&self, charmap: Charmap, code: u32) -> u16 {
128 match self {
129 Self::Fontations(f) => f.char_index(charmap, code),
130 Self::Type1(f) => {
133 let gid = match charmap {
134 Charmap::Unicode => char::from_u32(code).and_then(|c| f.unicode_to_gid(c)),
135 _ => u8::try_from(code).ok().and_then(|b| f.code_to_gid(b)),
136 };
137 gid.map_or(0, |g| g.0)
138 }
139 Self::None => 0,
140 }
141 }
142
143 #[must_use]
146 pub(crate) fn name_index(&self, name: &[u8]) -> u16 {
147 let Ok(name) = std::str::from_utf8(name) else {
148 return 0;
149 };
150 match self {
151 Self::Fontations(f) => f.name_index(name),
152 Self::Type1(f) => f.name_to_gid(name).map_or(0, |g| g.0),
153 Self::None => 0,
154 }
155 }
156
157 #[must_use]
159 pub(crate) fn glyph_name(&self, gid: Gid) -> Option<GlyphName> {
160 match self {
161 Self::Fontations(f) => f.glyph_name(gid).map(|n| GlyphName::new(n.into_bytes())),
162 Self::Type1(f) => f
163 .glyph_name(gid.into())
164 .map(|n| GlyphName::new(n.as_bytes().to_vec())),
165 Self::None => None,
166 }
167 }
168
169 #[must_use]
171 pub(crate) fn has_glyph_names(&self) -> bool {
172 match self {
173 Self::Fontations(f) => f.has_glyph_names(),
174 Self::Type1(_) => true,
175 Self::None => false,
176 }
177 }
178
179 #[must_use]
182 pub fn charmaps(&self) -> &[CharmapId] {
183 match self {
184 Self::Fontations(f) => f.charmaps(),
185 Self::Type1(_) => &TYPE1_CHARMAPS,
188 Self::None => &[],
189 }
190 }
191
192 #[must_use]
204 pub(crate) fn outline(&self, gid: Gid, params: GlyphParams) -> Option<BezPath> {
205 let upem = self.units_per_em();
206 let raw = match self {
207 Self::Fontations(f) => {
208 if f.is_hint_reliant()
219 && let Some(hinted) = self.hinted_outline(gid)
220 {
221 return Some(synthesize(hinted, params));
222 }
223 f.outline(gid)?
224 }
225 Self::Type1(f) => match Self::mm_instance(f, gid, params) {
226 Some(inst) => inst.outline(gid.into())?.0,
227 None => f.outline(gid.into())?.0,
228 },
229 Self::None => return None,
230 };
231 let trimmed = trim_empty_contours(raw)?;
232 let scaled = if upem == 0 || upem == 1000 {
233 trimmed
234 } else {
235 Affine::scale(1000.0 / f64::from(upem)) * trimmed
236 };
237 Some(synthesize(scaled, params))
238 }
239
240 #[must_use]
262 pub(crate) fn hinted_outline(&self, gid: Gid) -> Option<BezPath> {
263 let Self::Fontations(f) = self else {
264 return None;
265 };
266 let raw = f.hinted_outline(gid)?;
267 let trimmed = trim_empty_contours(raw)?;
268 Some(Affine::scale(1000.0 / f64::from(Face::HINT_PPEM)) * trimmed)
269 }
270
271 #[must_use]
273 pub fn default_advance(&self, gid: Gid) -> i32 {
274 self.advance(gid, GlyphParams::default())
275 }
276
277 #[must_use]
283 pub(crate) fn advance(&self, gid: Gid, params: GlyphParams) -> i32 {
284 let upem = self.units_per_em();
285 let raw = match self {
286 Self::Fontations(f) => f.advance(gid),
287 Self::Type1(f) => match Self::mm_instance(f, gid, params) {
288 Some(inst) => inst.advance(gid.into()),
289 None => f.outline(gid.into()).map(|(_, a)| a),
290 },
291 Self::None => None,
292 };
293 let Some(raw) = raw else { return 0 };
294 let raw = raw as i64;
297 if raw < i64::from(i32::MIN) / 1000 || raw > i64::from(i32::MAX) / 1000 {
298 return 0;
299 }
300 em_adjust(raw as i32, upem)
301 }
302
303 #[must_use]
306 pub(crate) fn advance_tt(&self, gid: Gid) -> i32 {
307 let upem = self.units_per_em();
308 let raw = match self {
309 Self::Fontations(f) => f.advance(gid),
310 Self::Type1(f) => f.outline(gid.into()).map(|(_, a)| a),
311 Self::None => None,
312 };
313 raw.map_or(0, |a| normalize_font_metric(a as i64, upem))
314 }
315
316 #[must_use]
323 pub(crate) fn glyph_bbox(&self, gid: Gid) -> Option<Rect> {
324 let upem = self.units_per_em();
325 let raw = match self {
326 Self::Fontations(f) => f.glyph_bbox(gid)?,
327 Self::Type1(f) => f.glyph_bounds(gid.into())?,
328 Self::None => return None,
329 };
330 let n = |v: f64| f64::from(normalize_font_metric(v as i64, upem));
331 Some(Rect::new(n(raw.x0), n(raw.y0), n(raw.x1), n(raw.y1)))
332 }
333
334 fn mm_instance(
342 font: &pdfrum_type1::Type1Font,
343 gid: Gid,
344 params: GlyphParams,
345 ) -> Option<pdfrum_type1::Type1Instance<'_>> {
346 let axes = font.mm_axes()?;
347 let weight_axis = axes.first()?;
348 let width_axis = axes.get(1)?;
349
350 let weight = if params.weight == 0 {
351 weight_axis.default
352 } else {
353 params.weight as f32
354 };
355
356 if params.dest_width == 0 {
357 return font.instantiate(&[weight, width_axis.default]);
358 }
359
360 let upem = font.units_per_em();
361 let probe = |coord: f32| -> Option<i32> {
362 let inst = font.instantiate(&[weight, coord])?;
363 let adv = inst.advance(gid.into())?;
364 Some(em_adjust(adv as i32, upem))
365 };
366 let (lo, hi) = (width_axis.min, width_axis.max);
367 let min_w = probe(lo)?;
368 let max_w = probe(hi)?;
369 if max_w == min_w {
370 return font.instantiate(&[weight, hi]);
372 }
373 let t = (params.dest_width - min_w) as f32 / (max_w - min_w) as f32;
374 font.instantiate(&[weight, (hi - lo).mul_add(t, lo)])
375 }
376
377 #[must_use]
379 pub fn postscript_name(&self) -> Option<String> {
380 match self {
381 Self::Fontations(f) => f.postscript_name(),
382 Self::Type1(f) => f
383 .postscript_name()
384 .map(ToOwned::to_owned)
385 .or_else(|| f.family_name().map(ToOwned::to_owned)),
386 Self::None => None,
387 }
388 }
389
390 #[must_use]
392 pub fn is_fixed_pitch(&self) -> bool {
393 match self {
394 Self::Fontations(f) => f.is_fixed_pitch(),
395 Self::Type1(f) => f.is_fixed_pitch(),
396 Self::None => false,
397 }
398 }
399
400 #[must_use]
402 pub fn is_italic(&self) -> bool {
403 match self {
404 Self::Fontations(f) => f.is_italic(),
405 Self::Type1(f) => f.italic_angle() != 0.0,
406 Self::None => false,
407 }
408 }
409
410 #[must_use]
412 pub fn is_bold(&self) -> bool {
413 match self {
414 Self::Fontations(f) => f.is_bold(),
415 Self::Type1(f) => {
416 let name = f.postscript_name().or_else(|| f.full_name()).unwrap_or("");
417 name.contains("Bold") || name.contains("Black")
418 }
419 Self::None => false,
420 }
421 }
422
423 #[must_use]
425 pub fn cap_height_unscaled(&self) -> Option<f32> {
426 match self {
427 Self::Fontations(f) => f.cap_height(),
428 Self::Type1(_) | Self::None => None,
429 }
430 }
431
432 #[must_use]
434 pub fn unscaled_ascent(&self) -> Option<i32> {
435 match self {
436 Self::Fontations(f) => f.metrics().and_then(|m| i32::try_from(m.ascender).ok()),
437 Self::Type1(f) => Some(f.bbox().y1 as i32),
438 Self::None => None,
439 }
440 }
441
442 #[must_use]
444 pub fn unscaled_descent(&self) -> Option<i32> {
445 match self {
446 Self::Fontations(f) => f.metrics().and_then(|m| i32::try_from(m.descender).ok()),
447 Self::Type1(f) => Some(f.bbox().y0 as i32),
448 Self::None => None,
449 }
450 }
451
452 #[must_use]
454 pub fn unscaled_bbox(&self) -> Option<(i32, i32, i32, i32)> {
455 match self {
456 Self::Fontations(f) => {
457 let m = f.metrics()?;
458 Some((
459 i32::try_from(m.bbox_left).ok()?,
460 i32::try_from(m.bbox_bottom).ok()?,
461 i32::try_from(m.bbox_right).ok()?,
462 i32::try_from(m.bbox_top).ok()?,
463 ))
464 }
465 Self::Type1(f) => {
466 let b = f.bbox();
467 Some((b.x0 as i32, b.y0 as i32, b.x1 as i32, b.y1 as i32))
468 }
469 Self::None => None,
470 }
471 }
472
473 #[must_use]
477 pub fn unicode_mappings(&self, max: u32) -> Vec<(u32, u16)> {
478 match self {
479 Self::Fontations(f) => f.unicode_mappings(max),
480 Self::Type1(f) => {
481 let mut out: Vec<(u32, u16)> = f
482 .unicode_pairs()
483 .filter(|(ch, gid)| u32::from(*ch) <= max && gid.0 != 0)
484 .map(|(ch, gid)| (u32::from(ch), gid.0))
485 .collect();
486 out.sort_unstable_by_key(|(cp, _)| *cp);
487 out
488 }
489 Self::None => Vec::new(),
490 }
491 }
492}
493
494fn trim_empty_contours(path: BezPath) -> Option<BezPath> {
502 use pdfrum_common::kurbo::PathEl;
503
504 let mut els: Vec<PathEl> = path.into_iter().collect();
505 loop {
506 let end = els
508 .iter()
509 .rposition(|e| !matches!(e, PathEl::ClosePath))
510 .map_or(0, |i| i + 1);
511
512 if end >= 2
514 && let (Some(PathEl::MoveTo(a)), Some(PathEl::LineTo(b))) =
515 (els.get(end - 2), els.get(end - 1))
516 && a == b
517 {
518 els.truncate(end - 2);
519 continue;
520 }
521 if end >= 4
523 && let (
524 Some(PathEl::MoveTo(a)),
525 Some(PathEl::CurveTo(_, _, b)),
526 Some(PathEl::CurveTo(_, _, c)),
527 Some(PathEl::CurveTo(_, _, d)),
528 ) = (
529 els.get(end - 4),
530 els.get(end - 3),
531 els.get(end - 2),
532 els.get(end - 1),
533 )
534 && a == b
535 && b == c
536 && c == d
537 {
538 els.truncate(end - 4);
539 continue;
540 }
541 break;
542 }
543 if els.iter().all(|e| matches!(e, PathEl::ClosePath)) {
544 return None;
545 }
546 let out = BezPath::from_vec(els);
547 if out.elements().is_empty() {
548 None
549 } else {
550 Some(out)
551 }
552}
553
554fn synthesize(path: BezPath, params: GlyphParams) -> BezPath {
567 SynthGlyph {
568 skew: params.skew,
569 vertical: params.vertical,
570 embolden: params.embolden,
571 }
572 .apply(path)
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use pdfrum_common::kurbo::{PathEl, Point};
579
580 #[test]
581 fn a_move_and_a_line_back_to_it_is_trimmed() {
582 let mut p = BezPath::new();
583 p.move_to((10.0, 10.0));
584 p.line_to((50.0, 10.0));
585 p.line_to((50.0, 50.0));
586 p.close_path();
587 p.move_to((7.0, 7.0));
588 p.line_to((7.0, 7.0));
589 let trimmed = trim_empty_contours(p).expect("the real contour survives");
590 assert_eq!(trimmed.elements().len(), 4);
591 assert!(matches!(
592 trimmed.elements().first(),
593 Some(PathEl::MoveTo(_))
594 ));
595 }
596
597 #[test]
598 fn a_move_and_three_curves_to_it_is_trimmed() {
599 let mut p = BezPath::new();
600 p.move_to((0.0, 0.0));
601 p.line_to((10.0, 0.0));
602 p.close_path();
603 let q = Point::new(3.0, 3.0);
604 p.move_to(q);
605 for _ in 0..3 {
606 p.curve_to(q, q, q);
607 }
608 let trimmed = trim_empty_contours(p).expect("the real contour survives");
609 assert_eq!(trimmed.elements().len(), 3);
610 }
611
612 #[test]
613 fn repeated_degenerate_contours_are_all_trimmed() {
614 let mut p = BezPath::new();
615 p.move_to((0.0, 0.0));
616 p.line_to((10.0, 0.0));
617 p.close_path();
618 for i in 0..3 {
619 let q = Point::new(f64::from(i), f64::from(i));
620 p.move_to(q);
621 p.line_to(q);
622 }
623 let trimmed = trim_empty_contours(p).expect("the real contour survives");
624 assert_eq!(trimmed.elements().len(), 3);
625 }
626
627 #[test]
628 fn an_entirely_degenerate_outline_is_none_not_an_empty_path() {
629 let mut p = BezPath::new();
630 p.move_to((5.0, 5.0));
631 p.line_to((5.0, 5.0));
632 assert!(trim_empty_contours(p).is_none());
633 assert!(trim_empty_contours(BezPath::new()).is_none());
634 }
635
636 #[test]
637 fn a_healthy_outline_is_untouched() {
638 let mut p = BezPath::new();
639 p.move_to((0.0, 0.0));
640 p.curve_to((10.0, 0.0), (10.0, 10.0), (0.0, 10.0));
641 p.close_path();
642 let n = p.elements().len();
643 assert_eq!(trim_empty_contours(p).map(|q| q.elements().len()), Some(n));
644 }
645
646 #[test]
647 fn only_a_face_on_the_hint_reliant_list_asks_for_the_interpreter() {
648 let plain = crate::testfonts::load("tt_composite_instructions.ttf");
651 let plain = Face::new(plain.into(), 0).expect("the fixture loads");
652 assert!(!plain.is_hint_reliant());
653
654 let tricky = crate::testfonts::load("tt_hint_reliant.ttf");
655 let tricky = Face::new(tricky.into(), 0).expect("the fixture loads");
656 assert!(tricky.is_hint_reliant());
657 }
658
659 #[test]
660 fn a_hint_reliant_face_draws_its_path_outlines_grid_fitted() {
661 let bytes = crate::testfonts::load("tt_hint_reliant.ttf");
666 let face = Face::new(bytes.into(), 0).expect("the fixture loads");
667 let source = GlyphSource::Fontations(face);
668 let gid = Gid(3);
669
670 let drawn = source
671 .outline(gid, GlyphParams::default())
672 .expect("the instructed composite draws");
673 let hinted = source
674 .hinted_outline(gid)
675 .expect("the fixture carries programs the interpreter accepts");
676 assert_eq!(drawn.to_svg(), hinted.to_svg());
677 }
678
679 #[test]
680 fn an_ordinary_face_keeps_its_unhinted_path_outlines() {
681 let bytes = crate::testfonts::load("tt_composite_instructions.ttf");
684 let face = Face::new(bytes.into(), 0).expect("the fixture loads");
685 let source = GlyphSource::Fontations(face.clone());
686 let gid = Gid(3);
687
688 let drawn = source
689 .outline(gid, GlyphParams::default())
690 .expect("the instructed composite draws");
691 let unhinted = face.outline(gid).expect("the glyph has an outline");
692 let upem = f64::from(face.units_per_em());
693 let expected = Affine::scale(1000.0 / upem) * unhinted;
694 assert_eq!(drawn.to_svg(), expected.to_svg());
695 }
696
697 #[test]
698 fn the_empty_source_answers_everything_with_nothing() {
699 let s = GlyphSource::None;
700 assert!(!s.is_some());
701 assert_eq!(s.units_per_em(), 0);
702 assert_eq!(s.num_glyphs(), 0);
703 assert!(!s.is_truetype());
704 assert_eq!(s.char_index(Charmap::Unicode, 0x41), 0);
705 assert_eq!(s.name_index(b"A"), 0);
706 assert!(s.glyph_name(Gid(0)).is_none());
707 assert!(!s.has_glyph_names());
708 assert!(s.charmaps().is_empty());
709 assert!(s.outline(Gid(0), GlyphParams::default()).is_none());
710 assert_eq!(s.advance(Gid(0), GlyphParams::default()), 0);
711 assert_eq!(s.advance_tt(Gid(0)), 0);
712 assert!(s.glyph_bbox(Gid(0)).is_none());
713 }
714}