Skip to main content

graphitepdf_font/
lib.rs

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}