Skip to main content

ferroui_base/media/fonts/tables/
feature_list_table.rs

1// Derived from SixLabors.Fonts (Apache License 2.0), see NOTICE.md.
2
3use crate::media::fonts::OpenTypeTag;
4use crate::media::IFontMemory;
5
6use super::big_endian_binary_reader::{BigEndianBinaryReader, FontTableError};
7
8const GSUB_TAG: OpenTypeTag = OpenTypeTag::from_chars('G', 'S', 'U', 'B');
9const GPOS_TAG: OpenTypeTag = OpenTypeTag::from_chars('G', 'P', 'O', 'S');
10
11/// The feature tags of a `GSUB` or `GPOS` table.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct FeatureListTable {
14    features: Vec<OpenTypeTag>,
15}
16
17impl FeatureListTable {
18    /// The distinct feature tags, in table order.
19    pub fn features(&self) -> &[OpenTypeTag] {
20        &self.features
21    }
22
23    /// Loads the features of the `GSUB` table.
24    ///
25    /// `Ok(None)` when the font has no `GSUB` table; `Err` when it is malformed.
26    pub fn load_gsub(font: &dyn IFontMemory) -> Result<Option<FeatureListTable>, FontTableError> {
27        let Some(table) = font.try_get_table(GSUB_TAG) else {
28            return Ok(None);
29        };
30
31        let mut reader = BigEndianBinaryReader::new(table.span());
32
33        Ok(Some(Self::load(&mut reader)?))
34    }
35
36    /// Loads the features of the `GPOS` table.
37    ///
38    /// `Ok(None)` when the font has no `GPOS` table; `Err` when it is malformed.
39    pub fn load_gpos(font: &dyn IFontMemory) -> Result<Option<FeatureListTable>, FontTableError> {
40        let Some(table) = font.try_get_table(GPOS_TAG) else {
41            return Ok(None);
42        };
43
44        let mut reader = BigEndianBinaryReader::new(table.span());
45
46        Ok(Some(Self::load(&mut reader)?))
47    }
48
49    fn load(reader: &mut BigEndianBinaryReader<'_>) -> Result<FeatureListTable, FontTableError> {
50        // GPOS/GSUB Header, Version 1.0
51        // +----------+-------------------+-----------------------------------------------------------+
52        // | Type     | Name              | Description                                               |
53        // +==========+===================+===========================================================+
54        // | uint16   | majorVersion      | Major version of the GPOS table, = 1                      |
55        // | uint16   | minorVersion      | Minor version of the GPOS table, = 0                      |
56        // | Offset16 | scriptListOffset  | Offset to ScriptList table, from beginning of GPOS table  |
57        // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table |
58        // | Offset16 | lookupListOffset  | Offset to LookupList table, from beginning of GPOS table  |
59        // +----------+-------------------+-----------------------------------------------------------+
60
61        reader.read_uint16()?;
62        reader.read_uint16()?;
63        reader.read_offset16()?;
64
65        let feature_list_offset = reader.read_offset16()?;
66
67        Self::load_at(reader, feature_list_offset as i32)
68    }
69
70    fn load_at(reader: &mut BigEndianBinaryReader<'_>, offset: i32) -> Result<FeatureListTable, FontTableError> {
71        // FeatureList
72        // +---------------+------------------------------+------------------------------------------------------------+
73        // | Type          | Name                         | Description                                                |
74        // +===============+==============================+============================================================+
75        // | uint16        | featureCount                 | Number of FeatureRecords in this table                     |
76        // | FeatureRecord | featureRecords[featureCount] | Array of FeatureRecords — zero-based, listed alphabetically |
77        // +---------------+------------------------------+------------------------------------------------------------+
78        reader.seek(offset)?;
79
80        let feature_count = reader.read_uint16()?;
81
82        if feature_count == 0 {
83            return Ok(FeatureListTable { features: Vec::new() });
84        }
85
86        let mut features: Vec<OpenTypeTag> = Vec::with_capacity(feature_count as usize);
87
88        for _ in 0..feature_count {
89            // FeatureRecord
90            // +----------+---------------+--------------------------------------------------------+
91            // | Type     | Name          | Description                                            |
92            // +==========+===============+========================================================+
93            // | Tag      | featureTag    | 4-byte feature identification tag                      |
94            // | Offset16 | featureOffset | Offset to Feature table, from beginning of FeatureList |
95            // +----------+---------------+--------------------------------------------------------+
96            let feature_tag = reader.read_uint32()?;
97            reader.read_offset16()?;
98
99            let tag = OpenTypeTag::new(feature_tag);
100
101            // Check for duplicates in already added features
102            if !features.contains(&tag) {
103                features.push(tag);
104            }
105        }
106
107        // Keep only the unique features.
108        features.shrink_to_fit();
109
110        Ok(FeatureListTable { features })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::media::fonts::tables::testing::{BigEndianBuffer, SyntheticFont};
118
119    fn build_layout_table(scripts: &[&str], features: &[&str]) -> Vec<u8> {
120        let mut buffer = BigEndianBuffer::new();
121
122        buffer.uint16(1).uint16(0);
123        let script_list_offset = buffer.reserve_offset16();
124        let feature_list_offset = buffer.reserve_offset16();
125        buffer.uint16(0); // lookupListOffset
126
127        let position = buffer.position();
128        buffer.patch_uint16(script_list_offset, position as i32);
129        buffer.uint16(scripts.len() as i32);
130
131        for script in scripts {
132            buffer.tag(script).uint16(0);
133        }
134
135        let position = buffer.position();
136        buffer.patch_uint16(feature_list_offset, position as i32);
137        buffer.uint16(features.len() as i32);
138
139        for feature in features {
140            buffer.tag(feature).uint16(0);
141        }
142
143        buffer.to_array()
144    }
145
146    #[test]
147    fn missing_tables_give_none() {
148        let font = SyntheticFont::new();
149
150        assert_eq!(FeatureListTable::load_gsub(&font), Ok(None));
151        assert_eq!(FeatureListTable::load_gpos(&font), Ok(None));
152    }
153
154    #[test]
155    fn loads_unique_features_in_table_order() {
156        let mut font = SyntheticFont::new();
157        font.replace("GSUB", build_layout_table(&["latn"], &["calt", "liga", "calt", "ss01", "liga"]));
158        font.replace("GPOS", build_layout_table(&[], &["kern"]));
159
160        let gsub = FeatureListTable::load_gsub(&font).unwrap().unwrap();
161        let gpos = FeatureListTable::load_gpos(&font).unwrap().unwrap();
162
163        assert_eq!(
164            gsub.features(),
165            [OpenTypeTag::parse("calt"), OpenTypeTag::parse("liga"), OpenTypeTag::parse("ss01")]
166        );
167        assert_eq!(gpos.features(), [OpenTypeTag::parse("kern")]);
168    }
169
170    #[test]
171    fn empty_feature_list_gives_no_features() {
172        let mut font = SyntheticFont::new();
173        font.replace("GSUB", build_layout_table(&[], &[]));
174
175        assert!(FeatureListTable::load_gsub(&font).unwrap().unwrap().features().is_empty());
176    }
177
178    #[test]
179    fn malformed_tables_are_errors() {
180        let mut font = SyntheticFont::new();
181        let table = build_layout_table(&[], &["calt", "liga"]);
182
183        // The second feature record is cut short.
184        font.replace("GSUB", table[..table.len() - 1].to_vec());
185        assert!(FeatureListTable::load_gsub(&font).unwrap_err().is_invalid_operation());
186
187        // The feature list offset points past the table.
188        let mut table = table;
189        table[6..8].copy_from_slice(&0xFFFFu16.to_be_bytes());
190        font.replace("GPOS", table);
191        assert!(FeatureListTable::load_gpos(&font).unwrap_err().is_argument_out_of_range());
192    }
193}