1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use std::collections::HashSet;
use std::fs::File;
use std::path::Path;

use futures::future::join_all;
use protobuf::Message;
use tokio::task::spawn_blocking;

use crate::proto::glyphs::{Fontstack, Glyphs};
use crate::PbfFontError;
use crate::PbfFontError::MissingFontFamilyName;

/// Generates a single combined font stack for the set of fonts provided.
///
/// See the documentation for [combine_glyphs] for further details.
/// Unlike [combine_glyphs], the result of this method will always contain a `glyphs` message,
/// even if the loaded range is empty for a given font.
pub async fn get_named_font_stack<P: AsRef<Path>>(
    font_path: P,
    font_names: &[&str],
    stack_name: String,
    start: u32,
    end: u32,
) -> Result<Glyphs, PbfFontError> {
    if font_names.is_empty() {
        return Err(MissingFontFamilyName);
    }

    // Load fonts
    let glyph_data = join_all(
        font_names
            .iter()
            .map(|font| load_glyphs(font_path.as_ref(), font, start, end)),
    )
    .await
    .into_iter()
    .filter_map(|g| g.ok())
    .collect();

    // Combine all the glyphs into a single instance, using the ordering to determine priority.
    // This can take some time, so mark it blocking.
    Ok(spawn_blocking(move || combine_glyphs(glyph_data))
        .await?
        .unwrap_or_else(|| {
            // Construct an empty message manually if the range is not covered
            let mut result = Glyphs::new();

            let mut stack = Fontstack::new();
            stack.set_name(stack_name);
            stack.set_range(format!("{start}-{end}"));

            result.stacks.push(stack);
            result
        }))
}

pub async fn get_font_stack<P: AsRef<Path>>(
    font_path: P,
    font_names: &[&str],
    start: u32,
    end: u32,
) -> Result<Glyphs, PbfFontError> {
    let stack_name = font_names.join(", ");
    get_named_font_stack(font_path, font_names, stack_name, start, end).await
}

/// Loads a single font PBF slice from disk.
///
/// Fonts are assumed to be stored in `<font_path>/<font_name>/<start>-<end>.pbf`.
pub async fn load_glyphs<P: AsRef<Path>>(
    font_path: P,
    font_name: &str,
    start: u32,
    end: u32,
) -> Result<Glyphs, PbfFontError> {
    let full_path = font_path
        .as_ref()
        .join(font_name)
        .join(format!("{start}-{end}.pbf"));

    // Note: Counter-intuitively, it's much faster to use blocking IO with `spawn_blocking` here,
    // since the `Message::parse_` call will block as well.
    Ok(spawn_blocking(|| {
        let mut file = File::open(full_path)?;
        Message::parse_from_reader(&mut file)
    })
    .await??)
}

/// Combines a list of SDF font glyphs into a single glyphs message.
/// All input font stacks are flattened into a single font stack containing all the glyphs.
/// The input order indicates precedence. If the same glyph ID is encountered multiple times,
/// only the first will be used.
///
/// NOTE: This returns `None` if there are no glyphs in the range. If you need to
/// construct an empty message, the responsibility lies with the caller.
#[must_use]
pub fn combine_glyphs(glyphs_to_combine: Vec<Glyphs>) -> Option<Glyphs> {
    let mut result = Glyphs::new();
    let mut combined_stack = Fontstack::new();
    let mut coverage: HashSet<u32> = HashSet::new();
    let mut start = u32::MAX;
    let mut end = u32::MIN;

    for mut glyph_stack in glyphs_to_combine {
        for mut font_stack in glyph_stack.stacks.drain(..) {
            if combined_stack.has_name() {
                let name = combined_stack.mut_name();
                name.push_str(", ");
                name.push_str(&font_stack.take_name());
            } else {
                combined_stack.set_name(font_stack.take_name());
            }

            for glyph in font_stack.glyphs.drain(..) {
                if let Some(id) = glyph.id {
                    if coverage.insert(id) {
                        combined_stack.glyphs.push(glyph);
                        if id < start {
                            start = id;
                        }
                        if id > end {
                            end = id;
                        }
                    }
                }
            }
        }
    }

    if coverage.is_empty() {
        return None;
    }

    combined_stack.set_range(format!("{start}-{end}"));
    result.stacks.push(combined_stack);

    Some(result)
}