1pub mod error;
2
3pub use error::*;
4
5use base64::Engine;
6use std::collections::{BTreeMap, HashMap};
7use std::fmt;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
12pub enum FontStyle {
13 #[default]
14 Normal,
15 Italic,
16 Oblique,
17}
18
19impl fmt::Display for FontStyle {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 let value = match self {
22 Self::Normal => "normal",
23 Self::Italic => "italic",
24 Self::Oblique => "oblique",
25 };
26
27 f.write_str(value)
28 }
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct FontWeight(u16);
33
34impl FontWeight {
35 pub const THIN: Self = Self(100);
36 pub const EXTRA_LIGHT: Self = Self(200);
37 pub const LIGHT: Self = Self(300);
38 pub const NORMAL: Self = Self(400);
39 pub const MEDIUM: Self = Self(500);
40 pub const SEMI_BOLD: Self = Self(600);
41 pub const BOLD: Self = Self(700);
42 pub const EXTRA_BOLD: Self = Self(800);
43 pub const BLACK: Self = Self(900);
44
45 pub fn new(weight: u16) -> Result<Self> {
46 if (1..=1000).contains(&weight) {
47 Ok(Self(weight))
48 } else {
49 Err(Error::InvalidFontWeight { weight })
50 }
51 }
52
53 pub const fn value(self) -> u16 {
54 self.0
55 }
56}
57
58impl Default for FontWeight {
59 fn default() -> Self {
60 Self::NORMAL
61 }
62}
63
64impl TryFrom<u16> for FontWeight {
65 type Error = Error;
66
67 fn try_from(value: u16) -> Result<Self> {
68 Self::new(value)
69 }
70}
71
72impl From<FontWeight> for u16 {
73 fn from(value: FontWeight) -> Self {
74 value.value()
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub enum StandardFont {
80 TimesRoman,
81 TimesBold,
82 TimesItalic,
83 TimesBoldItalic,
84 Helvetica,
85 HelveticaBold,
86 HelveticaOblique,
87 HelveticaBoldOblique,
88 Courier,
89 CourierBold,
90 CourierOblique,
91 CourierBoldOblique,
92 Symbol,
93 ZapfDingbats,
94}
95
96impl StandardFont {
97 pub const fn family_name(self) -> &'static str {
98 match self {
99 Self::TimesRoman | Self::TimesBold | Self::TimesItalic | Self::TimesBoldItalic => {
100 "Times-Roman"
101 }
102 Self::Helvetica
103 | Self::HelveticaBold
104 | Self::HelveticaOblique
105 | Self::HelveticaBoldOblique => "Helvetica",
106 Self::Courier | Self::CourierBold | Self::CourierOblique | Self::CourierBoldOblique => {
107 "Courier"
108 }
109 Self::Symbol => "Symbol",
110 Self::ZapfDingbats => "ZapfDingbats",
111 }
112 }
113
114 pub const fn font_style(self) -> FontStyle {
115 match self {
116 Self::TimesItalic | Self::TimesBoldItalic => FontStyle::Italic,
117 Self::HelveticaOblique
118 | Self::HelveticaBoldOblique
119 | Self::CourierOblique
120 | Self::CourierBoldOblique => FontStyle::Oblique,
121 _ => FontStyle::Normal,
122 }
123 }
124
125 pub const fn font_weight(self) -> FontWeight {
126 match self {
127 Self::TimesBold
128 | Self::TimesBoldItalic
129 | Self::HelveticaBold
130 | Self::HelveticaBoldOblique
131 | Self::CourierBold
132 | Self::CourierBoldOblique => FontWeight::BOLD,
133 _ => FontWeight::NORMAL,
134 }
135 }
136
137 pub const fn as_str(self) -> &'static str {
138 match self {
139 Self::TimesRoman => "Times-Roman",
140 Self::TimesBold => "Times-Bold",
141 Self::TimesItalic => "Times-Italic",
142 Self::TimesBoldItalic => "Times-BoldItalic",
143 Self::Helvetica => "Helvetica",
144 Self::HelveticaBold => "Helvetica-Bold",
145 Self::HelveticaOblique => "Helvetica-Oblique",
146 Self::HelveticaBoldOblique => "Helvetica-BoldOblique",
147 Self::Courier => "Courier",
148 Self::CourierBold => "Courier-Bold",
149 Self::CourierOblique => "Courier-Oblique",
150 Self::CourierBoldOblique => "Courier-BoldOblique",
151 Self::Symbol => "Symbol",
152 Self::ZapfDingbats => "ZapfDingbats",
153 }
154 }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq, Hash)]
158pub enum FontSource {
159 Local(PathBuf),
160 Remote(String),
161 DataUri(String),
162 Standard(StandardFont),
163}
164
165impl FontSource {
166 pub fn local(path: impl Into<PathBuf>) -> Self {
167 Self::Local(path.into())
168 }
169
170 pub fn remote(url: impl Into<String>) -> Self {
171 Self::Remote(url.into())
172 }
173
174 pub fn data_uri(uri: impl Into<String>) -> Self {
175 Self::DataUri(uri.into())
176 }
177
178 pub const fn standard(font: StandardFont) -> Self {
179 Self::Standard(font)
180 }
181}
182
183#[derive(Clone, Debug, PartialEq, Eq, Hash)]
184pub struct FontDescriptor {
185 family: String,
186 font_style: FontStyle,
187 font_weight: FontWeight,
188}
189
190impl FontDescriptor {
191 pub fn new(family: impl Into<String>) -> Self {
192 Self {
193 family: family.into(),
194 font_style: FontStyle::Normal,
195 font_weight: FontWeight::NORMAL,
196 }
197 }
198
199 pub fn family(&self) -> &str {
200 &self.family
201 }
202
203 pub const fn font_style(&self) -> FontStyle {
204 self.font_style
205 }
206
207 pub const fn font_weight(&self) -> FontWeight {
208 self.font_weight
209 }
210
211 pub fn with_style(mut self, font_style: FontStyle) -> Self {
212 self.font_style = font_style;
213 self
214 }
215
216 pub fn with_weight(mut self, font_weight: FontWeight) -> Self {
217 self.font_weight = font_weight;
218 self
219 }
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, Hash)]
223pub struct FontRegistration {
224 family: String,
225 source: FontSource,
226 font_style: FontStyle,
227 font_weight: FontWeight,
228}
229
230impl FontRegistration {
231 pub fn new(family: impl Into<String>, source: FontSource) -> Self {
232 Self {
233 family: family.into(),
234 source,
235 font_style: FontStyle::Normal,
236 font_weight: FontWeight::NORMAL,
237 }
238 }
239
240 pub fn family(&self) -> &str {
241 &self.family
242 }
243
244 pub const fn source(&self) -> &FontSource {
245 &self.source
246 }
247
248 pub const fn font_style(&self) -> FontStyle {
249 self.font_style
250 }
251
252 pub const fn font_weight(&self) -> FontWeight {
253 self.font_weight
254 }
255
256 pub fn with_style(mut self, font_style: FontStyle) -> Self {
257 self.font_style = font_style;
258 self
259 }
260
261 pub fn with_weight(mut self, font_weight: FontWeight) -> Self {
262 self.font_weight = font_weight;
263 self
264 }
265
266 fn descriptor(&self) -> FontDescriptor {
267 FontDescriptor::new(self.family.clone())
268 .with_style(self.font_style)
269 .with_weight(self.font_weight)
270 }
271}
272
273#[derive(Clone, Debug, PartialEq, Eq, Hash)]
274pub struct FontVariantRegistration {
275 source: FontSource,
276 font_style: FontStyle,
277 font_weight: FontWeight,
278}
279
280impl FontVariantRegistration {
281 pub fn new(source: FontSource) -> Self {
282 Self {
283 source,
284 font_style: FontStyle::Normal,
285 font_weight: FontWeight::NORMAL,
286 }
287 }
288
289 pub const fn source(&self) -> &FontSource {
290 &self.source
291 }
292
293 pub const fn font_style(&self) -> FontStyle {
294 self.font_style
295 }
296
297 pub const fn font_weight(&self) -> FontWeight {
298 self.font_weight
299 }
300
301 pub fn with_style(mut self, font_style: FontStyle) -> Self {
302 self.font_style = font_style;
303 self
304 }
305
306 pub fn with_weight(mut self, font_weight: FontWeight) -> Self {
307 self.font_weight = font_weight;
308 self
309 }
310}
311
312#[derive(Clone, Debug, PartialEq, Eq, Hash)]
313pub struct FontFamilyRegistration {
314 family: String,
315 fonts: Vec<FontVariantRegistration>,
316}
317
318impl FontFamilyRegistration {
319 pub fn new(
320 family: impl Into<String>,
321 fonts: impl IntoIterator<Item = FontVariantRegistration>,
322 ) -> Self {
323 Self {
324 family: family.into(),
325 fonts: fonts.into_iter().collect(),
326 }
327 }
328
329 pub fn family(&self) -> &str {
330 &self.family
331 }
332
333 pub fn fonts(&self) -> &[FontVariantRegistration] {
334 &self.fonts
335 }
336}
337
338#[derive(Clone, Debug, PartialEq, Eq)]
339pub struct RegisteredFont {
340 descriptor: FontDescriptor,
341 source: FontSource,
342}
343
344impl RegisteredFont {
345 pub fn descriptor(&self) -> &FontDescriptor {
346 &self.descriptor
347 }
348
349 pub const fn source(&self) -> &FontSource {
350 &self.source
351 }
352}
353
354#[derive(Clone, Debug, PartialEq, Eq)]
355pub enum LoadedFontData {
356 Binary(Vec<u8>),
357 Standard(StandardFont),
358}
359
360#[derive(Clone, Debug, PartialEq, Eq)]
361pub struct LoadedFont {
362 descriptor: FontDescriptor,
363 source: FontSource,
364 data: LoadedFontData,
365}
366
367impl LoadedFont {
368 pub fn descriptor(&self) -> &FontDescriptor {
369 &self.descriptor
370 }
371
372 pub const fn source(&self) -> &FontSource {
373 &self.source
374 }
375
376 pub fn data(&self) -> &LoadedFontData {
377 &self.data
378 }
379
380 pub fn bytes(&self) -> Option<&[u8]> {
381 match &self.data {
382 LoadedFontData::Binary(bytes) => Some(bytes.as_slice()),
383 LoadedFontData::Standard(_) => None,
384 }
385 }
386
387 pub fn standard_font(&self) -> Option<StandardFont> {
388 match &self.data {
389 LoadedFontData::Standard(font) => Some(*font),
390 LoadedFontData::Binary(_) => None,
391 }
392 }
393}
394
395pub type HyphenationCallback = Arc<dyn Fn(&str) -> Vec<String> + Send + Sync>;
396pub type EmojiUrlBuilder = Arc<dyn Fn(&str) -> String + Send + Sync>;
397
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
399pub enum EmojiFormat {
400 Png,
401 Svg,
402 Jpeg,
403 Gif,
404 Webp,
405}
406
407impl EmojiFormat {
408 pub const fn extension(self) -> &'static str {
409 match self {
410 Self::Png => "png",
411 Self::Svg => "svg",
412 Self::Jpeg => "jpg",
413 Self::Gif => "gif",
414 Self::Webp => "webp",
415 }
416 }
417}
418
419#[derive(Clone)]
420pub enum EmojiSource {
421 Url {
422 base_url: String,
423 format: EmojiFormat,
424 with_variation_selectors: bool,
425 },
426 Builder {
427 builder: EmojiUrlBuilder,
428 format: EmojiFormat,
429 with_variation_selectors: bool,
430 },
431}
432
433impl fmt::Debug for EmojiSource {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 match self {
436 Self::Url {
437 base_url,
438 format,
439 with_variation_selectors,
440 } => f
441 .debug_struct("EmojiSource::Url")
442 .field("base_url", base_url)
443 .field("format", format)
444 .field("with_variation_selectors", with_variation_selectors)
445 .finish(),
446 Self::Builder {
447 format,
448 with_variation_selectors,
449 ..
450 } => f
451 .debug_struct("EmojiSource::Builder")
452 .field("format", format)
453 .field("with_variation_selectors", with_variation_selectors)
454 .finish_non_exhaustive(),
455 }
456 }
457}
458
459impl EmojiSource {
460 pub fn url(base_url: impl Into<String>, format: EmojiFormat) -> Self {
461 Self::Url {
462 base_url: base_url.into(),
463 format,
464 with_variation_selectors: false,
465 }
466 }
467
468 pub fn builder<F>(builder: F, format: EmojiFormat) -> Self
469 where
470 F: Fn(&str) -> String + Send + Sync + 'static,
471 {
472 Self::Builder {
473 builder: Arc::new(builder),
474 format,
475 with_variation_selectors: false,
476 }
477 }
478
479 pub fn with_variation_selectors(mut self, with_variation_selectors: bool) -> Self {
480 match &mut self {
481 Self::Url {
482 with_variation_selectors: value,
483 ..
484 }
485 | Self::Builder {
486 with_variation_selectors: value,
487 ..
488 } => {
489 *value = with_variation_selectors;
490 }
491 }
492
493 self
494 }
495
496 pub fn resolve_url(&self, emoji: &str) -> Option<String> {
497 if emoji.is_empty() {
498 return None;
499 }
500
501 let code = match self {
502 Self::Url {
503 with_variation_selectors,
504 ..
505 }
506 | Self::Builder {
507 with_variation_selectors,
508 ..
509 } => emoji_codepoint_string(emoji, *with_variation_selectors)?,
510 };
511
512 match self {
513 Self::Url {
514 base_url, format, ..
515 } => {
516 let trimmed = base_url.trim_end_matches('/');
517 Some(format!("{trimmed}/{code}.{}", format.extension()))
518 }
519 Self::Builder { builder, .. } => Some(builder(&code)),
520 }
521 }
522}
523
524#[derive(Clone)]
525pub struct FontStore {
526 families: HashMap<String, FontFamily>,
527 emoji_source: Option<EmojiSource>,
528 hyphenation_callback: HyphenationCallback,
529}
530
531impl Default for FontStore {
532 fn default() -> Self {
533 Self::new()
534 }
535}
536
537impl FontStore {
538 pub fn new() -> Self {
539 let mut store = Self {
540 families: HashMap::new(),
541 emoji_source: None,
542 hyphenation_callback: Arc::new(|word| vec![word.to_string()]),
543 };
544 store.register_standard_fonts();
545 store
546 }
547
548 pub fn register_font(&mut self, registration: FontRegistration) -> Result<()> {
549 validate_family_name(registration.family())?;
550 self.insert_font(registration);
551 Ok(())
552 }
553
554 pub fn register_family(&mut self, registration: FontFamilyRegistration) -> Result<()> {
555 validate_family_name(registration.family())?;
556
557 let FontFamilyRegistration { family, fonts } = registration;
558
559 for font in fonts {
560 let registration = FontRegistration::new(family.clone(), font.source)
561 .with_style(font.font_style)
562 .with_weight(font.font_weight);
563 self.insert_font(registration);
564 }
565
566 Ok(())
567 }
568
569 pub fn get_font(&self, descriptor: &FontDescriptor) -> Result<RegisteredFont> {
570 let family =
571 self.families
572 .get(descriptor.family())
573 .ok_or_else(|| Error::UnknownFontFamily {
574 family: descriptor.family().to_string(),
575 })?;
576
577 let weights =
578 family
579 .fonts
580 .get(&descriptor.font_style())
581 .ok_or_else(|| Error::UnknownFontStyle {
582 family: descriptor.family().to_string(),
583 style: descriptor.font_style().to_string(),
584 })?;
585
586 let resolved_weight =
587 resolve_font_weight(weights.keys().copied(), descriptor.font_weight()).ok_or_else(
588 || Error::UnknownFontWeight {
589 family: descriptor.family().to_string(),
590 style: descriptor.font_style().to_string(),
591 weight: descriptor.font_weight().value(),
592 },
593 )?;
594
595 weights
596 .get(&resolved_weight)
597 .cloned()
598 .ok_or_else(|| Error::UnknownFontWeight {
599 family: descriptor.family().to_string(),
600 style: descriptor.font_style().to_string(),
601 weight: descriptor.font_weight().value(),
602 })
603 }
604
605 pub async fn load(&self, descriptor: &FontDescriptor) -> Result<LoadedFont> {
606 let font = self.get_font(descriptor)?;
607 let data = load_source(&font.source).await?;
608
609 Ok(LoadedFont {
610 descriptor: font.descriptor,
611 source: font.source,
612 data,
613 })
614 }
615
616 pub fn register_emoji_source(&mut self, source: EmojiSource) {
617 self.emoji_source = Some(source);
618 }
619
620 pub fn emoji_source(&self) -> Option<&EmojiSource> {
621 self.emoji_source.as_ref()
622 }
623
624 pub fn resolve_emoji_url(&self, emoji: &str) -> Option<String> {
625 self.emoji_source.as_ref()?.resolve_url(emoji)
626 }
627
628 pub fn register_hyphenation_callback<F>(&mut self, callback: F)
629 where
630 F: Fn(&str) -> Vec<String> + Send + Sync + 'static,
631 {
632 self.hyphenation_callback = Arc::new(callback);
633 }
634
635 pub fn hyphenate(&self, word: &str) -> Vec<String> {
636 (self.hyphenation_callback)(word)
637 }
638
639 fn insert_font(&mut self, registration: FontRegistration) {
640 let family = self
641 .families
642 .entry(registration.family.clone())
643 .or_default();
644
645 family
646 .fonts
647 .entry(registration.font_style)
648 .or_default()
649 .insert(
650 registration.font_weight,
651 RegisteredFont {
652 descriptor: registration.descriptor(),
653 source: registration.source,
654 },
655 );
656 }
657
658 fn register_standard_fonts(&mut self) {
659 self.insert_standard_font(StandardFont::Helvetica);
660 self.insert_standard_font(StandardFont::HelveticaBold);
661 self.insert_standard_font(StandardFont::HelveticaOblique);
662 self.insert_standard_font(StandardFont::HelveticaBoldOblique);
663 self.insert_standard_font(StandardFont::Courier);
664 self.insert_standard_font(StandardFont::CourierBold);
665 self.insert_standard_font(StandardFont::CourierOblique);
666 self.insert_standard_font(StandardFont::CourierBoldOblique);
667 self.insert_standard_font(StandardFont::TimesRoman);
668 self.insert_standard_font(StandardFont::TimesBold);
669 self.insert_standard_font(StandardFont::TimesItalic);
670 self.insert_standard_font(StandardFont::TimesBoldItalic);
671 self.insert_standard_font(StandardFont::Symbol);
672 self.insert_standard_font(StandardFont::ZapfDingbats);
673 }
674
675 fn insert_standard_font(&mut self, font: StandardFont) {
676 self.insert_font(
677 FontRegistration::new(font.family_name(), FontSource::standard(font))
678 .with_style(font.font_style())
679 .with_weight(font.font_weight()),
680 );
681 }
682}
683
684#[derive(Clone, Debug, Default)]
685struct FontFamily {
686 fonts: HashMap<FontStyle, BTreeMap<FontWeight, RegisteredFont>>,
687}
688
689fn validate_family_name(family: &str) -> Result<()> {
690 if family.trim().is_empty() {
691 Err(Error::InvalidFontSource {
692 message: String::from("font family cannot be empty"),
693 })
694 } else {
695 Ok(())
696 }
697}
698
699fn resolve_font_weight(
700 weights: impl IntoIterator<Item = FontWeight>,
701 target: FontWeight,
702) -> Option<FontWeight> {
703 let weights: Vec<_> = weights.into_iter().collect();
704 if weights.is_empty() {
705 return None;
706 }
707
708 if let Some(weight) = weights.iter().copied().find(|weight| *weight == target) {
709 return Some(weight);
710 }
711
712 let target_value = target.value();
713 let exact_or_between = |start: u16, end: u16| {
714 weights
715 .iter()
716 .copied()
717 .filter(|weight| {
718 let value = weight.value();
719 value >= start && value <= end
720 })
721 .min_by_key(|weight| weight.value())
722 };
723 let below_desc = || {
724 weights
725 .iter()
726 .copied()
727 .filter(|weight| weight.value() < target_value)
728 .max_by_key(|weight| weight.value())
729 };
730 let above_asc_from = |threshold: u16| {
731 weights
732 .iter()
733 .copied()
734 .filter(|weight| weight.value() > threshold)
735 .min_by_key(|weight| weight.value())
736 };
737 let above_target = || {
738 weights
739 .iter()
740 .copied()
741 .filter(|weight| weight.value() > target_value)
742 .min_by_key(|weight| weight.value())
743 };
744
745 if (400..=500).contains(&target_value) {
746 exact_or_between(target_value, 500)
747 .or_else(below_desc)
748 .or_else(|| above_asc_from(500))
749 } else if target_value < 400 {
750 below_desc().or_else(above_target)
751 } else {
752 above_target().or_else(below_desc)
753 }
754}
755
756async fn load_source(source: &FontSource) -> Result<LoadedFontData> {
757 match source {
758 FontSource::Standard(font) => Ok(LoadedFontData::Standard(*font)),
759 FontSource::Local(path) => {
760 let bytes = tokio::fs::read(path)
761 .await
762 .map_err(|error| Error::LocalFontLoad {
763 path: path.display().to_string(),
764 message: error.to_string(),
765 })?;
766 Ok(LoadedFontData::Binary(bytes))
767 }
768 FontSource::Remote(url) => {
769 let parsed = reqwest::Url::parse(url).map_err(|error| Error::InvalidFontSource {
770 message: format!("invalid remote font URL `{url}`: {error}"),
771 })?;
772 let scheme = parsed.scheme();
773 if scheme != "http" && scheme != "https" {
774 return Err(Error::UnsupportedRemoteScheme {
775 scheme: scheme.to_string(),
776 });
777 }
778
779 let response = reqwest::get(parsed)
780 .await
781 .map_err(|error| Error::RemoteFontLoad {
782 url: url.clone(),
783 message: error.to_string(),
784 })?
785 .error_for_status()
786 .map_err(|error| Error::RemoteFontLoad {
787 url: url.clone(),
788 message: error.to_string(),
789 })?;
790
791 let bytes = response
792 .bytes()
793 .await
794 .map_err(|error| Error::RemoteFontLoad {
795 url: url.clone(),
796 message: error.to_string(),
797 })?;
798
799 Ok(LoadedFontData::Binary(bytes.to_vec()))
800 }
801 FontSource::DataUri(uri) => Ok(LoadedFontData::Binary(decode_data_uri(uri)?)),
802 }
803}
804
805fn decode_data_uri(uri: &str) -> Result<Vec<u8>> {
806 let encoded = uri
807 .strip_prefix("data:")
808 .ok_or_else(|| Error::InvalidDataUri {
809 message: String::from("data URI must start with `data:`"),
810 })?;
811
812 let (metadata, payload) = encoded
813 .split_once(',')
814 .ok_or_else(|| Error::InvalidDataUri {
815 message: String::from("data URI must contain a metadata and payload separator"),
816 })?;
817
818 let is_base64 = metadata
819 .split(';')
820 .any(|part| part.eq_ignore_ascii_case("base64"));
821
822 if !is_base64 {
823 return Err(Error::InvalidDataUri {
824 message: String::from("only base64-encoded font data URIs are supported"),
825 });
826 }
827
828 base64::engine::general_purpose::STANDARD
829 .decode(payload)
830 .map_err(|error| Error::InvalidDataUri {
831 message: error.to_string(),
832 })
833}
834
835fn emoji_codepoint_string(emoji: &str, with_variation_selectors: bool) -> Option<String> {
836 let codes = emoji
837 .chars()
838 .filter(|character| with_variation_selectors || *character != '\u{fe0f}')
839 .map(|character| format!("{:x}", character as u32))
840 .collect::<Vec<_>>();
841
842 if codes.is_empty() {
843 None
844 } else {
845 Some(codes.join("-"))
846 }
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852 use std::io;
853 use tempfile::tempdir;
854 use tokio::io::{AsyncReadExt, AsyncWriteExt};
855 use tokio::net::TcpListener;
856
857 #[test]
858 fn registers_standard_fonts_by_default() {
859 let store = FontStore::new();
860
861 let descriptor = FontDescriptor::new("Helvetica").with_weight(FontWeight::BOLD);
862 let font = store
863 .get_font(&descriptor)
864 .expect("standard font should resolve");
865
866 assert_eq!(
867 font.source(),
868 &FontSource::standard(StandardFont::HelveticaBold)
869 );
870 }
871
872 #[test]
873 fn resolves_fallback_weights_using_css_rules() {
874 let mut store = FontStore::new();
875 store
876 .register_family(FontFamilyRegistration::new(
877 "Inter",
878 [
879 FontVariantRegistration::new(FontSource::data_uri(font_data_uri(b"regular")))
880 .with_weight(FontWeight::NORMAL),
881 FontVariantRegistration::new(FontSource::data_uri(font_data_uri(b"medium")))
882 .with_weight(FontWeight::MEDIUM),
883 FontVariantRegistration::new(FontSource::data_uri(font_data_uri(b"bold")))
884 .with_weight(FontWeight::BOLD),
885 ],
886 ))
887 .expect("family registration should succeed");
888
889 let mediumish = FontDescriptor::new("Inter")
890 .with_weight(FontWeight::new(450).expect("450 is a valid weight"));
891 let heavy = FontDescriptor::new("Inter")
892 .with_weight(FontWeight::new(800).expect("800 is a valid weight"));
893
894 let mediumish_font = store
895 .get_font(&mediumish)
896 .expect("450 should resolve to 500");
897 let heavy_font = store.get_font(&heavy).expect("800 should resolve to 700");
898
899 assert_eq!(
900 mediumish_font.descriptor().font_weight(),
901 FontWeight::MEDIUM
902 );
903 assert_eq!(heavy_font.descriptor().font_weight(), FontWeight::BOLD);
904 }
905
906 #[tokio::test]
907 async fn loads_local_fonts_asynchronously() {
908 let mut store = FontStore::new();
909 let directory = tempdir().expect("temporary directory should be created");
910 let path = directory.path().join("local-font.ttf");
911 let expected = b"local-font-bytes";
912 std::fs::write(&path, expected).expect("font file should be written");
913
914 store
915 .register_font(FontRegistration::new(
916 "LocalFamily",
917 FontSource::local(&path),
918 ))
919 .expect("font registration should succeed");
920
921 let loaded = store
922 .load(&FontDescriptor::new("LocalFamily"))
923 .await
924 .expect("local font should load");
925
926 assert_eq!(loaded.bytes(), Some(expected.as_slice()));
927 }
928
929 #[tokio::test]
930 async fn loads_remote_fonts_asynchronously() {
931 let mut store = FontStore::new();
932 let expected = b"remote-font-bytes".to_vec();
933 let url = spawn_font_server(expected.clone())
934 .await
935 .expect("test server should start");
936
937 store
938 .register_font(FontRegistration::new(
939 "RemoteFamily",
940 FontSource::remote(url),
941 ))
942 .expect("font registration should succeed");
943
944 let loaded = store
945 .load(&FontDescriptor::new("RemoteFamily"))
946 .await
947 .expect("remote font should load");
948
949 assert_eq!(loaded.bytes(), Some(expected.as_slice()));
950 }
951
952 #[tokio::test]
953 async fn loads_data_uri_fonts_asynchronously() {
954 let mut store = FontStore::new();
955 let expected = b"data-uri-font-bytes";
956
957 store
958 .register_font(FontRegistration::new(
959 "DataUriFamily",
960 FontSource::data_uri(font_data_uri(expected)),
961 ))
962 .expect("font registration should succeed");
963
964 let loaded = store
965 .load(&FontDescriptor::new("DataUriFamily"))
966 .await
967 .expect("data URI font should load");
968
969 assert_eq!(loaded.bytes(), Some(expected.as_slice()));
970 }
971
972 #[tokio::test]
973 async fn resolves_standard_fonts_without_binary_loading() {
974 let store = FontStore::new();
975 let loaded = store
976 .load(&FontDescriptor::new("Times-Roman").with_style(FontStyle::Italic))
977 .await
978 .expect("standard font should load");
979
980 assert_eq!(loaded.standard_font(), Some(StandardFont::TimesItalic));
981 assert_eq!(loaded.bytes(), None);
982 }
983
984 #[test]
985 fn registers_emoji_and_hyphenation_handlers() {
986 let mut store = FontStore::new();
987 store.register_hyphenation_callback(|word| {
988 vec![word[..5].to_string(), word[5..].to_string()]
989 });
990 store.register_emoji_source(EmojiSource::url(
991 "https://example.com/emojis/",
992 EmojiFormat::Png,
993 ));
994
995 assert_eq!(
996 store.hyphenate("graphite"),
997 vec![String::from("graph"), String::from("ite")]
998 );
999 assert_eq!(
1000 store.resolve_emoji_url("☺️"),
1001 Some(String::from("https://example.com/emojis/263a.png"))
1002 );
1003
1004 store.register_emoji_source(
1005 EmojiSource::url("https://example.com/emojis", EmojiFormat::Png)
1006 .with_variation_selectors(true),
1007 );
1008
1009 assert_eq!(
1010 store.resolve_emoji_url("☺️"),
1011 Some(String::from("https://example.com/emojis/263a-fe0f.png"))
1012 );
1013 }
1014
1015 fn font_data_uri(bytes: &[u8]) -> String {
1016 format!(
1017 "data:font/ttf;base64,{}",
1018 base64::engine::general_purpose::STANDARD.encode(bytes)
1019 )
1020 }
1021
1022 async fn spawn_font_server(body: Vec<u8>) -> io::Result<String> {
1023 let listener = TcpListener::bind("127.0.0.1:0").await?;
1024 let address = listener.local_addr()?;
1025
1026 tokio::spawn(async move {
1027 let (mut stream, _) = listener
1028 .accept()
1029 .await
1030 .expect("test server should accept a connection");
1031 let mut request_buffer = [0_u8; 1024];
1032 let _ = stream.read(&mut request_buffer).await;
1033
1034 let response = format!(
1035 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1036 body.len()
1037 );
1038 stream
1039 .write_all(response.as_bytes())
1040 .await
1041 .expect("test server should write headers");
1042 stream
1043 .write_all(&body)
1044 .await
1045 .expect("test server should write body");
1046 });
1047
1048 Ok(format!("http://{address}/font.ttf"))
1049 }
1050}