1use crate::cache::Cache;
4use crate::context::Context;
5use crate::device::Device;
6use crate::font::cid::Type0Font;
7use crate::font::generated::{
8 glyph_names, mac_expert, mac_os_roman, mac_roman, standard, tex_glyph_names, win_ansi,
9};
10use crate::font::true_type::TrueTypeFont;
11use crate::font::type1::Type1Font;
12use crate::font::type3::Type3;
13use crate::interpret::state::State;
14use crate::util::decode_or_warn;
15use crate::{CMapResolverFn, CacheKey, FontResolverFn, InterpreterSettings, Paint, WarningSinkFn};
16use bitflags::bitflags;
17use kurbo::{Affine, BezPath, Vec2};
18use log::warn;
19use outline::OutlineFont;
20use pdf_syntax::object::Name;
21use pdf_syntax::object::dict::keys::SUBTYPE;
22use pdf_syntax::object::dict::keys::*;
23use pdf_syntax::object::{Dict, Stream};
24use pdf_syntax::page::Resources;
25use pdf_syntax::xref::XRef;
26use skrifa::GlyphId;
27use std::fmt::Debug;
28use std::ops::Deref;
29use std::rc::Rc;
30use std::sync::Arc;
31
32mod blob;
33mod cid;
34mod generated;
35mod glyph_simulator;
36pub(crate) mod outline;
37mod standard_font;
38mod true_type;
39mod type1;
40pub(crate) mod type3;
41
42pub(crate) const UNITS_PER_EM: f32 = 1000.0;
43
44pub(crate) fn stretch_glyph(path: BezPath, expected_width: f32, actual_width: f32) -> BezPath {
45 if actual_width != 0.0 && actual_width != expected_width {
46 let stretch_factor = expected_width / actual_width;
47 Affine::scale_non_uniform(stretch_factor as f64, 1.0) * path
48 } else {
49 path
50 }
51}
52
53pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
55
56pub(crate) fn strip_subset_prefix(name: &str) -> &str {
62 match name.split_once('+') {
63 Some((prefix, rest)) if prefix.len() == 6 => rest,
64 _ => name,
65 }
66}
67
68use crate::util::hash128;
69pub use outline::OutlineFontData;
70use pdf_font::cmap::{BfString, CMap, CMapName, CharacterCollection};
71pub use standard_font::StandardFont;
72
73pub enum Glyph<'a> {
75 Outline(OutlineGlyph),
77 Type3(Box<Type3Glyph<'a>>),
79}
80
81impl Glyph<'_> {
82 pub fn as_unicode(&self) -> Option<BfString> {
104 match self {
105 Glyph::Outline(g) => g.as_unicode(),
106 Glyph::Type3(g) => g.as_unicode(),
107 }
108 }
109}
110
111#[derive(Clone, Debug)]
113pub struct GlyphIdentifier {
114 id: GlyphId,
115 font: OutlineFont,
116}
117
118impl CacheKey for GlyphIdentifier {
119 fn cache_key(&self) -> u128 {
120 hash128(&(self.id, self.font.cache_key()))
121 }
122}
123
124#[derive(Clone, Debug)]
126pub struct OutlineGlyph {
127 pub(crate) id: GlyphId,
128 pub(crate) font: OutlineFont,
129 pub(crate) char_code: u32,
130}
131
132impl OutlineGlyph {
133 pub fn outline(&self) -> BezPath {
135 self.font.outline_glyph(self.id, self.char_code)
136 }
137
138 pub fn identifier(&self) -> GlyphIdentifier {
144 GlyphIdentifier {
145 id: self.id,
146 font: self.font.clone(),
147 }
148 }
149
150 pub fn as_unicode(&self) -> Option<BfString> {
161 self.font
162 .char_code_to_unicode(self.char_code)
163 .and_then(if_meaningful)
164 }
165
166 pub fn font_data(&self) -> Option<OutlineFontData> {
170 self.font.font_data()
171 }
172
173 pub fn postscript_name(&self) -> Option<String> {
183 self.font.postscript_name()
184 }
185
186 pub fn glyph_id(&self) -> GlyphId {
188 self.id
189 }
190
191 pub fn advance_width(&self) -> Option<f32> {
196 self.font.glyph_advance_width(self.char_code)
197 }
198
199 pub fn font_metrics(&self) -> Option<(f64, f64, Option<f64>, Option<f64>)> {
207 self.font.font_metrics()
208 }
209
210 pub fn font_cache_key(&self) -> u128 {
215 self.font.cache_key()
216 }
217}
218
219#[derive(Clone)]
221pub struct Type3Glyph<'a> {
222 pub(crate) font: Rc<Type3<'a>>,
223 pub(crate) glyph_id: GlyphId,
224 pub(crate) state: State<'a>,
225 pub(crate) parent_resources: Resources<'a>,
226 pub(crate) cache: Cache,
227 pub(crate) xref: &'a XRef,
228 pub(crate) settings: InterpreterSettings,
229 pub(crate) char_code: u32,
230 pub(crate) nesting_depth: u32,
235}
236
237impl<'a> Type3Glyph<'a> {
239 pub fn interpret(
241 &self,
242 device: &mut impl Device<'a>,
243 transform: Affine,
244 glyph_transform: Affine,
245 paint: &Paint<'a>,
246 ) {
247 self.font
248 .render_glyph(self, transform, glyph_transform, paint, device);
249 }
250
251 pub fn as_unicode(&self) -> Option<BfString> {
258 self.font
259 .char_code_to_unicode(self.char_code)
260 .and_then(if_meaningful)
261 }
262}
263
264impl CacheKey for Type3Glyph<'_> {
265 fn cache_key(&self) -> u128 {
266 hash128(&(self.font.cache_key(), self.glyph_id))
267 }
268}
269
270#[derive(Clone, Debug)]
271pub(crate) struct Font<'a>(u128, FontType<'a>);
272
273impl<'a> Font<'a> {
274 pub(crate) fn new(
275 dict: &Dict<'a>,
276 font_resolver: &FontResolverFn,
277 cmap_resolver: &CMapResolverFn,
278 warning_sink: &WarningSinkFn,
279 ) -> Option<Self> {
280 let f_type = match dict.get::<Name>(SUBTYPE)?.deref() {
281 TYPE1 | MM_TYPE1 => FontType::Type1(Rc::new(Type1Font::new(
282 dict,
283 font_resolver,
284 cmap_resolver,
285 warning_sink,
286 )?)),
287 TRUE_TYPE | OPEN_TYPE => FontType::TrueType(Rc::new(TrueTypeFont::new(
289 dict,
290 font_resolver,
291 cmap_resolver,
292 warning_sink,
293 )?)),
294 TYPE0 => FontType::Type0(Rc::new(Type0Font::new(
295 dict,
296 font_resolver,
297 cmap_resolver,
298 warning_sink,
299 )?)),
300 TYPE3 => FontType::Type3(Rc::new(Type3::new(dict, cmap_resolver, warning_sink)?)),
301 f => {
302 warn!(
303 "unimplemented font type {:?}",
304 std::str::from_utf8(f).unwrap_or("unknown type")
305 );
306
307 return None;
308 }
309 };
310
311 let cache_key = dict.cache_key();
312
313 Some(Self(cache_key, f_type))
314 }
315
316 pub(crate) fn new_standard(
317 standard_font: StandardFont,
318 font_resolver: &FontResolverFn,
319 ) -> Option<Self> {
320 let font = Type1Font::new_standard(standard_font, font_resolver)?;
321
322 Some(Self(0, FontType::Type1(Rc::new(font))))
323 }
324
325 pub(crate) fn map_code(&self, code: u32) -> GlyphId {
326 match &self.1 {
327 FontType::Type1(f) => {
328 debug_assert!(code <= u8::MAX as u32);
329
330 f.map_code(code as u8)
331 }
332 FontType::TrueType(t) => {
333 debug_assert!(code <= u8::MAX as u32);
334
335 t.map_code(code as u8)
336 }
337 FontType::Type0(t) => t.map_code(code),
338 FontType::Type3(t) => {
339 debug_assert!(code <= u8::MAX as u32);
340
341 t.map_code(code as u8)
342 }
343 }
344 }
345
346 pub(crate) fn get_glyph(
347 &self,
348 glyph: GlyphId,
349 char_code: u32,
350 ctx: &mut Context<'a>,
351 resources: &Resources<'a>,
352 origin_displacement: Vec2,
353 ) -> (Glyph<'a>, Affine) {
354 let glyph_transform = ctx.get().text_state.full_transform()
355 * Affine::scale(1.0 / UNITS_PER_EM as f64)
356 * Affine::translate(origin_displacement);
357
358 let glyph = match &self.1 {
359 FontType::Type1(t) => {
360 let font = OutlineFont::Type1(t.clone());
361 Glyph::Outline(OutlineGlyph {
362 id: glyph,
363 font,
364 char_code,
365 })
366 }
367 FontType::TrueType(t) => {
368 let font = OutlineFont::TrueType(t.clone());
369 Glyph::Outline(OutlineGlyph {
370 id: glyph,
371 font,
372 char_code,
373 })
374 }
375 FontType::Type0(t) => {
376 let font = OutlineFont::Type0(t.clone());
377 Glyph::Outline(OutlineGlyph {
378 id: glyph,
379 font,
380 char_code,
381 })
382 }
383 FontType::Type3(t) => {
384 let shape_glyph = Type3Glyph {
385 nesting_depth: ctx.nesting_depth(),
386 font: t.clone(),
387 glyph_id: glyph,
388 state: ctx.get().clone(),
389 parent_resources: resources.clone(),
390 cache: ctx.object_cache.clone(),
391 xref: ctx.xref,
392 settings: ctx.settings.clone(),
393 char_code,
394 };
395
396 Glyph::Type3(Box::new(shape_glyph))
397 }
398 };
399
400 (glyph, glyph_transform)
401 }
402
403 pub(crate) fn code_advance(&self, code: u32) -> Vec2 {
404 match &self.1 {
405 FontType::Type1(t) => {
406 debug_assert!(code <= u8::MAX as u32);
407
408 Vec2::new(t.glyph_width(code as u8).unwrap_or(0.0) as f64, 0.0)
409 }
410 FontType::TrueType(t) => {
411 debug_assert!(code <= u8::MAX as u32);
412
413 Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
414 }
415 FontType::Type0(t) => t.code_advance(code),
416 FontType::Type3(t) => {
417 debug_assert!(code <= u8::MAX as u32);
418
419 Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
420 }
421 }
422 }
423
424 pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 {
425 match &self.1 {
426 FontType::Type1(_) => Vec2::default(),
427 FontType::TrueType(_) => Vec2::default(),
428 FontType::Type0(t) => t.origin_displacement(code),
429 FontType::Type3(_) => Vec2::default(),
430 }
431 }
432
433 pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) {
434 match &self.1 {
435 FontType::Type1(_) => (bytes[offset] as u32, 1),
436 FontType::TrueType(_) => (bytes[offset] as u32, 1),
437 FontType::Type0(t) => t.read_code(bytes, offset),
438 FontType::Type3(_) => (bytes[offset] as u32, 1),
439 }
440 }
441
442 pub(crate) fn is_horizontal(&self) -> bool {
443 match &self.1 {
444 FontType::Type1(_) => true,
445 FontType::TrueType(_) => true,
446 FontType::Type0(t) => t.is_horizontal(),
447 FontType::Type3(_) => true,
448 }
449 }
450}
451
452impl CacheKey for Font<'_> {
453 fn cache_key(&self) -> u128 {
454 self.0
455 }
456}
457
458#[derive(Clone, Debug)]
459enum FontType<'a> {
460 Type1(Rc<Type1Font>),
461 TrueType(Rc<TrueTypeFont>),
462 Type0(Rc<Type0Font>),
463 Type3(Rc<Type3<'a>>),
464}
465
466#[derive(Debug)]
467enum Encoding {
468 Standard,
469 MacRoman,
470 WinAnsi,
471 MacExpert,
472 BuiltIn,
473}
474
475impl Encoding {
476 fn map_code(&self, code: u8) -> Option<&'static str> {
477 if code == 0 {
478 return Some(".notdef");
479 }
480 match self {
481 Self::Standard => standard::get(code),
482 Self::MacRoman => mac_roman::get(code).or_else(|| mac_os_roman::get(code)),
483 Self::WinAnsi => win_ansi::get(code),
484 Self::MacExpert => mac_expert::get(code),
485 Self::BuiltIn => None,
486 }
487 }
488}
489
490#[derive(Debug, Copy, Clone)]
492pub enum FontStretch {
493 Normal,
495 UltraCondensed,
497 ExtraCondensed,
499 Condensed,
501 SemiCondensed,
503 SemiExpanded,
505 Expanded,
507 ExtraExpanded,
509 UltraExpanded,
511}
512
513impl FontStretch {
514 fn from_string(s: &str) -> Self {
515 match s {
516 "UltraCondensed" => Self::UltraCondensed,
517 "ExtraCondensed" => Self::ExtraCondensed,
518 "Condensed" => Self::Condensed,
519 "SemiCondensed" => Self::SemiCondensed,
520 "SemiExpanded" => Self::SemiExpanded,
521 "Expanded" => Self::Expanded,
522 "ExtraExpanded" => Self::ExtraExpanded,
523 "UltraExpanded" => Self::UltraExpanded,
524 _ => Self::Normal,
525 }
526 }
527}
528
529bitflags! {
530 #[derive(Debug)]
532 pub(crate) struct FontFlags: u32 {
533 const FIXED_PITCH = 1 << 0;
534 const SERIF = 1 << 1;
535 const SYMBOLIC = 1 << 2;
536 const SCRIPT = 1 << 3;
537 const NON_SYMBOLIC = 1 << 5;
538 const ITALIC = 1 << 6;
539 const ALL_CAP = 1 << 16;
540 const SMALL_CAP = 1 << 17;
541 const FORCE_BOLD = 1 << 18;
542 }
543}
544
545pub enum FontQuery {
547 Standard(StandardFont),
549 Fallback(FallbackFontQuery),
554}
555
556#[derive(Debug, Clone)]
558pub struct FallbackFontQuery {
559 pub post_script_name: Option<String>,
561 pub font_name: Option<String>,
563 pub font_family: Option<String>,
565 pub font_stretch: FontStretch,
567 pub font_weight: u32,
569 pub is_fixed_pitch: bool,
571 pub is_serif: bool,
573 pub is_italic: bool,
575 pub is_bold: bool,
577 pub is_small_cap: bool,
579 pub character_collection: Option<CharacterCollection>,
581}
582
583impl FallbackFontQuery {
584 pub(crate) fn new(dict: &Dict<'_>) -> Self {
585 let post_script_name = dict
586 .get::<Name>(BASE_FONT)
587 .map(|n| strip_subset_prefix(n.as_str()).to_string());
588
589 let mut data = Self {
590 post_script_name,
591 ..Default::default()
592 };
593
594 if let Some(descriptor) = dict.get::<Dict<'_>>(FONT_DESC) {
595 data.font_name = dict
596 .get::<Name>(FONT_NAME)
597 .map(|n| strip_subset_prefix(n.as_str()).to_string());
598 data.font_family = descriptor
599 .get::<Name>(FONT_FAMILY)
600 .map(|n| n.as_str().to_string());
601 data.font_stretch = descriptor
602 .get::<Name>(FONT_STRETCH)
603 .map(|n| FontStretch::from_string(n.as_str()))
604 .unwrap_or(FontStretch::Normal);
605 data.font_weight = descriptor.get::<u32>(FONT_WEIGHT).unwrap_or(400);
606
607 if let Some(flags) = descriptor
608 .get::<u32>(FLAGS)
609 .map(FontFlags::from_bits_truncate)
610 {
611 data.is_fixed_pitch = flags.contains(FontFlags::FIXED_PITCH);
612 data.is_serif = flags.contains(FontFlags::SERIF);
613 data.is_italic = flags.contains(FontFlags::ITALIC);
614 data.is_small_cap = flags.contains(FontFlags::SMALL_CAP);
615 }
616 }
617
618 data.is_bold |= data.font_weight >= 700;
619
620 if let Some(name) = &data.post_script_name {
621 let lower = name.to_ascii_lowercase();
622 data.is_italic |=
623 lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
624 data.is_bold |= lower.contains("bold")
625 || lower.contains("demi")
626 || lower.contains("semibold")
627 || lower.contains("heavy")
628 || lower.contains("black");
629 }
630
631 data
632 }
633
634 pub fn pick_standard_font(&self) -> StandardFont {
636 if self.is_fixed_pitch {
637 match (self.is_bold, self.is_italic) {
638 (true, true) => StandardFont::CourierBoldOblique,
639 (true, false) => StandardFont::CourierBold,
640 (false, true) => StandardFont::CourierOblique,
641 (false, false) => StandardFont::Courier,
642 }
643 } else if !self.is_serif {
644 match (self.is_bold, self.is_italic) {
645 (true, true) => StandardFont::HelveticaBoldOblique,
646 (true, false) => StandardFont::HelveticaBold,
647 (false, true) => StandardFont::HelveticaOblique,
648 (false, false) => StandardFont::Helvetica,
649 }
650 } else {
651 match (self.is_bold, self.is_italic) {
652 (true, true) => StandardFont::TimesBoldItalic,
653 (true, false) => StandardFont::TimesBold,
654 (false, true) => StandardFont::TimesItalic,
655 (false, false) => StandardFont::TimesRoman,
656 }
657 }
658 }
659}
660
661impl Default for FallbackFontQuery {
662 fn default() -> Self {
663 Self {
664 post_script_name: None,
665 font_name: None,
666 font_family: None,
667 font_stretch: FontStretch::Normal,
668 font_weight: 400,
669 is_fixed_pitch: false,
670 is_serif: false,
671 is_italic: false,
672 is_bold: false,
673 is_small_cap: false,
674 character_collection: None,
675 }
676 }
677}
678
679pub(crate) fn is_opaque(c: char) -> bool {
697 let v = c as u32;
698 (v < 0x20 && !matches!(v, 0x09 | 0x0A | 0x0C | 0x0D))
699 || (0x7F..=0x9F).contains(&v)
700 || (0xE000..=0xF8FF).contains(&v)
701 || (0xFDD0..=0xFDEF).contains(&v)
702 || (v & 0xFFFE) == 0xFFFE
703 || (0xF0000..=0xFFFFD).contains(&v)
704 || (0x10_0000..=0x10FFFD).contains(&v)
705}
706
707pub(crate) fn says_something(text: &str) -> bool {
715 !text.is_empty() && !text.chars().any(is_opaque)
716}
717
718pub(crate) fn if_meaningful(s: BfString) -> Option<BfString> {
721 let keep = match &s {
722 BfString::Char(c) => !is_opaque(*c),
723 BfString::String(s) => says_something(s),
724 };
725 keep.then_some(s)
726}
727
728pub(crate) fn text_if_meaningful(text: String) -> Option<String> {
736 says_something(&text).then_some(text)
737}
738
739pub(crate) fn bf_string_of(text: &str) -> BfString {
743 let mut chars = text.chars();
744 match (chars.next(), chars.next()) {
745 (Some(c), None) => BfString::Char(c),
746 _ => BfString::String(text.to_string()),
747 }
748}
749
750pub(crate) fn char_if_meaningful(c: char) -> Option<char> {
753 (!is_opaque(c)).then_some(c)
754}
755
756pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
761 if let Some(unicode_str) = glyph_names::get(name) {
763 return unicode_str.chars().next();
764 }
765
766 if let Some(unicode_str) = tex_glyph_names::get(name) {
770 return unicode_str.chars().next();
771 }
772
773 if let Some(c) = unicode_from_name(name) {
775 return Some(c);
776 }
777
778 if let Some(dot_pos) = name.find('.') {
782 let base = &name[..dot_pos];
783 if !base.is_empty() {
784 if let Some(c) = glyph_names::get(base).and_then(|s| s.chars().next()) {
785 return Some(c);
786 }
787 if let Some(c) = tex_glyph_names::get(base).and_then(|s| s.chars().next()) {
788 return Some(c);
789 }
790 if let Some(c) = unicode_from_name(base) {
791 return Some(c);
792 }
793 }
794 }
795
796 if name.starts_with('a')
799 && name.len() >= 2
800 && let Ok(code) = name[1..].parse::<u32>()
801 && let Some(c) = char::from_u32(code)
802 && (!c.is_control() || c == ' ')
803 {
804 return Some(c);
805 }
806
807 warn!("failed to map glyph name {} to unicode", name);
808 None
809}
810
811pub(crate) fn full_from_the_lists(name: &str) -> Option<String> {
822 let look = |n: &str| {
823 glyph_names::get(n)
824 .or_else(|| tex_glyph_names::get(n))
825 .map(str::to_string)
826 };
827
828 look(name).or_else(|| {
829 let (base, _) = name.split_once('.')?;
830 (!base.is_empty()).then(|| look(base))?
831 })
832}
833
834pub(crate) fn glyph_name_to_string(name: &str) -> Option<String> {
845 if let Some(s) = full_from_the_lists(name) {
850 return Some(s);
851 }
852
853 if let Some(c) = glyph_name_to_unicode(name) {
856 return Some(c.to_string());
857 }
858
859 let base = name.split_once('.').map(|(b, _)| b).unwrap_or(name);
860 if !base.contains('_') {
861 return None;
862 }
863
864 let mut out = String::new();
865 for part in base.split('_') {
866 if part.is_empty() {
867 return None;
868 }
869 let c = glyph_name_to_unicode(part)?;
870 out.push(c);
871 }
872 (!out.is_empty()).then_some(out)
873}
874
875pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
876 let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
877
878 name.starts_with("uni")
879 .then(|| name.get(3..).and_then(convert))
880 .or_else(|| {
881 name.starts_with("u")
882 .then(|| name.get(1..).and_then(convert))
883 })
884 .flatten()
885}
886
887pub(crate) fn read_to_unicode(
888 dict: &Dict<'_>,
889 cmap_resolver: &CMapResolverFn,
890 warning_sink: &WarningSinkFn,
891) -> Option<CMap> {
892 dict.get::<Stream<'_>>(TO_UNICODE)
893 .and_then(|s| decode_or_warn(&s, warning_sink))
894 .or_else(|| {
897 dict.get::<Name>(TO_UNICODE)
898 .and_then(|name| (cmap_resolver)(CMapName::from_bytes(name.as_ref())))
899 .map(|d| d.to_vec())
900 })
901 .and_then(|data| {
902 let cmap_resolver = cmap_resolver.clone();
903 CMap::parse(&data, move |name| (cmap_resolver)(name))
904 })
905}
906
907pub(crate) fn synthesize_unicode_map_from_encoding(dict: &Dict<'_>) -> Option<Vec<Option<String>>> {
910 let (base_encoding, differences) = true_type::read_encoding(dict);
911 if matches!(base_encoding, Encoding::BuiltIn) && differences.is_empty() {
914 return None;
915 }
916 let mut table: Vec<Option<String>> = vec![None; 256];
920 for code in 0u8..=255 {
921 let glyph_name = differences
922 .get(&code)
923 .map(String::as_str)
924 .or_else(|| base_encoding.map_code(code));
925 if let Some(name) = glyph_name {
926 table[code as usize] = glyph_name_to_string(name);
927 }
928 }
929 Some(table)
930}
931
932pub(crate) fn normalized_glyph_name(mut name: &str) -> &str {
937 if name == "nbspace" || name == "nonbreakingspace" {
938 name = "space";
939 }
940
941 if name == "sfthyphen" || name == "softhyphen" {
942 name = "hyphen";
943 }
944
945 name
946}
947
948#[cfg(test)]
949mod normalized_glyph_name_tests {
950 use super::normalized_glyph_name;
951
952 #[test]
953 fn maps_space_aliases() {
954 assert_eq!(normalized_glyph_name("nbspace"), "space");
955 assert_eq!(normalized_glyph_name("nonbreakingspace"), "space");
956 }
957
958 #[test]
959 fn maps_hyphen_aliases() {
960 assert_eq!(normalized_glyph_name("sfthyphen"), "hyphen");
961 assert_eq!(normalized_glyph_name("softhyphen"), "hyphen");
962 }
963
964 #[test]
965 fn preserves_unrelated_names() {
966 assert_eq!(normalized_glyph_name("A"), "A");
967 assert_eq!(normalized_glyph_name(".notdef"), ".notdef");
968 assert_eq!(normalized_glyph_name("hyphen"), "hyphen");
969 assert_eq!(normalized_glyph_name("space"), "space");
970 }
971}
972
973#[cfg(test)]
974mod glyph_name_to_unicode_tests {
975 use super::{glyph_name_to_string, glyph_name_to_unicode};
976
977 #[test]
978 fn standard_agl_name() {
979 assert_eq!(glyph_name_to_unicode("A"), Some('A'));
980 assert_eq!(glyph_name_to_unicode("space"), Some(' '));
981 assert_eq!(glyph_name_to_unicode("hyphen"), Some('-'));
982 }
983
984 #[test]
985 fn ligature_name_underscore_joined() {
986 assert_eq!(glyph_name_to_string("f_i"), Some("fi".to_string()));
990 assert_eq!(glyph_name_to_string("f_f_i"), Some("ffi".to_string()));
991 assert_eq!(glyph_name_to_string("A_B_C"), Some("ABC".to_string()));
992 }
993
994 #[test]
995 fn ligature_name_with_suffix() {
996 assert_eq!(glyph_name_to_string("f_i.alt"), Some("fi".to_string()));
998 }
999
1000 #[test]
1001 fn ligature_name_falls_back_to_single_char_path() {
1002 assert_eq!(glyph_name_to_string("fi"), Some("\u{FB01}".to_string()));
1004 assert_eq!(glyph_name_to_string("A"), Some("A".to_string()));
1005 }
1006
1007 #[test]
1008 fn ligature_name_rejects_unresolvable_component() {
1009 assert!(glyph_name_to_string("A_totallyUnknownGlyph").is_none());
1012 assert!(glyph_name_to_string("_").is_none());
1013 assert!(glyph_name_to_string("A__B").is_none());
1014 }
1015
1016 #[test]
1017 fn uni_prefix() {
1018 assert_eq!(glyph_name_to_unicode("uni0041"), Some('A'));
1019 assert_eq!(glyph_name_to_unicode("uni00E9"), Some('é'));
1020 }
1021
1022 #[test]
1023 fn u_prefix() {
1024 assert_eq!(glyph_name_to_unicode("u0041"), Some('A'));
1025 assert_eq!(glyph_name_to_unicode("u2022"), Some('•'));
1026 }
1027
1028 #[test]
1029 fn variant_suffix_stripped() {
1030 assert_eq!(glyph_name_to_unicode("A.swash"), Some('A'));
1031 assert_eq!(glyph_name_to_unicode("comma.alt"), Some(','));
1032 assert_eq!(glyph_name_to_unicode("space.narrow"), Some(' '));
1033 }
1034
1035 #[test]
1036 fn variant_suffix_with_uni_prefix() {
1037 assert_eq!(glyph_name_to_unicode("uni0041.ss01"), Some('A'));
1038 }
1039
1040 #[test]
1041 fn a_decimal_glyph_name() {
1042 assert_eq!(glyph_name_to_unicode("a65"), Some('A'));
1043 assert_eq!(glyph_name_to_unicode("a32"), Some(' '));
1044 assert_eq!(glyph_name_to_unicode("a97"), Some('a'));
1045 }
1046
1047 #[test]
1048 fn a_decimal_rejects_control_chars() {
1049 assert_eq!(glyph_name_to_unicode("a0"), None);
1050 assert_eq!(glyph_name_to_unicode("a7"), None);
1051 }
1052
1053 #[test]
1054 fn unknown_name_returns_none() {
1055 assert_eq!(glyph_name_to_unicode("xyzzynonexistent"), None);
1056 }
1057}
1058
1059#[cfg(test)]
1060mod fallback_font_query_tests {
1061 use super::*;
1062
1063 fn query_with(name: &str, flags: u32, weight: u32) -> FallbackFontQuery {
1064 let mut q = FallbackFontQuery {
1065 post_script_name: Some(name.to_string()),
1066 font_weight: weight,
1067 ..Default::default()
1068 };
1069
1070 let font_flags = FontFlags::from_bits_truncate(flags);
1071 q.is_fixed_pitch = font_flags.contains(FontFlags::FIXED_PITCH);
1072 q.is_serif = font_flags.contains(FontFlags::SERIF);
1073 q.is_italic = font_flags.contains(FontFlags::ITALIC);
1074 q.is_small_cap = font_flags.contains(FontFlags::SMALL_CAP);
1075
1076 q.is_bold |= q.font_weight >= 700;
1077
1078 if let Some(name) = &q.post_script_name {
1079 let lower = name.to_ascii_lowercase();
1080 q.is_italic |=
1081 lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
1082 q.is_bold |= lower.contains("bold")
1083 || lower.contains("demi")
1084 || lower.contains("semibold")
1085 || lower.contains("heavy")
1086 || lower.contains("black");
1087 }
1088 q
1089 }
1090
1091 #[test]
1092 fn fixed_pitch_flag_selects_courier() {
1093 let q = query_with("LetterGothic", FontFlags::FIXED_PITCH.bits(), 400);
1094 assert!(matches!(q.pick_standard_font(), StandardFont::Courier));
1095 }
1096
1097 #[test]
1098 fn demi_in_name_selects_bold() {
1099 let q = query_with("FranklinGothic-Demi", FontFlags::SERIF.bits(), 400);
1100 assert!(q.is_bold);
1101 assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1102 }
1103
1104 #[test]
1105 fn oblique_detected_as_italic() {
1106 let q = query_with("HelveticaNeue-LightOblique", 0, 400);
1107 assert!(q.is_italic);
1108 assert!(matches!(
1109 q.pick_standard_font(),
1110 StandardFont::HelveticaOblique
1111 ));
1112 }
1113
1114 #[test]
1115 fn font_weight_700_detected_as_bold() {
1116 let q = query_with("CustomFont", 0, 700);
1117 assert!(q.is_bold);
1118 assert!(matches!(
1119 q.pick_standard_font(),
1120 StandardFont::HelveticaBold
1121 ));
1122 }
1123
1124 #[test]
1125 fn semibold_detected_as_bold() {
1126 let q = query_with("AGaramond-Semibold", FontFlags::SERIF.bits(), 400);
1127 assert!(q.is_bold);
1128 assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
1129 }
1130}