Skip to main content

ferroui_base/media/fonts/tables/
script_list_table.rs

1use std::collections::HashSet;
2
3use crate::media::fonts::OpenTypeTag;
4use crate::media::IFontMemory;
5
6use super::big_endian_binary_reader::{BigEndianBinaryReader, FontTableError};
7
8const GSUB: OpenTypeTag = OpenTypeTag::from_chars('G', 'S', 'U', 'B');
9const GPOS: OpenTypeTag = OpenTypeTag::from_chars('G', 'P', 'O', 'S');
10
11/// Reads the script tags of the `GSUB` and `GPOS` script lists.
12pub struct ScriptListTable;
13
14impl ScriptListTable {
15    /// Adds the script tags of both layout tables to `script_tags`.
16    ///
17    /// Returns `false` when a present table is malformed (the shaping
18    /// capability is then unknown); tags read before the failure stay in the set.
19    pub fn try_read_script_tags(font: &dyn IFontMemory, script_tags: &mut HashSet<OpenTypeTag>) -> bool {
20        // Non-short-circuiting '&' so both tables are always attempted.
21        Self::try_read_script_list(font, GSUB, script_tags) & Self::try_read_script_list(font, GPOS, script_tags)
22    }
23
24    fn try_read_script_list(
25        font: &dyn IFontMemory,
26        table_tag: OpenTypeTag,
27        script_tags: &mut HashSet<OpenTypeTag>,
28    ) -> bool {
29        let Some(table) = font.try_get_table(table_tag) else {
30            // An absent table simply contributes no scripts
31            return true;
32        };
33
34        // Malformed layout table — capability unknown.
35        Self::read_script_list(table.span(), script_tags).is_ok()
36    }
37
38    fn read_script_list(span: &[u8], script_tags: &mut HashSet<OpenTypeTag>) -> Result<(), FontTableError> {
39        let mut reader = BigEndianBinaryReader::new(span);
40
41        // GSUB/GPOS header: majorVersion, minorVersion, scriptListOffset, featureListOffset,
42        // lookupListOffset.
43        reader.read_uint16()?;
44        reader.read_uint16()?;
45        let script_list_offset = reader.read_offset16()?;
46
47        // ScriptList: scriptCount, then scriptCount ScriptRecords of (Tag, Offset16).
48        reader.seek(script_list_offset as i32)?;
49
50        let script_count = reader.read_uint16()?;
51
52        for _ in 0..script_count {
53            script_tags.insert(OpenTypeTag::new(reader.read_uint32()?));
54            reader.read_offset16()?; // scriptOffset — not needed
55        }
56
57        Ok(())
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use crate::media::fonts::tables::testing::{BigEndianBuffer, SyntheticFont};
65
66    fn build_layout_table(scripts: &[&str]) -> Vec<u8> {
67        let mut buffer = BigEndianBuffer::new();
68
69        buffer.uint16(1).uint16(0).uint16(10).uint16(0).uint16(0);
70        buffer.uint16(scripts.len() as i32);
71
72        for script in scripts {
73            buffer.tag(script).uint16(0);
74        }
75
76        buffer.to_array()
77    }
78
79    #[test]
80    fn absent_tables_contribute_no_scripts() {
81        let mut tags = HashSet::new();
82
83        assert!(ScriptListTable::try_read_script_tags(&SyntheticFont::new(), &mut tags));
84        assert!(tags.is_empty());
85    }
86
87    #[test]
88    fn reads_the_scripts_of_both_tables() {
89        let mut font = SyntheticFont::new();
90        font.replace("GSUB", build_layout_table(&["DFLT", "latn"]));
91        font.replace("GPOS", build_layout_table(&["latn", "cyrl"]));
92
93        let mut tags = HashSet::new();
94
95        assert!(ScriptListTable::try_read_script_tags(&font, &mut tags));
96        assert_eq!(tags.len(), 3);
97        assert!(tags.contains(&OpenTypeTag::parse("DFLT")));
98        assert!(tags.contains(&OpenTypeTag::parse("latn")));
99        assert!(tags.contains(&OpenTypeTag::parse("cyrl")));
100    }
101
102    #[test]
103    fn malformed_table_reports_unknown_but_still_reads_the_other_table() {
104        let mut font = SyntheticFont::new();
105        let gsub = build_layout_table(&["arab", "latn"]);
106
107        // GSUB is cut inside its second script record.
108        font.replace("GSUB", gsub[..gsub.len() - 3].to_vec());
109        font.replace("GPOS", build_layout_table(&["cyrl"]));
110
111        let mut tags = HashSet::new();
112
113        assert!(!ScriptListTable::try_read_script_tags(&font, &mut tags));
114        assert!(tags.contains(&OpenTypeTag::parse("arab")));
115        assert!(tags.contains(&OpenTypeTag::parse("cyrl")));
116        assert!(!tags.contains(&OpenTypeTag::parse("latn")));
117    }
118}