1mod cache;
9mod face;
10
11pub use cache::{GlyphCache, GlyphKey};
12pub use face::{Charmap, CharmapId, Face};
13
14pub use crate::descriptor::em_adjust;
15pub(crate) use crate::descriptor::normalize_font_metric;
16
17use crate::Gid;
18use crate::ids::GlyphName;
19use pdfrum_common::kurbo::{Affine, BezPath, Rect};
20use pdfrum_common::{Diagnostics, Limits};
21use std::sync::Arc;
22
23#[derive(Debug, Clone, Default)]
29pub enum GlyphSource {
30 Fontations(Face),
32 Type1(Arc<pdfrum_type1::Type1Font>),
34 #[default]
36 None,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub(crate) struct GlyphParams {
46 pub dest_width: i32,
49 pub weight: i32,
51}
52
53impl GlyphSource {
54 #[must_use]
58 pub fn from_bytes(bytes: impl Into<Arc<[u8]>>) -> Option<Self> {
59 let bytes = bytes.into();
60 if let Some(face) = Face::new(Arc::clone(&bytes), 0) {
61 return Some(Self::Fontations(face));
62 }
63 let mut diags = Diagnostics::default();
64 pdfrum_type1::Type1Font::parse(&bytes, &Limits::default(), &mut diags)
65 .ok()
66 .map(|font| Self::Type1(Arc::new(font)))
67 }
68
69 #[must_use]
71 pub(crate) fn is_some(&self) -> bool {
72 !matches!(self, Self::None)
73 }
74
75 #[must_use]
77 pub fn units_per_em(&self) -> u16 {
78 match self {
79 Self::Fontations(f) => f.units_per_em(),
80 Self::Type1(f) => f.units_per_em(),
81 Self::None => 0,
82 }
83 }
84
85 #[must_use]
87 pub fn num_glyphs(&self) -> u32 {
88 match self {
89 Self::Fontations(f) => f.num_glyphs(),
90 Self::Type1(f) => f.num_glyphs(),
91 Self::None => 0,
92 }
93 }
94
95 #[must_use]
98 pub fn is_truetype(&self) -> bool {
99 match self {
100 Self::Fontations(f) => f.is_truetype(),
101 Self::Type1(_) | Self::None => false,
102 }
103 }
104
105 #[must_use]
111 pub fn char_index(&self, charmap: Charmap, code: u32) -> u16 {
112 match self {
113 Self::Fontations(f) => f.char_index(charmap, code),
114 Self::Type1(f) => {
117 let gid = match charmap {
118 Charmap::Unicode => char::from_u32(code).and_then(|c| f.unicode_to_gid(c)),
119 _ => u8::try_from(code).ok().and_then(|b| f.code_to_gid(b)),
120 };
121 gid.map_or(0, |g| g.0)
122 }
123 Self::None => 0,
124 }
125 }
126
127 #[must_use]
130 pub(crate) fn name_index(&self, name: &[u8]) -> u16 {
131 let Ok(name) = std::str::from_utf8(name) else {
132 return 0;
133 };
134 match self {
135 Self::Fontations(f) => f.name_index(name),
136 Self::Type1(f) => f.name_to_gid(name).map_or(0, |g| g.0),
137 Self::None => 0,
138 }
139 }
140
141 #[must_use]
143 pub(crate) fn glyph_name(&self, gid: Gid) -> Option<GlyphName> {
144 match self {
145 Self::Fontations(f) => f.glyph_name(gid).map(|n| GlyphName::new(n.into_bytes())),
146 Self::Type1(f) => f
147 .glyph_name(gid.into())
148 .map(|n| GlyphName::new(n.as_bytes().to_vec())),
149 Self::None => None,
150 }
151 }
152
153 #[must_use]
155 pub(crate) fn has_glyph_names(&self) -> bool {
156 match self {
157 Self::Fontations(f) => f.has_glyph_names(),
158 Self::Type1(_) => true,
159 Self::None => false,
160 }
161 }
162
163 #[must_use]
166 pub fn charmaps(&self) -> Vec<CharmapId> {
167 match self {
168 Self::Fontations(f) => f.charmaps(),
169 Self::Type1(_) => vec![CharmapId::UNICODE_SYNTHETIC, CharmapId::ADOBE_CUSTOM],
172 Self::None => Vec::new(),
173 }
174 }
175
176 #[must_use]
188 pub(crate) fn outline(&self, gid: Gid, params: GlyphParams) -> Option<BezPath> {
189 let upem = self.units_per_em();
190 let raw = match self {
191 Self::Fontations(f) => {
192 if f.composite_is_instructed(gid)
197 && let Some(hinted) = self.hinted_outline(gid)
198 {
199 return Some(hinted);
200 }
201 f.outline(gid)?
202 }
203 Self::Type1(f) => match Self::mm_instance(f, gid, params) {
204 Some(inst) => inst.outline(gid.into())?.0,
205 None => f.outline(gid.into())?.0,
206 },
207 Self::None => return None,
208 };
209 let trimmed = trim_empty_contours(raw)?;
210 if upem == 0 || upem == 1000 {
211 return Some(trimmed);
212 }
213 let scale = 1000.0 / f64::from(upem);
214 Some(Affine::scale(scale) * trimmed)
215 }
216
217 #[must_use]
239 pub(crate) fn hinted_outline(&self, gid: Gid) -> Option<BezPath> {
240 let Self::Fontations(f) = self else {
241 return None;
242 };
243 let raw = f.hinted_outline(gid)?;
244 let trimmed = trim_empty_contours(raw)?;
245 Some(Affine::scale(1000.0 / f64::from(Face::HINT_PPEM)) * trimmed)
246 }
247
248 #[must_use]
250 pub fn default_advance(&self, gid: Gid) -> i32 {
251 self.advance(gid, GlyphParams::default())
252 }
253
254 #[must_use]
260 pub(crate) fn advance(&self, gid: Gid, params: GlyphParams) -> i32 {
261 let upem = self.units_per_em();
262 let raw = match self {
263 Self::Fontations(f) => f.advance(gid),
264 Self::Type1(f) => match Self::mm_instance(f, gid, params) {
265 Some(inst) => inst.advance(gid.into()),
266 None => f.outline(gid.into()).map(|(_, a)| a),
267 },
268 Self::None => None,
269 };
270 let Some(raw) = raw else { return 0 };
271 let raw = raw as i64;
274 if raw < i64::from(i32::MIN) / 1000 || raw > i64::from(i32::MAX) / 1000 {
275 return 0;
276 }
277 em_adjust(raw as i32, upem)
278 }
279
280 #[must_use]
283 pub(crate) fn advance_tt(&self, gid: Gid) -> i32 {
284 let upem = self.units_per_em();
285 let raw = match self {
286 Self::Fontations(f) => f.advance(gid),
287 Self::Type1(f) => f.outline(gid.into()).map(|(_, a)| a),
288 Self::None => None,
289 };
290 raw.map_or(0, |a| normalize_font_metric(a as i64, upem))
291 }
292
293 #[must_use]
300 pub(crate) fn glyph_bbox(&self, gid: Gid) -> Option<Rect> {
301 let upem = self.units_per_em();
302 let raw = match self {
303 Self::Fontations(f) => f.glyph_bbox(gid)?,
304 Self::Type1(f) => f.glyph_bounds(gid.into())?,
305 Self::None => return None,
306 };
307 let n = |v: f64| f64::from(normalize_font_metric(v as i64, upem));
308 Some(Rect::new(n(raw.x0), n(raw.y0), n(raw.x1), n(raw.y1)))
309 }
310
311 fn mm_instance(
319 font: &pdfrum_type1::Type1Font,
320 gid: Gid,
321 params: GlyphParams,
322 ) -> Option<pdfrum_type1::Type1Instance<'_>> {
323 let axes = font.mm_axes()?;
324 let weight_axis = axes.first()?;
325 let width_axis = axes.get(1)?;
326
327 let weight = if params.weight == 0 {
328 weight_axis.default
329 } else {
330 params.weight as f32
331 };
332
333 if params.dest_width == 0 {
334 return font.instantiate(&[weight, width_axis.default]);
335 }
336
337 let upem = font.units_per_em();
338 let probe = |coord: f32| -> Option<i32> {
339 let inst = font.instantiate(&[weight, coord])?;
340 let adv = inst.advance(gid.into())?;
341 Some(em_adjust(adv as i32, upem))
342 };
343 let (lo, hi) = (width_axis.min, width_axis.max);
344 let min_w = probe(lo)?;
345 let max_w = probe(hi)?;
346 if max_w == min_w {
347 return font.instantiate(&[weight, hi]);
349 }
350 let t = (params.dest_width - min_w) as f32 / (max_w - min_w) as f32;
351 font.instantiate(&[weight, (hi - lo).mul_add(t, lo)])
352 }
353
354 #[must_use]
356 pub fn postscript_name(&self) -> Option<String> {
357 match self {
358 Self::Fontations(f) => f.postscript_name(),
359 Self::Type1(f) => f
360 .postscript_name()
361 .map(ToOwned::to_owned)
362 .or_else(|| f.family_name().map(ToOwned::to_owned)),
363 Self::None => None,
364 }
365 }
366
367 #[must_use]
369 pub fn is_fixed_pitch(&self) -> bool {
370 match self {
371 Self::Fontations(f) => f.is_fixed_pitch(),
372 Self::Type1(f) => f.is_fixed_pitch(),
373 Self::None => false,
374 }
375 }
376
377 #[must_use]
379 pub fn is_italic(&self) -> bool {
380 match self {
381 Self::Fontations(f) => f.is_italic(),
382 Self::Type1(f) => f.italic_angle() != 0.0,
383 Self::None => false,
384 }
385 }
386
387 #[must_use]
389 pub fn is_bold(&self) -> bool {
390 match self {
391 Self::Fontations(f) => f.is_bold(),
392 Self::Type1(f) => {
393 let name = f.postscript_name().or_else(|| f.full_name()).unwrap_or("");
394 name.contains("Bold") || name.contains("Black")
395 }
396 Self::None => false,
397 }
398 }
399
400 #[must_use]
402 pub fn cap_height_unscaled(&self) -> Option<f32> {
403 match self {
404 Self::Fontations(f) => f.cap_height(),
405 Self::Type1(_) | Self::None => None,
406 }
407 }
408
409 #[must_use]
411 pub fn unscaled_ascent(&self) -> Option<i32> {
412 match self {
413 Self::Fontations(f) => f.metrics().and_then(|m| i32::try_from(m.ascender).ok()),
414 Self::Type1(f) => Some(f.bbox().y1 as i32),
415 Self::None => None,
416 }
417 }
418
419 #[must_use]
421 pub fn unscaled_descent(&self) -> Option<i32> {
422 match self {
423 Self::Fontations(f) => f.metrics().and_then(|m| i32::try_from(m.descender).ok()),
424 Self::Type1(f) => Some(f.bbox().y0 as i32),
425 Self::None => None,
426 }
427 }
428
429 #[must_use]
431 pub fn unscaled_bbox(&self) -> Option<(i32, i32, i32, i32)> {
432 match self {
433 Self::Fontations(f) => {
434 let m = f.metrics()?;
435 Some((
436 i32::try_from(m.bbox_left).ok()?,
437 i32::try_from(m.bbox_bottom).ok()?,
438 i32::try_from(m.bbox_right).ok()?,
439 i32::try_from(m.bbox_top).ok()?,
440 ))
441 }
442 Self::Type1(f) => {
443 let b = f.bbox();
444 Some((b.x0 as i32, b.y0 as i32, b.x1 as i32, b.y1 as i32))
445 }
446 Self::None => None,
447 }
448 }
449
450 #[must_use]
454 pub fn unicode_mappings(&self, max: u32) -> Vec<(u32, u16)> {
455 match self {
456 Self::Fontations(f) => f.unicode_mappings(max),
457 Self::Type1(f) => {
458 let mut out: Vec<(u32, u16)> = f
459 .unicode_pairs()
460 .filter(|(ch, gid)| u32::from(*ch) <= max && gid.0 != 0)
461 .map(|(ch, gid)| (u32::from(ch), gid.0))
462 .collect();
463 out.sort_unstable_by_key(|(cp, _)| *cp);
464 out
465 }
466 Self::None => Vec::new(),
467 }
468 }
469}
470
471fn trim_empty_contours(path: BezPath) -> Option<BezPath> {
479 use pdfrum_common::kurbo::PathEl;
480
481 let mut els: Vec<PathEl> = path.into_iter().collect();
482 loop {
483 let end = els
485 .iter()
486 .rposition(|e| !matches!(e, PathEl::ClosePath))
487 .map_or(0, |i| i + 1);
488
489 if end >= 2
491 && let (Some(PathEl::MoveTo(a)), Some(PathEl::LineTo(b))) =
492 (els.get(end - 2), els.get(end - 1))
493 && a == b
494 {
495 els.truncate(end - 2);
496 continue;
497 }
498 if end >= 4
500 && let (
501 Some(PathEl::MoveTo(a)),
502 Some(PathEl::CurveTo(_, _, b)),
503 Some(PathEl::CurveTo(_, _, c)),
504 Some(PathEl::CurveTo(_, _, d)),
505 ) = (
506 els.get(end - 4),
507 els.get(end - 3),
508 els.get(end - 2),
509 els.get(end - 1),
510 )
511 && a == b
512 && b == c
513 && c == d
514 {
515 els.truncate(end - 4);
516 continue;
517 }
518 break;
519 }
520 if els.iter().all(|e| matches!(e, PathEl::ClosePath)) {
521 return None;
522 }
523 let out = BezPath::from_vec(els);
524 if out.elements().is_empty() {
525 None
526 } else {
527 Some(out)
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use pdfrum_common::kurbo::{PathEl, Point};
535
536 #[test]
537 fn a_move_and_a_line_back_to_it_is_trimmed() {
538 let mut p = BezPath::new();
539 p.move_to((10.0, 10.0));
540 p.line_to((50.0, 10.0));
541 p.line_to((50.0, 50.0));
542 p.close_path();
543 p.move_to((7.0, 7.0));
544 p.line_to((7.0, 7.0));
545 let trimmed = trim_empty_contours(p).expect("the real contour survives");
546 assert_eq!(trimmed.elements().len(), 4);
547 assert!(matches!(
548 trimmed.elements().first(),
549 Some(PathEl::MoveTo(_))
550 ));
551 }
552
553 #[test]
554 fn a_move_and_three_curves_to_it_is_trimmed() {
555 let mut p = BezPath::new();
556 p.move_to((0.0, 0.0));
557 p.line_to((10.0, 0.0));
558 p.close_path();
559 let q = Point::new(3.0, 3.0);
560 p.move_to(q);
561 for _ in 0..3 {
562 p.curve_to(q, q, q);
563 }
564 let trimmed = trim_empty_contours(p).expect("the real contour survives");
565 assert_eq!(trimmed.elements().len(), 3);
566 }
567
568 #[test]
569 fn repeated_degenerate_contours_are_all_trimmed() {
570 let mut p = BezPath::new();
571 p.move_to((0.0, 0.0));
572 p.line_to((10.0, 0.0));
573 p.close_path();
574 for i in 0..3 {
575 let q = Point::new(f64::from(i), f64::from(i));
576 p.move_to(q);
577 p.line_to(q);
578 }
579 let trimmed = trim_empty_contours(p).expect("the real contour survives");
580 assert_eq!(trimmed.elements().len(), 3);
581 }
582
583 #[test]
584 fn an_entirely_degenerate_outline_is_none_not_an_empty_path() {
585 let mut p = BezPath::new();
586 p.move_to((5.0, 5.0));
587 p.line_to((5.0, 5.0));
588 assert!(trim_empty_contours(p).is_none());
589 assert!(trim_empty_contours(BezPath::new()).is_none());
590 }
591
592 #[test]
593 fn a_healthy_outline_is_untouched() {
594 let mut p = BezPath::new();
595 p.move_to((0.0, 0.0));
596 p.curve_to((10.0, 0.0), (10.0, 10.0), (0.0, 10.0));
597 p.close_path();
598 let n = p.elements().len();
599 assert_eq!(trim_empty_contours(p).map(|q| q.elements().len()), Some(n));
600 }
601
602 #[test]
603 fn only_a_composite_carrying_bytecode_is_reported_as_instructed() {
604 let bytes = crate::testfonts::load("tt_composite_instructions.ttf");
605 let face = Face::new(bytes.into(), 0).expect("the fixture loads");
606 assert!(!face.composite_is_instructed(Gid(1)));
609 assert!(!face.composite_is_instructed(Gid(2)));
610 assert!(face.composite_is_instructed(Gid(3)));
611 }
612
613 #[test]
614 fn a_gid_past_the_end_of_loca_is_not_instructed() {
615 let bytes = crate::testfonts::load("tt_composite_instructions.ttf");
616 let face = Face::new(bytes.into(), 0).expect("the fixture loads");
617 assert!(!face.composite_is_instructed(Gid(u16::MAX)));
618 }
619
620 #[test]
621 fn the_empty_source_answers_everything_with_nothing() {
622 let s = GlyphSource::None;
623 assert!(!s.is_some());
624 assert_eq!(s.units_per_em(), 0);
625 assert_eq!(s.num_glyphs(), 0);
626 assert!(!s.is_truetype());
627 assert_eq!(s.char_index(Charmap::Unicode, 0x41), 0);
628 assert_eq!(s.name_index(b"A"), 0);
629 assert!(s.glyph_name(Gid(0)).is_none());
630 assert!(!s.has_glyph_names());
631 assert!(s.charmaps().is_empty());
632 assert!(s.outline(Gid(0), GlyphParams::default()).is_none());
633 assert_eq!(s.advance(Gid(0), GlyphParams::default()), 0);
634 assert_eq!(s.advance_tt(Gid(0)), 0);
635 assert!(s.glyph_bbox(Gid(0)).is_none());
636 }
637}