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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
mod cmap;
mod parsing;
mod tables;
mod types;

use crate::fonts::cmap::Cmap;
use crate::fonts::parsing::*;
use crate::fonts::tables::*;
use crate::fonts::types::Tag;
use std::borrow::Cow;
use std::cmp;
use std::sync::Arc;

/// The EM square unit
pub struct Em;

/// The unit of FWord and UFWord
struct FontDesignUnit;

#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
pub struct GlyphId(pub u16);

#[derive(Debug)]
pub enum FontError {
    /// Victor only supports TrueType fonts at the moment.
    UnsupportedFormat,

    /// The font file contains an offset to beyond the end of the file.
    OffsetBeyondEof,

    /// The font file contains an offset that puts the end of the pointed object
    /// beyond the end of the file.
    OffsetPlusLengthBeyondEof,

    /// One of the required TrueType tables is missing in this font.
    MissingTable,

    /// This font doesn’t have a “PostScript name” string in a supported encoding.
    NoSupportedPostscriptName,

    /// This font doesn’t have a character map in a supported format.
    NoSupportedCmap,

    /// This font doesn’t have any horizontal metrics for glyphs.
    NoHorizontalGlyphMetrics,
}

pub struct Font {
    bytes: Cow<'static, [u8]>,
    cmap: Cmap,
    postscript_name: String,
    glyph_count: u16,
    font_design_units_per_em: euclid::TypedScale<f32, Em, FontDesignUnit>,
    horizontal_metrics: Slice<LongHorizontalMetricsRecord>,

    /// Distance from baseline of highest ascender
    ascender: euclid::Length<i16, FontDesignUnit>,

    /// Distance from baseline of lowest descender
    descender: euclid::Length<i16, FontDesignUnit>,

    /// The bounding box of the union of all glyphs
    min_x: euclid::Length<i16, FontDesignUnit>,
    min_y: euclid::Length<i16, FontDesignUnit>,
    max_x: euclid::Length<i16, FontDesignUnit>,
    max_y: euclid::Length<i16, FontDesignUnit>,
}

// #[cfg(target_pointer_width = "64")]
// fn _assert_size_of() {
//     let _ = std::mem::transmute::<Cmap, [u8; 24]>;
//     let _ = std::mem::transmute::<Font, [u8; 112]>;
// }

impl Font {
    pub fn parse<B: Into<Cow<'static, [u8]>>>(bytes: B) -> Result<Arc<Self>, FontError> {
        Self::parse_cow(bytes.into())
    }

    fn parse_cow(bytes: Cow<'static, [u8]>) -> Result<Arc<Self>, FontError> {
        let mut font = Self::parse_without_cow_bytes_field(&bytes)?;
        font.bytes = bytes;
        Ok(Arc::new(font))
    }

    #[inline]
    fn parse_without_cow_bytes_field(bytes: &[u8]) -> Result<Self, FontError> {
        let bytes: &[u8] = &*bytes;
        let offset_table = Position::<OffsetSubtable>::initial();
        let scaler_type = offset_table.scaler_type().read_from(bytes)?;
        const TRUETYPE: u32 = 0x74727565; // "true" in big-endian
        if scaler_type != TRUETYPE && scaler_type != 0x_0001_0000 {
            Err(FontError::UnsupportedFormat)?
        }
        let table_directory = Slice::new(
            offset_table.followed_by::<TableDirectoryEntry>(),
            offset_table.table_count().read_from(bytes)?,
        );

        let maxp = table_directory.find_table::<MaximumProfile>(bytes)?;
        let header = table_directory.find_table::<FontHeader>(bytes)?;
        let glyph_count = maxp.num_glyphs().read_from(bytes)?;
        let horizontal_header = table_directory.find_table::<HorizontalHeader>(bytes)?;

        Ok(Font {
            bytes: b""[..].into(),
            postscript_name: read_postscript_name(&bytes, table_directory)?,
            cmap: Cmap::parse(bytes, table_directory)?,
            glyph_count,
            horizontal_metrics: Slice::new(
                table_directory.find_table::<LongHorizontalMetricsRecord>(bytes)?,
                horizontal_header
                    .number_of_long_horizontal_metrics()
                    .read_from(bytes)?,
            ),
            font_design_units_per_em: header.units_per_em().read_from(bytes)?.cast(),
            ascender: horizontal_header.ascender().read_from(bytes)?,
            descender: horizontal_header.descender().read_from(bytes)?,
            min_x: header.min_x().read_from(bytes)?,
            min_y: header.min_y().read_from(bytes)?,
            max_x: header.max_x().read_from(bytes)?,
            max_y: header.max_y().read_from(bytes)?,
        })
    }

    pub fn bytes(&self) -> &[u8] {
        &self.bytes
    }
    pub fn postscript_name(&self) -> &str {
        &self.postscript_name
    }
    pub fn glyph_count(&self) -> u16 {
        self.glyph_count
    }

    pub fn each_code_point<F>(&self, f: F) -> Result<(), FontError>
    where
        F: FnMut(char, GlyphId),
    {
        self.cmap.each_code_point(&self.bytes, f)
    }

    pub fn glyph_id(&self, ch: char) -> Result<GlyphId, FontError> {
        let ch = ch as u32;
        let result = match self.cmap {
            Cmap::Format4(ref table) => table.get(&self.bytes, ch),
            Cmap::Format12(ref table) => table.get(&self.bytes, ch),
        };
        const NOTDEF_GLYPH: u16 = 0;
        Ok(GlyphId(result?.unwrap_or(NOTDEF_GLYPH)))
    }

    pub fn glyph_width(&self, glyph_id: GlyphId) -> Result<euclid::Length<f32, Em>, FontError> {
        let last_index = self
            .horizontal_metrics
            .count()
            .checked_sub(1)
            .ok_or(FontError::NoHorizontalGlyphMetrics)?;
        let index = cmp::min(glyph_id.0 as u32, last_index);
        let w = self
            .horizontal_metrics
            .get_unchecked(index)
            .advance_width()
            .read_from(&self.bytes)?;
        Ok(self.to_ems(w))
    }

    fn to_ems<T>(&self, length: euclid::Length<T, FontDesignUnit>) -> euclid::Length<f32, Em>
    where
        T: num_traits::NumCast + Clone,
    {
        length.cast() / self.font_design_units_per_em
    }

    pub fn ascender(&self) -> euclid::Length<f32, Em> {
        self.to_ems(self.ascender)
    }
    pub fn descender(&self) -> euclid::Length<f32, Em> {
        self.to_ems(self.descender)
    }
    pub fn min_x(&self) -> euclid::Length<f32, Em> {
        self.to_ems(self.min_x)
    }
    pub fn min_y(&self) -> euclid::Length<f32, Em> {
        self.to_ems(self.min_y)
    }
    pub fn max_x(&self) -> euclid::Length<f32, Em> {
        self.to_ems(self.max_x)
    }
    pub fn max_y(&self) -> euclid::Length<f32, Em> {
        self.to_ems(self.max_y)
    }
}

fn read_postscript_name(
    bytes: &[u8],
    table_directory: Slice<TableDirectoryEntry>,
) -> Result<String, FontError> {
    /// Macintosh encodings seem to be ASCII-compatible, and a PostScript name is within ASCII
    fn decode_macintosh(string_bytes: &[u8]) -> String {
        String::from_utf8_lossy(string_bytes).into_owned()
    }

    /// Latin-1 range only
    fn decode_ucs2(string_bytes: &[u8]) -> String {
        string_bytes
            .chunks(2)
            .map(|chunk| {
                if chunk.len() < 2 || chunk[0] != 0 {
                    '\u{FFFD}'
                } else {
                    chunk[1] as char
                }
            })
            .collect::<String>()
    }

    let naming_table_header = table_directory.find_table::<NamingTableHeader>(bytes)?;
    let name_records = Slice::new(
        naming_table_header.followed_by::<NameRecord>(),
        naming_table_header.count().read_from(bytes)?,
    );
    let string_storage_start: Position<()> =
        naming_table_header.offset_bytes(naming_table_header.string_offset().read_from(bytes)?);
    let string_bytes = |record: Position<NameRecord>| {
        Slice::<u8>::new(
            string_storage_start.offset_bytes(record.string_offset().read_from(bytes)?),
            record.length().read_from(bytes)?,
        )
        .read_from(bytes)
    };

    for record in name_records {
        const POSTSCRIPT_NAME: u16 = 6;
        if record.name_id().read_from(bytes)? != POSTSCRIPT_NAME {
            continue;
        }

        const MACINTOSH: u16 = 1;
        const MICROSOFT: u16 = 3;
        const UNICODE_BMP: u16 = 1;
        let postscript_name = match (
            record.platform_id().read_from(bytes)?,
            record.encoding_id().read_from(bytes)?,
        ) {
            (MACINTOSH, _) => decode_macintosh(string_bytes(record)?),
            (MICROSOFT, UNICODE_BMP) => decode_ucs2(string_bytes(record)?),
            _ => continue,
        };
        return Ok(postscript_name);
    }

    Err(FontError::NoSupportedPostscriptName)
}

trait SfntTable {
    const TAG: Tag;
}

impl Slice<TableDirectoryEntry> {
    fn find_table<T: SfntTable>(&self, bytes: &[u8]) -> Result<Position<T>, FontError> {
        let search = self.binary_search_by_key(&T::TAG, |entry| entry.tag().read_from(bytes))?;
        let entry = search.ok_or(FontError::MissingTable)?;
        let offset = entry.table_offset().read_from(bytes)?;
        Ok(Position::<OffsetSubtable>::initial().offset_bytes(offset))
    }
}

#[doc(hidden)]
pub mod _reexports_for_macros {
    pub use lazy_static::*;
    pub use std::sync::Arc;
}

#[macro_export]
macro_rules! include_fonts {
    ( $( $NAME: ident: $filename: expr, )+ ) => {
        $(
            $crate::fonts::_reexports_for_macros::lazy_static! {
                pub static ref $NAME:
                    $crate::fonts::_reexports_for_macros::Arc<$crate::fonts::Font> =
                {
                    $crate::fonts::Font::parse(
                        include_bytes!($filename) as &'static [u8]
                    ).unwrap()
                };
            }
        )+
    };
}

include_fonts! {
    BITSTREAM_VERA_SANS: "../../fonts/vera/Vera.ttf",
}