1use crate::Gid;
11use pdfrum_common::kurbo::{BezPath, Rect};
12use read_fonts::TableProvider;
13use read_fonts::tables::cmap::PlatformId;
14use skrifa::MetadataProvider;
15use skrifa::instance::{LocationRef, Size};
16use skrifa::outline::{
17 DrawSettings, Engine as HintingEngine, HintingInstance, HintingOptions, OutlinePen,
18 Target as HintingTarget,
19};
20use std::collections::HashMap;
21use std::fmt;
22use std::sync::{Arc, OnceLock, RwLock};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct CharmapId {
33 pub platform: u16,
35 pub encoding: u16,
37}
38
39impl CharmapId {
40 pub const WINDOWS_UNICODE: Self = Self {
42 platform: 3,
43 encoding: 1,
44 };
45 pub const WINDOWS_SYMBOL: Self = Self {
47 platform: 3,
48 encoding: 0,
49 };
50 pub const MAC_ROMAN: Self = Self {
52 platform: 1,
53 encoding: 0,
54 };
55 pub const UNICODE_SYNTHETIC: Self = Self {
57 platform: 0,
58 encoding: 3,
59 };
60 pub const ADOBE_CUSTOM: Self = Self {
63 platform: 4,
64 encoding: 0,
65 };
66
67 #[must_use]
73 pub fn is_unicode(self) -> bool {
74 self.platform == 0 || (self.platform == 3 && (self.encoding == 1 || self.encoding == 10))
75 }
76
77 #[must_use]
80 pub(crate) fn face_encoding(self) -> crate::encoding::FaceEncoding {
81 use crate::encoding::FaceEncoding as E;
82 match (self.platform, self.encoding) {
83 (0, _) | (3, 1 | 10) => E::Unicode,
84 (3, 0) => E::Symbol,
85 (1, 0) => E::AppleRoman,
86 (4, _) => E::AdobeCustom,
87 _ => E::Other,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
97pub enum Charmap {
98 #[default]
100 Unicode,
101 Index(usize),
103 None,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115enum Backend {
116 Sfnt,
118 BareCff,
120}
121
122#[derive(Clone)]
129pub struct Face {
130 bytes: Arc<[u8]>,
131 index: u32,
132 backend: Backend,
133 upem: u16,
134 num_glyphs: u32,
135 is_truetype: bool,
136 charmaps: Vec<CharmapId>,
137 hinting: Arc<OnceLock<Option<HintingInstance>>>,
156 names: Arc<OnceLock<HashMap<Box<[u8]>, u16>>>,
160 advances: Arc<RwLock<HashMap<Gid, Option<f32>>>>,
179 boxes: Arc<RwLock<HashMap<Gid, Option<Rect>>>>,
190}
191
192impl fmt::Debug for Face {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.debug_struct("Face")
195 .field("bytes", &format_args!("{} bytes", self.bytes.len()))
196 .field("index", &self.index)
197 .field("backend", &self.backend)
198 .field("upem", &self.upem)
199 .field("num_glyphs", &self.num_glyphs)
200 .field("is_truetype", &self.is_truetype)
201 .field("charmaps", &self.charmaps)
202 .field("hinting", &self.hinting.get().map(Option::is_some))
203 .field("names", &self.names.get().map(HashMap::len))
204 .field(
205 "advances",
206 &self.advances.read().map(|cache| cache.len()).ok(),
207 )
208 .field("boxes", &self.boxes.read().map(|cache| cache.len()).ok())
209 .finish()
210 }
211}
212
213impl Face {
214 #[must_use]
221 pub fn new(bytes: Arc<[u8]>, index: u32) -> Option<Self> {
222 Self::from_sfnt(&bytes, index).or_else(|| Self::from_bare_cff(bytes, index))
227 }
228
229 fn from_sfnt(bytes: &Arc<[u8]>, index: u32) -> Option<Self> {
231 let font = skrifa::FontRef::from_index(bytes.as_ref(), index).ok()?;
232 let upem = font.head().map_or(0, |h| h.units_per_em());
233 let num_glyphs = u32::from(font.maxp().ok()?.num_glyphs());
234 let is_truetype = font.glyf().is_ok();
238 let charmaps: Vec<CharmapId> = font
239 .cmap()
240 .map(|cmap| {
241 cmap.encoding_records()
242 .iter()
243 .map(|rec| CharmapId {
244 platform: platform_ordinal(rec.platform_id()),
245 encoding: rec.encoding_id(),
246 })
247 .collect()
248 })
249 .unwrap_or_default();
250 Some(Self {
251 bytes: Arc::clone(bytes),
252 index,
253 backend: Backend::Sfnt,
254 upem,
255 num_glyphs,
256 is_truetype,
257 charmaps,
258 hinting: Arc::default(),
259 names: Arc::default(),
260 advances: Arc::default(),
261 boxes: Arc::default(),
262 })
263 }
264
265 fn from_bare_cff(bytes: Arc<[u8]>, index: u32) -> Option<Self> {
273 let cff = read_fonts::ps::cff::CffFontRef::new(bytes.as_ref(), 0, None).ok()?;
274 let num_glyphs = cff.num_glyphs();
275 let upem = u16::try_from(cff.upem()).unwrap_or(1000);
276 Some(Self {
277 bytes,
278 index,
279 backend: Backend::BareCff,
280 upem,
281 num_glyphs,
282 is_truetype: false,
284 charmaps: vec![CharmapId::UNICODE_SYNTHETIC, CharmapId::ADOBE_CUSTOM],
285 hinting: Arc::default(),
286 names: Arc::default(),
287 advances: Arc::default(),
288 boxes: Arc::default(),
289 })
290 }
291
292 fn cff(&self) -> Option<read_fonts::ps::cff::CffFontRef<'_>> {
294 if self.backend != Backend::BareCff {
295 return None;
296 }
297 read_fonts::ps::cff::CffFontRef::new(&self.bytes, 0, None).ok()
298 }
299
300 fn cff_glyph_id(
310 cff: &read_fonts::ps::cff::CffFontRef<'_>,
311 gid: Gid,
312 ) -> read_fonts::types::GlyphId {
313 let raw = read_fonts::types::GlyphId::new(u32::from(gid.0));
314 if !cff.is_cid() {
315 return raw;
316 }
317 cff.charset()
319 .and_then(|charset| {
320 charset
321 .glyph_id(read_fonts::ps::string::Sid::new(gid.0))
322 .ok()
323 })
324 .unwrap_or(raw)
325 }
326
327 #[must_use]
329 pub fn units_per_em(&self) -> u16 {
330 self.upem
331 }
332
333 #[must_use]
335 pub fn num_glyphs(&self) -> u32 {
336 self.num_glyphs
337 }
338
339 #[must_use]
341 pub fn is_truetype(&self) -> bool {
342 self.is_truetype
343 }
344
345 #[must_use]
348 pub fn charmaps(&self) -> Vec<CharmapId> {
349 self.charmaps.clone()
350 }
351
352 #[must_use]
354 pub fn char_index(&self, charmap: Charmap, code: u32) -> u16 {
355 if let Some(cff) = self.cff() {
356 let gid = match charmap {
361 Charmap::None => None,
362 Charmap::Unicode => self.cff_unicode_to_gid(code),
363 Charmap::Index(_) => u8::try_from(code).ok().and_then(|b| cff.encoding()?.map(b)),
364 };
365 return gid
366 .and_then(|g| u16::try_from(g.to_u32()).ok())
367 .unwrap_or(0);
368 }
369
370 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
371 return 0;
372 };
373 let gid = match charmap {
374 Charmap::None => None,
375 Charmap::Unicode => font.charmap().map(code),
376 Charmap::Index(i) => font
377 .cmap()
378 .ok()
379 .and_then(|cmap| {
380 let rec = cmap.encoding_records().get(i)?;
381 rec.subtable(cmap.offset_data()).ok()
382 })
383 .and_then(|sub| sub.map_codepoint(code)),
384 };
385 gid.and_then(|g| u16::try_from(g.to_u32()).ok())
386 .unwrap_or(0)
387 }
388
389 fn cff_unicode_to_gid(&self, code: u32) -> Option<read_fonts::types::GlyphId> {
391 let ch = char::from_u32(code)?;
392 let mut buf = [0u8; read_fonts::ps::agl::MAX_NAME_LEN];
393 let name = read_fonts::ps::agl::char_to_name(u32::from(ch), &mut buf)?;
394 let gid = self.name_index(name);
395 (gid != 0).then(|| read_fonts::types::GlyphId::new(u32::from(gid)))
396 }
397
398 fn build_name_map(&self) -> HashMap<Box<[u8]>, u16> {
402 if let Some(cff) = self.cff() {
403 let Some(charset) = cff.charset() else {
404 return HashMap::new();
405 };
406 let mut map = HashMap::new();
407 for gid in 0..self.num_glyphs {
408 let Ok(g) = u16::try_from(gid) else { break };
409 let Ok(sid) = charset.string_id(read_fonts::types::GlyphId::new(gid)) else {
410 continue;
411 };
412 if let Some(bytes) = cff.string(sid) {
413 map.entry(bytes.into()).or_insert(g);
414 }
415 }
416 return map;
417 }
418 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
419 return HashMap::new();
420 };
421 let Ok(post) = font.post() else {
422 return HashMap::new();
423 };
424 let default_names = &read_fonts::tables::post::DEFAULT_GLYPH_NAMES;
425 let mut map = HashMap::new();
426 if post.version() == read_fonts::types::Version16Dot16::VERSION_1_0 {
427 for (gid, name) in default_names
428 .iter()
429 .enumerate()
430 .take(self.num_glyphs as usize)
431 {
432 let Ok(g) = u16::try_from(gid) else { break };
433 map.entry(name.as_bytes().into()).or_insert(g);
434 }
435 return map;
436 }
437 if post.version() != read_fonts::types::Version16Dot16::VERSION_2_0 {
438 return map;
439 }
440 let Some(index) = post.glyph_name_index() else {
441 return map;
442 };
443 let strings: Vec<&str> = post
446 .string_data()
447 .map(|d| d.iter().map_while(Result::ok).map(|s| s.as_str()).collect())
448 .unwrap_or_default();
449 for gid in 0..self.num_glyphs {
450 let Ok(g) = u16::try_from(gid) else { break };
451 let Some(idx) = index.get(gid as usize) else {
452 break;
453 };
454 let idx = usize::from(idx.get());
455 let name = if idx < default_names.len() {
456 default_names.get(idx).copied()
457 } else {
458 strings.get(idx - default_names.len()).copied()
459 };
460 if let Some(name) = name {
461 map.entry(name.as_bytes().into()).or_insert(g);
462 }
463 }
464 map
465 }
466
467 #[must_use]
469 pub fn name_index(&self, name: &str) -> u16 {
470 self.names
471 .get_or_init(|| self.build_name_map())
472 .get(name.as_bytes())
473 .copied()
474 .unwrap_or(0)
475 }
476
477 #[cfg(test)]
480 pub(crate) fn name_index_by_scan(&self, name: &str) -> u16 {
481 if let Some(cff) = self.cff() {
482 let Some(charset) = cff.charset() else {
483 return 0;
484 };
485 for gid in 0..self.num_glyphs {
486 let Ok(g) = u16::try_from(gid) else { break };
487 let id = read_fonts::types::GlyphId::new(gid);
488 let Ok(sid) = charset.string_id(id) else {
489 continue;
490 };
491 if cff.string(sid) == Some(name.as_bytes()) {
492 return g;
493 }
494 }
495 return 0;
496 }
497 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
498 return 0;
499 };
500 let Ok(post) = font.post() else { return 0 };
501 for gid in 0..self.num_glyphs {
502 let Ok(g) = u16::try_from(gid) else { break };
503 if post.glyph_name(read_fonts::types::GlyphId16::new(g)) == Some(name) {
504 return g;
505 }
506 }
507 0
508 }
509
510 #[must_use]
512 pub fn glyph_name(&self, gid: Gid) -> Option<String> {
513 if let Some(cff) = self.cff() {
514 let sid = cff
515 .charset()?
516 .string_id(read_fonts::types::GlyphId::new(u32::from(gid.0)))
517 .ok()?;
518 return cff
519 .string(sid)
520 .and_then(|b| std::str::from_utf8(b).ok())
521 .map(ToOwned::to_owned);
522 }
523 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
524 font.post()
525 .ok()?
526 .glyph_name(read_fonts::types::GlyphId16::new(gid.0))
527 .map(ToOwned::to_owned)
528 }
529
530 #[must_use]
532 pub fn has_glyph_names(&self) -> bool {
533 if self.backend == Backend::BareCff {
534 return self.cff().and_then(|c| c.charset()).is_some();
536 }
537 skrifa::FontRef::from_index(&self.bytes, self.index)
538 .ok()
539 .and_then(|f| f.post().ok())
540 .is_some_and(|p| p.glyph_name(read_fonts::types::GlyphId16::new(0)).is_some())
541 }
542
543 pub(crate) const HINT_PPEM: f32 = 64.0;
561
562 #[must_use]
583 pub(crate) fn hinted_outline(&self, gid: Gid) -> Option<BezPath> {
584 let instance = self.hinting_instance()?;
585 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
586 let glyph = font
587 .outline_glyphs()
588 .get(skrifa::GlyphId::new(u32::from(gid.0)))?;
589 let mut pen = PathPen::default();
590 glyph
591 .draw(DrawSettings::hinted(instance, false), &mut pen)
592 .ok()?;
593 Some(pen.path)
594 }
595
596 fn hinting_instance(&self) -> Option<&HintingInstance> {
598 self.hinting
599 .get_or_init(|| {
600 if self.backend != Backend::Sfnt {
601 return None;
602 }
603 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
604 HintingInstance::new(
613 &font.outline_glyphs(),
614 Size::new(Self::HINT_PPEM),
615 LocationRef::default(),
616 HintingOptions {
617 engine: HintingEngine::Interpreter,
618 target: HintingTarget::default(),
619 },
620 )
621 .ok()
622 })
623 .as_ref()
624 }
625
626 #[must_use]
640 pub(crate) fn composite_is_instructed(&self, gid: Gid) -> bool {
641 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
642 return false;
643 };
644 let (Ok(glyf), Ok(loca)) = (font.glyf(), font.loca(None)) else {
645 return false;
646 };
647 let raw = read_fonts::types::GlyphId::new(u32::from(gid.0));
648 match loca.get_glyf(raw, &glyf) {
649 Ok(Some(read_fonts::tables::glyf::Glyph::Composite(c))) => {
650 c.count_and_instructions().1.is_some_and(|i| !i.is_empty())
651 }
652 _ => false,
653 }
654 }
655
656 #[must_use]
666 pub(crate) fn outline(&self, gid: Gid) -> Option<BezPath> {
667 let mut pen = PathPen::default();
668 if let Some(cff) = self.cff() {
669 let id = Self::cff_glyph_id(&cff, gid);
670 let subfont_index = cff.subfont_index(id)?;
671 let subfont = cff.subfont(subfont_index, &[]).ok()?;
672 cff.draw(&subfont, id, &[], None, &mut pen).ok()?;
675 return Some(pen.path);
676 }
677 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
678 let glyph = font
679 .outline_glyphs()
680 .get(skrifa::GlyphId::new(u32::from(gid.0)))?;
681 glyph
682 .draw(
683 DrawSettings::unhinted(Size::unscaled(), LocationRef::default()),
684 &mut pen,
685 )
686 .ok()?;
687 Some(pen.path)
688 }
689
690 #[must_use]
695 pub(crate) fn advance(&self, gid: Gid) -> Option<f32> {
696 if let Ok(cache) = self.advances.read()
697 && let Some(hit) = cache.get(&gid)
698 {
699 return *hit;
700 }
701 let computed = self.advance_uncached(gid);
702 if let Ok(mut cache) = self.advances.write() {
703 cache.insert(gid, computed);
704 }
705 computed
706 }
707
708 #[must_use]
710 fn advance_uncached(&self, gid: Gid) -> Option<f32> {
711 if let Some(cff) = self.cff() {
712 let id = Self::cff_glyph_id(&cff, gid);
713 let subfont_index = cff.subfont_index(id)?;
714 let subfont = cff.subfont(subfont_index, &[]).ok()?;
715 let mut pen = PathPen::default();
716 return cff.draw(&subfont, id, &[], None, &mut pen).ok().flatten();
717 }
718 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
719 font.glyph_metrics(Size::unscaled(), LocationRef::default())
720 .advance_width(skrifa::GlyphId::new(u32::from(gid.0)))
721 }
722
723 #[must_use]
733 pub(crate) fn glyph_bbox(&self, gid: Gid) -> Option<Rect> {
734 if let Ok(cache) = self.boxes.read()
735 && let Some(hit) = cache.get(&gid)
736 {
737 return *hit;
738 }
739 let computed = self.glyph_bbox_uncached(gid);
740 if let Ok(mut cache) = self.boxes.write() {
741 cache.insert(gid, computed);
742 }
743 computed
744 }
745
746 #[must_use]
748 fn glyph_bbox_uncached(&self, gid: Gid) -> Option<Rect> {
749 if self.backend != Backend::BareCff {
750 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
751 if let Some(b) = font
752 .glyph_metrics(Size::unscaled(), LocationRef::default())
753 .bounds(skrifa::GlyphId::new(u32::from(gid.0)))
754 {
755 return Some(Rect::new(
756 f64::from(b.x_min),
757 f64::from(b.y_min),
758 f64::from(b.x_max),
759 f64::from(b.y_max),
760 ));
761 }
762 }
763 let path = self.outline(gid)?;
764 let b = pdfrum_common::kurbo::Shape::bounding_box(&path);
765 (b.width() > 0.0 || b.height() > 0.0).then_some(b)
766 }
767
768 #[must_use]
770 pub(crate) fn metrics(&self) -> Option<crate::descriptor::FaceMetrics> {
771 if self.backend == Backend::BareCff {
772 return None;
776 }
777 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
778 let head = font.head().ok()?;
779 let hhea = font.hhea().ok()?;
780 Some(crate::descriptor::FaceMetrics {
781 upem: head.units_per_em(),
782 bbox_left: i64::from(head.x_min()),
783 bbox_top: i64::from(head.y_max()),
784 bbox_right: i64::from(head.x_max()),
785 bbox_bottom: i64::from(head.y_min()),
786 ascender: i64::from(hhea.ascender().to_i16()),
787 descender: i64::from(hhea.descender().to_i16()),
788 })
789 }
790
791 #[must_use]
793 pub(crate) fn bytes(&self) -> &Arc<[u8]> {
794 &self.bytes
795 }
796
797 #[must_use]
799 pub(crate) fn index(&self) -> u32 {
800 self.index
801 }
802
803 #[must_use]
807 pub(crate) fn display_name(&self) -> Option<String> {
808 if let Some(cff) = self.cff() {
809 let meta = cff.metadata()?;
810 return meta
811 .family_name()
812 .or_else(|| meta.name())
813 .map(ToOwned::to_owned);
814 }
815 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
816 let strings = font.localized_strings(skrifa::string::StringId::FAMILY_NAME);
817 let family: String = strings.english_or_first()?.chars().collect();
818 if family.is_empty() {
819 return None;
820 }
821 let style: String = font
822 .localized_strings(skrifa::string::StringId::SUBFAMILY_NAME)
823 .english_or_first()
824 .map(|s| s.chars().collect())
825 .unwrap_or_default();
826 if style.is_empty() || style == "Regular" {
827 Some(family)
828 } else {
829 Some(format!("{family} {style}"))
830 }
831 }
832
833 #[must_use]
835 pub fn postscript_name(&self) -> Option<String> {
836 if let Some(cff) = self.cff() {
837 let meta = cff.metadata()?;
838 return meta
839 .name()
840 .or_else(|| meta.family_name())
841 .map(ToOwned::to_owned);
842 }
843 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
844 let ps: String = font
845 .localized_strings(skrifa::string::StringId::POSTSCRIPT_NAME)
846 .english_or_first()
847 .map(|s| s.chars().collect())
848 .unwrap_or_default();
849 if !ps.is_empty() {
850 return Some(ps);
851 }
852 self.display_name()
853 }
854
855 #[must_use]
857 pub fn is_fixed_pitch(&self) -> bool {
858 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
859 return false;
860 };
861 font.post().is_ok_and(|p| p.is_fixed_pitch() != 0)
862 }
863
864 #[must_use]
866 pub fn is_italic(&self) -> bool {
867 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
868 return false;
869 };
870 if let Ok(os2) = font.os2() {
871 let sel = os2.fs_selection();
872 if sel.contains(read_fonts::tables::os2::SelectionFlags::ITALIC)
873 || sel.contains(read_fonts::tables::os2::SelectionFlags::OBLIQUE)
874 {
875 return true;
876 }
877 }
878 if font.head().is_ok_and(|h| {
879 h.mac_style()
880 .contains(read_fonts::tables::head::MacStyle::ITALIC)
881 }) {
882 return true;
883 }
884 font.post().is_ok_and(|p| p.italic_angle().to_f64() != 0.0)
885 }
886
887 #[must_use]
889 pub fn is_bold(&self) -> bool {
890 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
891 return false;
892 };
893 if let Ok(os2) = font.os2() {
894 if os2
895 .fs_selection()
896 .contains(read_fonts::tables::os2::SelectionFlags::BOLD)
897 {
898 return true;
899 }
900 if os2.us_weight_class() >= 700 {
901 return true;
902 }
903 }
904 font.head().is_ok_and(|h| {
905 h.mac_style()
906 .contains(read_fonts::tables::head::MacStyle::BOLD)
907 })
908 }
909
910 #[must_use]
912 pub fn cap_height(&self) -> Option<f32> {
913 let font = skrifa::FontRef::from_index(&self.bytes, self.index).ok()?;
914 font.os2().ok()?.s_cap_height().map(f32::from)
915 }
916
917 #[must_use]
922 pub fn unicode_mappings(&self, max: u32) -> Vec<(u32, u16)> {
923 if self.backend == Backend::BareCff {
924 return (0..=max)
925 .filter_map(|cp| {
926 let gid = self.char_index(Charmap::Unicode, cp);
927 (gid != 0).then_some((cp, gid))
928 })
929 .collect();
930 }
931 let Ok(font) = skrifa::FontRef::from_index(&self.bytes, self.index) else {
932 return Vec::new();
933 };
934 let mut out: Vec<(u32, u16)> = font
935 .charmap()
936 .mappings()
937 .filter_map(|(cp, gid)| {
938 if cp > max {
939 return None;
940 }
941 let g = u16::try_from(gid.to_u32()).ok()?;
942 (g != 0).then_some((cp, g))
943 })
944 .collect();
945 out.sort_unstable_by_key(|(cp, _)| *cp);
946 out.dedup_by_key(|(cp, _)| *cp);
947 out
948 }
949}
950
951fn platform_ordinal(p: PlatformId) -> u16 {
952 match p {
953 PlatformId::Unicode => 0,
954 PlatformId::Macintosh => 1,
955 PlatformId::ISO => 2,
956 PlatformId::Windows => 3,
957 PlatformId::Custom => 4,
958 PlatformId::Unknown => u16::MAX,
960 }
961}
962
963#[derive(Default)]
969struct PathPen {
970 path: BezPath,
971 current: (f32, f32),
972 open: bool,
973}
974
975impl OutlinePen for PathPen {
976 fn move_to(&mut self, x: f32, y: f32) {
977 if self.open {
978 self.path.close_path();
979 }
980 self.path.move_to((f64::from(x), f64::from(y)));
981 self.current = (x, y);
982 self.open = true;
983 }
984
985 fn line_to(&mut self, x: f32, y: f32) {
986 if self.open {
987 self.path.line_to((f64::from(x), f64::from(y)));
988 self.current = (x, y);
989 }
990 }
991
992 fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
993 if !self.open {
994 return;
995 }
996 let (px, py) = self.current;
999 let c1 = (
1000 f64::from(px) + 2.0 / 3.0 * f64::from(cx0 - px),
1001 f64::from(py) + 2.0 / 3.0 * f64::from(cy0 - py),
1002 );
1003 let c2 = (
1004 f64::from(cx0) + f64::from(x - cx0) / 3.0,
1005 f64::from(cy0) + f64::from(y - cy0) / 3.0,
1006 );
1007 self.path.curve_to(c1, c2, (f64::from(x), f64::from(y)));
1008 self.current = (x, y);
1009 }
1010
1011 fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
1012 if !self.open {
1013 return;
1014 }
1015 self.path.curve_to(
1016 (f64::from(cx0), f64::from(cy0)),
1017 (f64::from(cx1), f64::from(cy1)),
1018 (f64::from(x), f64::from(y)),
1019 );
1020 self.current = (x, y);
1021 }
1022
1023 fn close(&mut self) {
1024 if self.open {
1025 self.path.close_path();
1026 self.open = false;
1027 }
1028 }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 #[test]
1036 fn the_name_map_answers_what_the_scan_answered() {
1037 let fixtures = [
1038 "tt_custom_40.ttf",
1039 "tt_macroman_10.ttf",
1040 "tt_macroman_empty.ttf",
1041 "tt_named_no_cmap.ttf",
1042 "tt_sjis_and_unicode.ttf",
1043 "tt_symbol_30.ttf",
1044 "tt_symbol_and_macroman.ttf",
1045 "tt_symbol_empty.ttf",
1046 "tt_unicode_03_and_symbol.ttf",
1047 "tt_unicode_03.ttf",
1048 "tt_unicode_31_and_symbol.ttf",
1049 "tt_unicode_31.ttf",
1050 ];
1051 let mut named_faces = 0;
1052 for fixture in fixtures {
1053 let bytes: Arc<[u8]> = crate::testfonts::load(fixture).into();
1054 let face = Face::new(bytes, 0).unwrap();
1055 let map = face.build_name_map();
1056 named_faces += usize::from(!map.is_empty());
1057 for (name, gid) in &map {
1058 let name = std::str::from_utf8(name).unwrap();
1059 let scanned = face.name_index_by_scan(name);
1060 assert_eq!(*gid, scanned, "{fixture}: {name}");
1061 assert_eq!(face.name_index(name), scanned, "{fixture}: {name}");
1062 }
1063 assert_eq!(face.name_index("nonesuch"), 0, "{fixture}");
1064 assert_eq!(face.name_index_by_scan("nonesuch"), 0, "{fixture}");
1065 }
1066 assert!(
1067 named_faces > 0,
1068 "no fixture carries glyph names; the pin proves nothing"
1069 );
1070 }
1071
1072 use super::*;
1073
1074 #[test]
1075 fn charmap_ids_classify_unicode_correctly() {
1076 assert!(CharmapId::WINDOWS_UNICODE.is_unicode());
1077 assert!(CharmapId::UNICODE_SYNTHETIC.is_unicode());
1078 assert!(
1079 CharmapId {
1080 platform: 3,
1081 encoding: 10
1082 }
1083 .is_unicode()
1084 );
1085 assert!(!CharmapId::WINDOWS_SYMBOL.is_unicode());
1088 assert!(!CharmapId::MAC_ROMAN.is_unicode());
1089 }
1090
1091 #[test]
1092 fn charmap_ids_map_to_face_encodings() {
1093 use crate::encoding::FaceEncoding as E;
1094 assert_eq!(CharmapId::WINDOWS_UNICODE.face_encoding(), E::Unicode);
1095 assert_eq!(CharmapId::WINDOWS_SYMBOL.face_encoding(), E::Symbol);
1096 assert_eq!(CharmapId::MAC_ROMAN.face_encoding(), E::AppleRoman);
1097 assert_eq!(CharmapId::ADOBE_CUSTOM.face_encoding(), E::AdobeCustom);
1098 assert_eq!(
1099 CharmapId {
1100 platform: 2,
1101 encoding: 7
1102 }
1103 .face_encoding(),
1104 E::Other
1105 );
1106 }
1107
1108 #[test]
1109 fn garbage_bytes_yield_no_face() {
1110 assert!(Face::new(Arc::from(&b""[..]), 0).is_none());
1111 assert!(Face::new(Arc::from(&b"not a font at all"[..]), 0).is_none());
1112 assert!(Face::new(Arc::from(vec![0u8; 4096].as_slice()), 0).is_none());
1113 }
1114
1115 #[test]
1116 fn a_foxit_base14_blob_reads_as_a_non_truetype_face() {
1117 let bytes: Arc<[u8]> = Arc::from(crate::subst::standard_font_data(
1118 crate::StandardFont::Helvetica,
1119 ));
1120 let face = Face::new(bytes, 0).expect("bare CFF is readable");
1121 assert!(!face.is_truetype(), "a bare CFF has no glyf table");
1122 assert!(face.num_glyphs() > 100);
1123 assert_eq!(face.units_per_em(), 1000);
1124 }
1125
1126 #[test]
1127 fn the_pen_elevates_quadratics_to_cubics() {
1128 let mut pen = PathPen::default();
1129 pen.move_to(0.0, 0.0);
1130 pen.quad_to(30.0, 60.0, 60.0, 0.0);
1131 pen.close();
1132 let els: Vec<_> = pen.path.into_iter().collect();
1133 assert_eq!(els.len(), 3);
1134 assert!(matches!(
1135 els.get(1),
1136 Some(pdfrum_common::kurbo::PathEl::CurveTo(..))
1137 ));
1138 }
1139
1140 #[test]
1141 fn the_pen_ignores_segments_before_any_move() {
1142 let mut pen = PathPen::default();
1143 pen.line_to(10.0, 10.0);
1144 pen.curve_to(1.0, 1.0, 2.0, 2.0, 3.0, 3.0);
1145 pen.close();
1146 assert!(pen.path.elements().is_empty());
1147 }
1148}