allsorts_subset_browser/tables/
os2.rs

1//! Parsing of the `OS/2` table.
2//!
3//! > The OS/2 table consists of a set of metrics and other data that are required in OpenType fonts.
4//!
5//! — <https://docs.microsoft.com/en-us/typography/opentype/spec/os2>
6
7use std::convert::TryInto;
8
9use bitflags::bitflags;
10
11use crate::binary::read::{ReadBinaryDep, ReadCtxt};
12use crate::binary::write::{WriteBinary, WriteContext};
13use crate::binary::{I16Be, U16Be, U32Be};
14use crate::error::{ParseError, WriteError};
15use crate::tables::Fixed;
16
17/// `OS/2` table
18///
19/// <https://docs.microsoft.com/en-us/typography/opentype/spec/os2>
20#[derive(Clone)]
21pub struct Os2 {
22    pub version: u16,
23    pub x_avg_char_width: i16,
24    pub us_weight_class: u16,
25    pub us_width_class: u16,
26    pub fs_type: u16,
27    pub y_subscript_x_size: i16,
28    pub y_subscript_y_size: i16,
29    pub y_subscript_x_offset: i16,
30    pub y_subscript_y_offset: i16,
31    pub y_superscript_x_size: i16,
32    pub y_superscript_y_size: i16,
33    pub y_superscript_x_offset: i16,
34    pub y_superscript_y_offset: i16,
35    pub y_strikeout_size: i16,
36    pub y_strikeout_position: i16,
37    pub s_family_class: i16,
38    pub panose: [u8; 10],
39    pub ul_unicode_range1: u32,
40    pub ul_unicode_range2: u32,
41    pub ul_unicode_range3: u32,
42    pub ul_unicode_range4: u32,
43    pub ach_vend_id: u32, // tag
44    pub fs_selection: FsSelection,
45    pub us_first_char_index: u16,
46    pub us_last_char_index: u16,
47    // Note: Documentation for OS/2 version 0 in Apple’s TrueType Reference Manual stops at the
48    // usLastCharIndex field and does not include the last five fields of the table as it was
49    // defined by Microsoft. Some legacy TrueType fonts may have been built with a shortened
50    // version 0 OS/2 table. Applications should check the table length for a version 0 OS/2 table
51    // before reading these fields.
52    pub version0: Option<Version0>,
53    pub version1: Option<Version1>,
54    pub version2to4: Option<Version2to4>,
55    pub version5: Option<Version5>,
56}
57
58#[derive(Clone)]
59pub struct Version0 {
60    pub s_typo_ascender: i16,
61    pub s_typo_descender: i16,
62    pub s_typo_line_gap: i16,
63    pub us_win_ascent: u16,
64    pub us_win_descent: u16,
65}
66
67#[derive(Clone)]
68pub struct Version1 {
69    pub ul_code_page_range1: u32,
70    pub ul_code_page_range2: u32,
71}
72
73#[derive(Clone)]
74pub struct Version2to4 {
75    pub sx_height: i16,
76    pub s_cap_height: i16,
77    pub us_default_char: u16,
78    pub us_break_char: u16,
79    pub us_max_context: u16,
80}
81
82#[derive(Clone)]
83pub struct Version5 {
84    pub us_lower_optical_point_size: u16,
85    pub us_upper_optical_point_size: u16,
86}
87
88bitflags! {
89    /// fsSelection field in `OS/2`
90    ///
91    /// ```text
92    /// Bit #  macStyle bit  C definition     Description
93    /// 0      bit 1         ITALIC           Font contains italic or oblique glyphs, otherwise
94    ///                                       they are upright.
95    /// 1                    UNDERSCORE       glyphs are underscored.
96    /// 2                    NEGATIVE         glyphs have their foreground and background reversed.
97    /// 3                    OUTLINED         Outline (hollow) glyphs, otherwise they are solid.
98    /// 4                    STRIKEOUT        glyphs are overstruck.
99    /// 5      bit 0         BOLD             glyphs are emboldened.
100    /// 6                    REGULAR          glyphs are in the standard weight/style for the font.
101    /// 7                    USE_TYPO_METRICS If set, it is strongly recommended that applications
102    ///                                       use OS/2.sTypoAscender - OS/2.sTypoDescender +
103    ///                                       OS/2.sTypoLineGap as the default line spacing for
104    ///                                       this font.
105    /// 8                    WWS              The font has 'name' table strings consistent with a
106    ///                                       weight/width/slope family without requiring use of
107    ///                                       name IDs 21 and 22.
108    /// 9                    OBLIQUE          Font contains oblique glyphs.
109    /// 10–15                <reserved>       Reserved; set to 0.
110    /// ```
111    pub struct FsSelection: u16 {
112        const ITALIC = 1 << 0;
113        const UNDERSCORE = 1 << 1;
114        const NEGATIVE = 1 << 2;
115        const OUTLINED = 1 << 3;
116        const STRIKEOUT = 1 << 4;
117        const BOLD = 1 << 5;
118        const REGULAR = 1 << 6;
119        const USE_TYPO_METRICS = 1 << 7;
120        const WWS = 1 << 8;
121        const OBLIQUE = 1 << 9;
122        // 10–15 Reserved; set to 0.
123    }
124}
125
126impl Os2 {
127    /// Map a width value to an OS/2 width class
128    pub(crate) fn value_to_width_class(value: Fixed) -> u16 {
129        const WIDTH_CLASS_MAP: &[(Fixed, u16)] = &[
130            // using from_raw so it works in const context
131            (Fixed::from_raw(3276800), 1),  // 50.0
132            (Fixed::from_raw(4096000), 2),  // 62.5
133            (Fixed::from_raw(4915200), 3),  // 75.0
134            (Fixed::from_raw(5734400), 4),  // 87.5
135            (Fixed::from_raw(6553600), 5),  // 100.0
136            (Fixed::from_raw(7372800), 6),  // 112.5
137            (Fixed::from_raw(8192000), 7),  // 125.0
138            (Fixed::from_raw(9830400), 8),  // 150.0
139            (Fixed::from_raw(13107200), 9), // 200.0
140        ];
141
142        // Map the value to one of the width classes. Width is a percentage and can be
143        // anything > 1 but classes are only defined for 50% to 200%.
144        match WIDTH_CLASS_MAP.binary_search_by_key(&value, |&(val, _cls)| val) {
145            Ok(i) => WIDTH_CLASS_MAP[i].1,
146            Err(i) => {
147                // get the values at i and i-1, choose the one it's closer to
148                if i < 1 {
149                    return WIDTH_CLASS_MAP[0].1;
150                }
151                if i >= WIDTH_CLASS_MAP.len() {
152                    return WIDTH_CLASS_MAP.last().unwrap().1;
153                }
154                let (a, clsa) = WIDTH_CLASS_MAP[i - 1];
155                let (b, clsb) = WIDTH_CLASS_MAP[i];
156                if (value - a) > (b - value) {
157                    clsb
158                } else {
159                    clsa
160                }
161            }
162        }
163    }
164}
165
166impl ReadBinaryDep for Os2 {
167    type HostType<'a> = Self;
168    type Args<'a> = usize;
169
170    // The format of this table has changed over time. The original TrueType specification had this
171    // table at 68 bytes long. The first OpenType version had it at 78 bytes long, and the current
172    // OpenType version is even larger. To determine which kind of table your software is dealing
173    // with, it's best both to consider the table's version and its size.
174    fn read_dep<'a>(ctxt: &mut ReadCtxt<'a>, table_size: usize) -> Result<Self, ParseError> {
175        let version = ctxt.read::<U16Be>()?;
176        let x_avg_char_width = ctxt.read::<I16Be>()?;
177        let us_weight_class = ctxt.read::<U16Be>()?;
178        let us_width_class = ctxt.read::<U16Be>()?;
179        let fs_type = ctxt.read::<U16Be>()?;
180        let y_subscript_x_size = ctxt.read::<I16Be>()?;
181        let y_subscript_y_size = ctxt.read::<I16Be>()?;
182        let y_subscript_x_offset = ctxt.read::<I16Be>()?;
183        let y_subscript_y_offset = ctxt.read::<I16Be>()?;
184        let y_superscript_x_size = ctxt.read::<I16Be>()?;
185        let y_superscript_y_size = ctxt.read::<I16Be>()?;
186        let y_superscript_x_offset = ctxt.read::<I16Be>()?;
187        let y_superscript_y_offset = ctxt.read::<I16Be>()?;
188        let y_strikeout_size = ctxt.read::<I16Be>()?;
189        let y_strikeout_position = ctxt.read::<I16Be>()?;
190        let s_family_class = ctxt.read::<I16Be>()?;
191        // NOTE(unwrap): Safe as slice is guaranteed to have 10 elements
192        let panose: [u8; 10] = ctxt.read_slice(10)?.try_into().unwrap();
193        let ul_unicode_range1 = ctxt.read::<U32Be>()?;
194        let ul_unicode_range2 = ctxt.read::<U32Be>()?;
195        let ul_unicode_range3 = ctxt.read::<U32Be>()?;
196        let ul_unicode_range4 = ctxt.read::<U32Be>()?;
197        let ach_vend_id = ctxt.read::<U32Be>()?;
198        let fs_selection = ctxt.read::<U16Be>().map(FsSelection::from_bits_truncate)?;
199        let us_first_char_index = ctxt.read::<U16Be>()?;
200        let us_last_char_index = ctxt.read::<U16Be>()?;
201
202        // Read version specific fields
203        let version0 = if table_size >= 78 {
204            let s_typo_ascender = ctxt.read::<I16Be>()?;
205            let s_typo_descender = ctxt.read::<I16Be>()?;
206            let s_typo_line_gap = ctxt.read::<I16Be>()?;
207            let us_win_ascent = ctxt.read::<U16Be>()?;
208            let us_win_descent = ctxt.read::<U16Be>()?;
209            Some(Version0 {
210                s_typo_ascender,
211                s_typo_descender,
212                s_typo_line_gap,
213                us_win_ascent,
214                us_win_descent,
215            })
216        } else {
217            None
218        };
219
220        let version1 = if version >= 1 {
221            let ul_code_page_range1 = ctxt.read::<U32Be>()?;
222            let ul_code_page_range2 = ctxt.read::<U32Be>()?;
223            Some(Version1 {
224                ul_code_page_range1,
225                ul_code_page_range2,
226            })
227        } else {
228            None
229        };
230
231        let version2to4 = if version >= 2 {
232            let sx_height = ctxt.read::<I16Be>()?;
233            let s_cap_height = ctxt.read::<I16Be>()?;
234            let us_default_char = ctxt.read::<U16Be>()?;
235            let us_break_char = ctxt.read::<U16Be>()?;
236            let us_max_context = ctxt.read::<U16Be>()?;
237            Some(Version2to4 {
238                sx_height,
239                s_cap_height,
240                us_default_char,
241                us_break_char,
242                us_max_context,
243            })
244        } else {
245            None
246        };
247
248        let version5 = if version >= 5 {
249            let us_lower_optical_point_size = ctxt.read::<U16Be>()?;
250            let us_upper_optical_point_size = ctxt.read::<U16Be>()?;
251            Some(Version5 {
252                us_lower_optical_point_size,
253                us_upper_optical_point_size,
254            })
255        } else {
256            None
257        };
258
259        Ok(Os2 {
260            version,
261            x_avg_char_width,
262            us_weight_class,
263            us_width_class,
264            fs_type,
265            y_subscript_x_size,
266            y_subscript_y_size,
267            y_subscript_x_offset,
268            y_subscript_y_offset,
269            y_superscript_x_size,
270            y_superscript_y_size,
271            y_superscript_x_offset,
272            y_superscript_y_offset,
273            y_strikeout_size,
274            y_strikeout_position,
275            s_family_class,
276            panose,
277            ul_unicode_range1,
278            ul_unicode_range2,
279            ul_unicode_range3,
280            ul_unicode_range4,
281            ach_vend_id,
282            fs_selection,
283            us_first_char_index,
284            us_last_char_index,
285            version0,
286            version1,
287            version2to4,
288            version5,
289        })
290    }
291}
292
293impl WriteBinary<&Self> for Os2 {
294    type Output = ();
295
296    fn write<C: WriteContext>(ctxt: &mut C, table: &Self) -> Result<Self::Output, WriteError> {
297        // TODO: make impossible states unrepresentable
298        // The way the data is structured means that it's possible to have say a v5 struct present
299        // but the preceding ones absent, which is invalid.
300        let version = if table.version5.is_some() {
301            5_u16
302        } else if table.version2to4.is_some() {
303            4
304        } else if table.version1.is_some() {
305            1
306        } else {
307            0
308        };
309
310        U16Be::write(ctxt, version)?;
311        I16Be::write(ctxt, table.x_avg_char_width)?;
312        U16Be::write(ctxt, table.us_weight_class)?;
313        U16Be::write(ctxt, table.us_width_class)?;
314        U16Be::write(ctxt, table.fs_type)?;
315        I16Be::write(ctxt, table.y_subscript_x_size)?;
316        I16Be::write(ctxt, table.y_subscript_y_size)?;
317        I16Be::write(ctxt, table.y_subscript_x_offset)?;
318        I16Be::write(ctxt, table.y_subscript_y_offset)?;
319        I16Be::write(ctxt, table.y_superscript_x_size)?;
320        I16Be::write(ctxt, table.y_superscript_y_size)?;
321        I16Be::write(ctxt, table.y_superscript_x_offset)?;
322        I16Be::write(ctxt, table.y_superscript_y_offset)?;
323        I16Be::write(ctxt, table.y_strikeout_size)?;
324        I16Be::write(ctxt, table.y_strikeout_position)?;
325        I16Be::write(ctxt, table.s_family_class)?;
326        ctxt.write_bytes(&table.panose)?;
327        U32Be::write(ctxt, table.ul_unicode_range1)?;
328        U32Be::write(ctxt, table.ul_unicode_range2)?;
329        U32Be::write(ctxt, table.ul_unicode_range3)?;
330        U32Be::write(ctxt, table.ul_unicode_range4)?;
331        U32Be::write(ctxt, table.ach_vend_id)?;
332        U16Be::write(ctxt, table.fs_selection.bits())?;
333        U16Be::write(ctxt, table.us_first_char_index)?;
334        U16Be::write(ctxt, table.us_last_char_index)?;
335
336        if let Some(v0) = &table.version0 {
337            Version0::write(ctxt, v0)?;
338        }
339        if let Some(v1) = &table.version1 {
340            Version1::write(ctxt, v1)?;
341        }
342        if let Some(v2) = &table.version2to4 {
343            Version2to4::write(ctxt, v2)?;
344        }
345        if let Some(v5) = &table.version5 {
346            Version5::write(ctxt, v5)?;
347        }
348        Ok(())
349    }
350}
351
352impl WriteBinary<&Self> for Version0 {
353    type Output = ();
354
355    fn write<C: WriteContext>(ctxt: &mut C, table: &Self) -> Result<Self::Output, WriteError> {
356        I16Be::write(ctxt, table.s_typo_ascender)?;
357        I16Be::write(ctxt, table.s_typo_descender)?;
358        I16Be::write(ctxt, table.s_typo_line_gap)?;
359        U16Be::write(ctxt, table.us_win_ascent)?;
360        U16Be::write(ctxt, table.us_win_descent)?;
361        Ok(())
362    }
363}
364
365impl WriteBinary<&Self> for Version1 {
366    type Output = ();
367
368    fn write<C: WriteContext>(ctxt: &mut C, table: &Self) -> Result<Self::Output, WriteError> {
369        U32Be::write(ctxt, table.ul_code_page_range1)?;
370        U32Be::write(ctxt, table.ul_code_page_range2)?;
371        Ok(())
372    }
373}
374
375impl WriteBinary<&Self> for Version2to4 {
376    type Output = ();
377
378    fn write<C: WriteContext>(ctxt: &mut C, table: &Self) -> Result<Self::Output, WriteError> {
379        I16Be::write(ctxt, table.sx_height)?;
380        I16Be::write(ctxt, table.s_cap_height)?;
381        U16Be::write(ctxt, table.us_default_char)?;
382        U16Be::write(ctxt, table.us_break_char)?;
383        U16Be::write(ctxt, table.us_max_context)?;
384        Ok(())
385    }
386}
387
388impl WriteBinary<&Self> for Version5 {
389    type Output = ();
390
391    fn write<C: WriteContext>(ctxt: &mut C, table: &Self) -> Result<Self::Output, WriteError> {
392        U16Be::write(ctxt, table.us_lower_optical_point_size)?;
393        U16Be::write(ctxt, table.us_upper_optical_point_size)?;
394        Ok(())
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    #[cfg(feature = "prince")]
404    fn test_read() {
405        // Imports are in here to stop warnings when prince feature is not enabled
406        use crate::binary::read::ReadScope;
407        use crate::tables::{FontTableProvider, OpenTypeFont};
408        use crate::tag;
409        use crate::tests::read_fixture;
410
411        let buffer = read_fixture("../../../tests/data/fonts/HardGothicNormal.ttf");
412        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
413        let provider = opentype_file.table_provider(0).unwrap();
414        let os_2_data = provider.read_table_data(tag::OS_2).unwrap();
415
416        let os_2 = ReadScope::new(&os_2_data)
417            .read_dep::<Os2>(os_2_data.len())
418            .expect("unable to parse OS/2 table");
419        assert_eq!(os_2.version, 1);
420        assert!(os_2.version0.is_some());
421        assert!(os_2.version1.is_some());
422        assert!(os_2.version2to4.is_none());
423        assert!(os_2.version5.is_none());
424    }
425
426    #[test]
427    #[cfg(feature = "prince")]
428    fn test_write() {
429        // Imports are in here to stop warnings when prince feature is not enabled
430        use crate::binary::read::ReadScope;
431        use crate::binary::write::WriteBuffer;
432        use crate::tables::{FontTableProvider, OpenTypeFont};
433        use crate::tag;
434        use crate::tests::read_fixture;
435
436        let buffer = read_fixture("../../../tests/data/fonts/HardGothicNormal.ttf");
437        let opentype_file = ReadScope::new(&buffer).read::<OpenTypeFont<'_>>().unwrap();
438        let provider = opentype_file.table_provider(0).unwrap();
439        let os_2_data = provider.read_table_data(tag::OS_2).unwrap();
440
441        let os_2 = ReadScope::new(&os_2_data)
442            .read_dep::<Os2>(os_2_data.len())
443            .expect("unable to parse OS/2 table");
444
445        let mut out = WriteBuffer::new();
446        Os2::write(&mut out, &os_2).unwrap();
447        let written = out.into_inner();
448        assert_eq!(written.as_slice(), &*os_2_data);
449    }
450
451    #[test]
452    fn map_weight_class() {
453        assert_eq!(Os2::value_to_width_class(Fixed::from(0.)), 1);
454        assert_eq!(Os2::value_to_width_class(Fixed::from(1.)), 1);
455        assert_eq!(Os2::value_to_width_class(Fixed::from(50.)), 1);
456        assert_eq!(Os2::value_to_width_class(Fixed::from(51.)), 1);
457        assert_eq!(Os2::value_to_width_class(Fixed::from(60.)), 2);
458        assert_eq!(Os2::value_to_width_class(Fixed::from(150.)), 8);
459        assert_eq!(Os2::value_to_width_class(Fixed::from(300.)), 9);
460    }
461}