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, 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::{CMapResolverFn, CacheKey, FontResolverFn, InterpreterSettings, Paint};
15use bitflags::bitflags;
16use kurbo::{Affine, BezPath, Vec2};
17use log::warn;
18use outline::OutlineFont;
19use pdf_syntax::object::Name;
20use pdf_syntax::object::dict::keys::SUBTYPE;
21use pdf_syntax::object::dict::keys::*;
22use pdf_syntax::object::{Dict, Stream};
23use pdf_syntax::page::Resources;
24use pdf_syntax::xref::XRef;
25use skrifa::GlyphId;
26use std::fmt::Debug;
27use std::ops::Deref;
28use std::rc::Rc;
29use std::sync::Arc;
30
31mod blob;
32mod cid;
33mod generated;
34mod glyph_simulator;
35pub(crate) mod outline;
36mod standard_font;
37mod true_type;
38mod type1;
39pub(crate) mod type3;
40
41pub(crate) const UNITS_PER_EM: f32 = 1000.0;
42
43pub(crate) fn stretch_glyph(path: BezPath, expected_width: f32, actual_width: f32) -> BezPath {
44 if actual_width != 0.0 && actual_width != expected_width {
45 let stretch_factor = expected_width / actual_width;
46 Affine::scale_non_uniform(stretch_factor as f64, 1.0) * path
47 } else {
48 path
49 }
50}
51
52pub type FontData = Arc<dyn AsRef<[u8]> + Send + Sync>;
54
55pub(crate) fn strip_subset_prefix(name: &str) -> &str {
61 match name.split_once('+') {
62 Some((prefix, rest)) if prefix.len() == 6 => rest,
63 _ => name,
64 }
65}
66
67use crate::util::hash128;
68pub use outline::OutlineFontData;
69use pdf_font::cmap::{BfString, CMap, CMapName, CharacterCollection};
70pub use standard_font::StandardFont;
71
72pub enum Glyph<'a> {
74 Outline(OutlineGlyph),
76 Type3(Box<Type3Glyph<'a>>),
78}
79
80impl Glyph<'_> {
81 pub fn as_unicode(&self) -> Option<BfString> {
103 match self {
104 Glyph::Outline(g) => g.as_unicode(),
105 Glyph::Type3(g) => g.as_unicode(),
106 }
107 }
108}
109
110#[derive(Clone, Debug)]
112pub struct GlyphIdentifier {
113 id: GlyphId,
114 font: OutlineFont,
115}
116
117impl CacheKey for GlyphIdentifier {
118 fn cache_key(&self) -> u128 {
119 hash128(&(self.id, self.font.cache_key()))
120 }
121}
122
123#[derive(Clone, Debug)]
125pub struct OutlineGlyph {
126 pub(crate) id: GlyphId,
127 pub(crate) font: OutlineFont,
128 pub(crate) char_code: u32,
129}
130
131impl OutlineGlyph {
132 pub fn outline(&self) -> BezPath {
134 self.font.outline_glyph(self.id, self.char_code)
135 }
136
137 pub fn identifier(&self) -> GlyphIdentifier {
143 GlyphIdentifier {
144 id: self.id,
145 font: self.font.clone(),
146 }
147 }
148
149 pub fn as_unicode(&self) -> Option<BfString> {
153 self.font.char_code_to_unicode(self.char_code)
154 }
155
156 pub fn font_data(&self) -> Option<OutlineFontData> {
160 self.font.font_data()
161 }
162
163 pub fn glyph_id(&self) -> GlyphId {
165 self.id
166 }
167
168 pub fn advance_width(&self) -> Option<f32> {
173 self.font.glyph_advance_width(self.char_code)
174 }
175
176 pub fn font_cache_key(&self) -> u128 {
181 self.font.cache_key()
182 }
183}
184
185#[derive(Clone)]
187pub struct Type3Glyph<'a> {
188 pub(crate) font: Rc<Type3<'a>>,
189 pub(crate) glyph_id: GlyphId,
190 pub(crate) state: State<'a>,
191 pub(crate) parent_resources: Resources<'a>,
192 pub(crate) cache: Cache,
193 pub(crate) xref: &'a XRef,
194 pub(crate) settings: InterpreterSettings,
195 pub(crate) char_code: u32,
196}
197
198impl<'a> Type3Glyph<'a> {
200 pub fn interpret(
202 &self,
203 device: &mut impl Device<'a>,
204 transform: Affine,
205 glyph_transform: Affine,
206 paint: &Paint<'a>,
207 ) {
208 self.font
209 .render_glyph(self, transform, glyph_transform, paint, device);
210 }
211
212 pub fn as_unicode(&self) -> Option<BfString> {
216 self.font.char_code_to_unicode(self.char_code)
217 }
218}
219
220impl CacheKey for Type3Glyph<'_> {
221 fn cache_key(&self) -> u128 {
222 hash128(&(self.font.cache_key(), self.glyph_id))
223 }
224}
225
226#[derive(Clone, Debug)]
227pub(crate) struct Font<'a>(u128, FontType<'a>);
228
229impl<'a> Font<'a> {
230 pub(crate) fn new(
231 dict: &Dict<'a>,
232 font_resolver: &FontResolverFn,
233 cmap_resolver: &CMapResolverFn,
234 ) -> Option<Self> {
235 let f_type = match dict.get::<Name>(SUBTYPE)?.deref() {
236 TYPE1 | MM_TYPE1 => {
237 FontType::Type1(Rc::new(Type1Font::new(dict, font_resolver, cmap_resolver)?))
238 }
239 TRUE_TYPE | OPEN_TYPE => FontType::TrueType(Rc::new(TrueTypeFont::new(
241 dict,
242 font_resolver,
243 cmap_resolver,
244 )?)),
245 TYPE0 => FontType::Type0(Rc::new(Type0Font::new(dict, font_resolver, cmap_resolver)?)),
246 TYPE3 => FontType::Type3(Rc::new(Type3::new(dict, cmap_resolver)?)),
247 f => {
248 warn!(
249 "unimplemented font type {:?}",
250 std::str::from_utf8(f).unwrap_or("unknown type")
251 );
252
253 return None;
254 }
255 };
256
257 let cache_key = dict.cache_key();
258
259 Some(Self(cache_key, f_type))
260 }
261
262 pub(crate) fn new_standard(
263 standard_font: StandardFont,
264 font_resolver: &FontResolverFn,
265 ) -> Option<Self> {
266 let font = Type1Font::new_standard(standard_font, font_resolver)?;
267
268 Some(Self(0, FontType::Type1(Rc::new(font))))
269 }
270
271 pub(crate) fn map_code(&self, code: u32) -> GlyphId {
272 match &self.1 {
273 FontType::Type1(f) => {
274 debug_assert!(code <= u8::MAX as u32);
275
276 f.map_code(code as u8)
277 }
278 FontType::TrueType(t) => {
279 debug_assert!(code <= u8::MAX as u32);
280
281 t.map_code(code as u8)
282 }
283 FontType::Type0(t) => t.map_code(code),
284 FontType::Type3(t) => {
285 debug_assert!(code <= u8::MAX as u32);
286
287 t.map_code(code as u8)
288 }
289 }
290 }
291
292 pub(crate) fn get_glyph(
293 &self,
294 glyph: GlyphId,
295 char_code: u32,
296 ctx: &mut Context<'a>,
297 resources: &Resources<'a>,
298 origin_displacement: Vec2,
299 ) -> (Glyph<'a>, Affine) {
300 let glyph_transform = ctx.get().text_state.full_transform()
301 * Affine::scale(1.0 / UNITS_PER_EM as f64)
302 * Affine::translate(origin_displacement);
303
304 let glyph = match &self.1 {
305 FontType::Type1(t) => {
306 let font = OutlineFont::Type1(t.clone());
307 Glyph::Outline(OutlineGlyph {
308 id: glyph,
309 font,
310 char_code,
311 })
312 }
313 FontType::TrueType(t) => {
314 let font = OutlineFont::TrueType(t.clone());
315 Glyph::Outline(OutlineGlyph {
316 id: glyph,
317 font,
318 char_code,
319 })
320 }
321 FontType::Type0(t) => {
322 let font = OutlineFont::Type0(t.clone());
323 Glyph::Outline(OutlineGlyph {
324 id: glyph,
325 font,
326 char_code,
327 })
328 }
329 FontType::Type3(t) => {
330 let shape_glyph = Type3Glyph {
331 font: t.clone(),
332 glyph_id: glyph,
333 state: ctx.get().clone(),
334 parent_resources: resources.clone(),
335 cache: ctx.object_cache.clone(),
336 xref: ctx.xref,
337 settings: ctx.settings.clone(),
338 char_code,
339 };
340
341 Glyph::Type3(Box::new(shape_glyph))
342 }
343 };
344
345 (glyph, glyph_transform)
346 }
347
348 pub(crate) fn code_advance(&self, code: u32) -> Vec2 {
349 match &self.1 {
350 FontType::Type1(t) => {
351 debug_assert!(code <= u8::MAX as u32);
352
353 Vec2::new(t.glyph_width(code as u8).unwrap_or(0.0) as f64, 0.0)
354 }
355 FontType::TrueType(t) => {
356 debug_assert!(code <= u8::MAX as u32);
357
358 Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
359 }
360 FontType::Type0(t) => t.code_advance(code),
361 FontType::Type3(t) => {
362 debug_assert!(code <= u8::MAX as u32);
363
364 Vec2::new(t.glyph_width(code as u8) as f64, 0.0)
365 }
366 }
367 }
368
369 pub(crate) fn origin_displacement(&self, code: u32) -> Vec2 {
370 match &self.1 {
371 FontType::Type1(_) => Vec2::default(),
372 FontType::TrueType(_) => Vec2::default(),
373 FontType::Type0(t) => t.origin_displacement(code),
374 FontType::Type3(_) => Vec2::default(),
375 }
376 }
377
378 pub(crate) fn read_code(&self, bytes: &[u8], offset: usize) -> (u32, usize) {
379 match &self.1 {
380 FontType::Type1(_) => (bytes[offset] as u32, 1),
381 FontType::TrueType(_) => (bytes[offset] as u32, 1),
382 FontType::Type0(t) => t.read_code(bytes, offset),
383 FontType::Type3(_) => (bytes[offset] as u32, 1),
384 }
385 }
386
387 pub(crate) fn is_horizontal(&self) -> bool {
388 match &self.1 {
389 FontType::Type1(_) => true,
390 FontType::TrueType(_) => true,
391 FontType::Type0(t) => t.is_horizontal(),
392 FontType::Type3(_) => true,
393 }
394 }
395}
396
397impl CacheKey for Font<'_> {
398 fn cache_key(&self) -> u128 {
399 self.0
400 }
401}
402
403#[derive(Clone, Debug)]
404enum FontType<'a> {
405 Type1(Rc<Type1Font>),
406 TrueType(Rc<TrueTypeFont>),
407 Type0(Rc<Type0Font>),
408 Type3(Rc<Type3<'a>>),
409}
410
411#[derive(Debug)]
412enum Encoding {
413 Standard,
414 MacRoman,
415 WinAnsi,
416 MacExpert,
417 BuiltIn,
418}
419
420impl Encoding {
421 fn map_code(&self, code: u8) -> Option<&'static str> {
422 if code == 0 {
423 return Some(".notdef");
424 }
425 match self {
426 Self::Standard => standard::get(code),
427 Self::MacRoman => mac_roman::get(code).or_else(|| mac_os_roman::get(code)),
428 Self::WinAnsi => win_ansi::get(code),
429 Self::MacExpert => mac_expert::get(code),
430 Self::BuiltIn => None,
431 }
432 }
433}
434
435#[derive(Debug, Copy, Clone)]
437pub enum FontStretch {
438 Normal,
440 UltraCondensed,
442 ExtraCondensed,
444 Condensed,
446 SemiCondensed,
448 SemiExpanded,
450 Expanded,
452 ExtraExpanded,
454 UltraExpanded,
456}
457
458impl FontStretch {
459 fn from_string(s: &str) -> Self {
460 match s {
461 "UltraCondensed" => Self::UltraCondensed,
462 "ExtraCondensed" => Self::ExtraCondensed,
463 "Condensed" => Self::Condensed,
464 "SemiCondensed" => Self::SemiCondensed,
465 "SemiExpanded" => Self::SemiExpanded,
466 "Expanded" => Self::Expanded,
467 "ExtraExpanded" => Self::ExtraExpanded,
468 "UltraExpanded" => Self::UltraExpanded,
469 _ => Self::Normal,
470 }
471 }
472}
473
474bitflags! {
475 #[derive(Debug)]
477 pub(crate) struct FontFlags: u32 {
478 const FIXED_PITCH = 1 << 0;
479 const SERIF = 1 << 1;
480 const SYMBOLIC = 1 << 2;
481 const SCRIPT = 1 << 3;
482 const NON_SYMBOLIC = 1 << 5;
483 const ITALIC = 1 << 6;
484 const ALL_CAP = 1 << 16;
485 const SMALL_CAP = 1 << 17;
486 const FORCE_BOLD = 1 << 18;
487 }
488}
489
490pub enum FontQuery {
492 Standard(StandardFont),
494 Fallback(FallbackFontQuery),
499}
500
501#[derive(Debug, Clone)]
503pub struct FallbackFontQuery {
504 pub post_script_name: Option<String>,
506 pub font_name: Option<String>,
508 pub font_family: Option<String>,
510 pub font_stretch: FontStretch,
512 pub font_weight: u32,
514 pub is_fixed_pitch: bool,
516 pub is_serif: bool,
518 pub is_italic: bool,
520 pub is_bold: bool,
522 pub is_small_cap: bool,
524 pub character_collection: Option<CharacterCollection>,
526}
527
528impl FallbackFontQuery {
529 pub(crate) fn new(dict: &Dict<'_>) -> Self {
530 let post_script_name = dict
531 .get::<Name>(BASE_FONT)
532 .map(|n| strip_subset_prefix(n.as_str()).to_string());
533
534 let mut data = Self {
535 post_script_name,
536 ..Default::default()
537 };
538
539 if let Some(descriptor) = dict.get::<Dict<'_>>(FONT_DESC) {
540 data.font_name = dict
541 .get::<Name>(FONT_NAME)
542 .map(|n| strip_subset_prefix(n.as_str()).to_string());
543 data.font_family = descriptor
544 .get::<Name>(FONT_FAMILY)
545 .map(|n| n.as_str().to_string());
546 data.font_stretch = descriptor
547 .get::<Name>(FONT_STRETCH)
548 .map(|n| FontStretch::from_string(n.as_str()))
549 .unwrap_or(FontStretch::Normal);
550 data.font_weight = descriptor.get::<u32>(FONT_WEIGHT).unwrap_or(400);
551
552 if let Some(flags) = descriptor
553 .get::<u32>(FLAGS)
554 .map(FontFlags::from_bits_truncate)
555 {
556 data.is_fixed_pitch = flags.contains(FontFlags::FIXED_PITCH);
557 data.is_serif = flags.contains(FontFlags::SERIF);
558 data.is_italic = flags.contains(FontFlags::ITALIC);
559 data.is_small_cap = flags.contains(FontFlags::SMALL_CAP);
560 }
561 }
562
563 data.is_bold |= data.font_weight >= 700;
564
565 if let Some(name) = &data.post_script_name {
566 let lower = name.to_ascii_lowercase();
567 data.is_italic |=
568 lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
569 data.is_bold |= lower.contains("bold")
570 || lower.contains("demi")
571 || lower.contains("semibold")
572 || lower.contains("heavy")
573 || lower.contains("black");
574 }
575
576 data
577 }
578
579 pub fn pick_standard_font(&self) -> StandardFont {
581 if self.is_fixed_pitch {
582 match (self.is_bold, self.is_italic) {
583 (true, true) => StandardFont::CourierBoldOblique,
584 (true, false) => StandardFont::CourierBold,
585 (false, true) => StandardFont::CourierOblique,
586 (false, false) => StandardFont::Courier,
587 }
588 } else if !self.is_serif {
589 match (self.is_bold, self.is_italic) {
590 (true, true) => StandardFont::HelveticaBoldOblique,
591 (true, false) => StandardFont::HelveticaBold,
592 (false, true) => StandardFont::HelveticaOblique,
593 (false, false) => StandardFont::Helvetica,
594 }
595 } else {
596 match (self.is_bold, self.is_italic) {
597 (true, true) => StandardFont::TimesBoldItalic,
598 (true, false) => StandardFont::TimesBold,
599 (false, true) => StandardFont::TimesItalic,
600 (false, false) => StandardFont::TimesRoman,
601 }
602 }
603 }
604}
605
606impl Default for FallbackFontQuery {
607 fn default() -> Self {
608 Self {
609 post_script_name: None,
610 font_name: None,
611 font_family: None,
612 font_stretch: FontStretch::Normal,
613 font_weight: 400,
614 is_fixed_pitch: false,
615 is_serif: false,
616 is_italic: false,
617 is_bold: false,
618 is_small_cap: false,
619 character_collection: None,
620 }
621 }
622}
623
624pub(crate) fn glyph_name_to_unicode(name: &str) -> Option<char> {
629 if let Some(unicode_str) = glyph_names::get(name) {
631 return unicode_str.chars().next();
632 }
633
634 if let Some(c) = unicode_from_name(name) {
636 return Some(c);
637 }
638
639 if let Some(dot_pos) = name.find('.') {
643 let base = &name[..dot_pos];
644 if !base.is_empty() {
645 if let Some(c) = glyph_names::get(base).and_then(|s| s.chars().next()) {
646 return Some(c);
647 }
648 if let Some(c) = unicode_from_name(base) {
649 return Some(c);
650 }
651 }
652 }
653
654 if name.starts_with('a')
657 && name.len() >= 2
658 && let Ok(code) = name[1..].parse::<u32>()
659 && let Some(c) = char::from_u32(code)
660 && (!c.is_control() || c == ' ')
661 {
662 return Some(c);
663 }
664
665 warn!("failed to map glyph name {} to unicode", name);
666 None
667}
668
669pub(crate) fn glyph_name_to_string(name: &str) -> Option<String> {
680 if let Some(c) = glyph_name_to_unicode(name) {
683 return Some(c.to_string());
684 }
685
686 let base = name.split_once('.').map(|(b, _)| b).unwrap_or(name);
687 if !base.contains('_') {
688 return None;
689 }
690
691 let mut out = String::new();
692 for part in base.split('_') {
693 if part.is_empty() {
694 return None;
695 }
696 let c = glyph_name_to_unicode(part)?;
697 out.push(c);
698 }
699 (!out.is_empty()).then_some(out)
700}
701
702pub(crate) fn unicode_from_name(name: &str) -> Option<char> {
703 let convert = |input: &str| u32::from_str_radix(input, 16).ok().and_then(char::from_u32);
704
705 name.starts_with("uni")
706 .then(|| name.get(3..).and_then(convert))
707 .or_else(|| {
708 name.starts_with("u")
709 .then(|| name.get(1..).and_then(convert))
710 })
711 .flatten()
712}
713
714pub(crate) fn read_to_unicode(dict: &Dict<'_>, cmap_resolver: &CMapResolverFn) -> Option<CMap> {
715 dict.get::<Stream<'_>>(TO_UNICODE)
716 .and_then(|s| s.decoded().ok())
717 .or_else(|| {
720 dict.get::<Name>(TO_UNICODE)
721 .and_then(|name| (cmap_resolver)(CMapName::from_bytes(name.as_ref())))
722 .map(|d| d.to_vec())
723 })
724 .and_then(|data| {
725 let cmap_resolver = cmap_resolver.clone();
726 CMap::parse(&data, move |name| (cmap_resolver)(name))
727 })
728}
729
730pub(crate) fn synthesize_unicode_map_from_encoding(dict: &Dict<'_>) -> Option<[Option<char>; 256]> {
733 let (base_encoding, differences) = true_type::read_encoding(dict);
734 if matches!(base_encoding, Encoding::BuiltIn) && differences.is_empty() {
737 return None;
738 }
739 let mut table: [Option<char>; 256] = [None; 256];
740 for code in 0u8..=255 {
741 let glyph_name = differences
742 .get(&code)
743 .map(String::as_str)
744 .or_else(|| base_encoding.map_code(code));
745 if let Some(name) = glyph_name {
746 table[code as usize] = glyph_name_to_unicode(name);
747 }
748 }
749 Some(table)
750}
751
752pub(crate) fn normalized_glyph_name(mut name: &str) -> &str {
757 if name == "nbspace" || name == "nonbreakingspace" {
758 name = "space";
759 }
760
761 if name == "sfthyphen" || name == "softhyphen" {
762 name = "hyphen";
763 }
764
765 name
766}
767
768#[cfg(test)]
769mod normalized_glyph_name_tests {
770 use super::normalized_glyph_name;
771
772 #[test]
773 fn maps_space_aliases() {
774 assert_eq!(normalized_glyph_name("nbspace"), "space");
775 assert_eq!(normalized_glyph_name("nonbreakingspace"), "space");
776 }
777
778 #[test]
779 fn maps_hyphen_aliases() {
780 assert_eq!(normalized_glyph_name("sfthyphen"), "hyphen");
781 assert_eq!(normalized_glyph_name("softhyphen"), "hyphen");
782 }
783
784 #[test]
785 fn preserves_unrelated_names() {
786 assert_eq!(normalized_glyph_name("A"), "A");
787 assert_eq!(normalized_glyph_name(".notdef"), ".notdef");
788 assert_eq!(normalized_glyph_name("hyphen"), "hyphen");
789 assert_eq!(normalized_glyph_name("space"), "space");
790 }
791}
792
793#[cfg(test)]
794mod glyph_name_to_unicode_tests {
795 use super::{glyph_name_to_string, glyph_name_to_unicode};
796
797 #[test]
798 fn standard_agl_name() {
799 assert_eq!(glyph_name_to_unicode("A"), Some('A'));
800 assert_eq!(glyph_name_to_unicode("space"), Some(' '));
801 assert_eq!(glyph_name_to_unicode("hyphen"), Some('-'));
802 }
803
804 #[test]
805 fn ligature_name_underscore_joined() {
806 assert_eq!(glyph_name_to_string("f_i"), Some("fi".to_string()));
810 assert_eq!(glyph_name_to_string("f_f_i"), Some("ffi".to_string()));
811 assert_eq!(glyph_name_to_string("A_B_C"), Some("ABC".to_string()));
812 }
813
814 #[test]
815 fn ligature_name_with_suffix() {
816 assert_eq!(glyph_name_to_string("f_i.alt"), Some("fi".to_string()));
818 }
819
820 #[test]
821 fn ligature_name_falls_back_to_single_char_path() {
822 assert_eq!(glyph_name_to_string("fi"), Some("\u{FB01}".to_string()));
824 assert_eq!(glyph_name_to_string("A"), Some("A".to_string()));
825 }
826
827 #[test]
828 fn ligature_name_rejects_unresolvable_component() {
829 assert!(glyph_name_to_string("A_totallyUnknownGlyph").is_none());
832 assert!(glyph_name_to_string("_").is_none());
833 assert!(glyph_name_to_string("A__B").is_none());
834 }
835
836 #[test]
837 fn uni_prefix() {
838 assert_eq!(glyph_name_to_unicode("uni0041"), Some('A'));
839 assert_eq!(glyph_name_to_unicode("uni00E9"), Some('é'));
840 }
841
842 #[test]
843 fn u_prefix() {
844 assert_eq!(glyph_name_to_unicode("u0041"), Some('A'));
845 assert_eq!(glyph_name_to_unicode("u2022"), Some('•'));
846 }
847
848 #[test]
849 fn variant_suffix_stripped() {
850 assert_eq!(glyph_name_to_unicode("A.swash"), Some('A'));
851 assert_eq!(glyph_name_to_unicode("comma.alt"), Some(','));
852 assert_eq!(glyph_name_to_unicode("space.narrow"), Some(' '));
853 }
854
855 #[test]
856 fn variant_suffix_with_uni_prefix() {
857 assert_eq!(glyph_name_to_unicode("uni0041.ss01"), Some('A'));
858 }
859
860 #[test]
861 fn a_decimal_glyph_name() {
862 assert_eq!(glyph_name_to_unicode("a65"), Some('A'));
863 assert_eq!(glyph_name_to_unicode("a32"), Some(' '));
864 assert_eq!(glyph_name_to_unicode("a97"), Some('a'));
865 }
866
867 #[test]
868 fn a_decimal_rejects_control_chars() {
869 assert_eq!(glyph_name_to_unicode("a0"), None);
870 assert_eq!(glyph_name_to_unicode("a7"), None);
871 }
872
873 #[test]
874 fn unknown_name_returns_none() {
875 assert_eq!(glyph_name_to_unicode("xyzzynonexistent"), None);
876 }
877}
878
879#[cfg(test)]
880mod fallback_font_query_tests {
881 use super::*;
882
883 fn query_with(name: &str, flags: u32, weight: u32) -> FallbackFontQuery {
884 let mut q = FallbackFontQuery {
885 post_script_name: Some(name.to_string()),
886 font_weight: weight,
887 ..Default::default()
888 };
889
890 let font_flags = FontFlags::from_bits_truncate(flags);
891 q.is_fixed_pitch = font_flags.contains(FontFlags::FIXED_PITCH);
892 q.is_serif = font_flags.contains(FontFlags::SERIF);
893 q.is_italic = font_flags.contains(FontFlags::ITALIC);
894 q.is_small_cap = font_flags.contains(FontFlags::SMALL_CAP);
895
896 q.is_bold |= q.font_weight >= 700;
897
898 if let Some(name) = &q.post_script_name {
899 let lower = name.to_ascii_lowercase();
900 q.is_italic |=
901 lower.contains("italic") || lower.contains("oblique") || lower.contains("slant");
902 q.is_bold |= lower.contains("bold")
903 || lower.contains("demi")
904 || lower.contains("semibold")
905 || lower.contains("heavy")
906 || lower.contains("black");
907 }
908 q
909 }
910
911 #[test]
912 fn fixed_pitch_flag_selects_courier() {
913 let q = query_with("LetterGothic", FontFlags::FIXED_PITCH.bits(), 400);
914 assert!(matches!(q.pick_standard_font(), StandardFont::Courier));
915 }
916
917 #[test]
918 fn demi_in_name_selects_bold() {
919 let q = query_with("FranklinGothic-Demi", FontFlags::SERIF.bits(), 400);
920 assert!(q.is_bold);
921 assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
922 }
923
924 #[test]
925 fn oblique_detected_as_italic() {
926 let q = query_with("HelveticaNeue-LightOblique", 0, 400);
927 assert!(q.is_italic);
928 assert!(matches!(
929 q.pick_standard_font(),
930 StandardFont::HelveticaOblique
931 ));
932 }
933
934 #[test]
935 fn font_weight_700_detected_as_bold() {
936 let q = query_with("CustomFont", 0, 700);
937 assert!(q.is_bold);
938 assert!(matches!(
939 q.pick_standard_font(),
940 StandardFont::HelveticaBold
941 ));
942 }
943
944 #[test]
945 fn semibold_detected_as_bold() {
946 let q = query_with("AGaramond-Semibold", FontFlags::SERIF.bits(), 400);
947 assert!(q.is_bold);
948 assert!(matches!(q.pick_standard_font(), StandardFont::TimesBold));
949 }
950}