ferroui_base/media/fonts/tables/
feature_list_table.rs1use 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#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct FeatureListTable {
14 features: Vec<OpenTypeTag>,
15}
16
17impl FeatureListTable {
18 pub fn features(&self) -> &[OpenTypeTag] {
20 &self.features
21 }
22
23 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 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 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 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 let feature_tag = reader.read_uint32()?;
97 reader.read_offset16()?;
98
99 let tag = OpenTypeTag::new(feature_tag);
100
101 if !features.contains(&tag) {
103 features.push(tag);
104 }
105 }
106
107 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); 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 font.replace("GSUB", table[..table.len() - 1].to_vec());
185 assert!(FeatureListTable::load_gsub(&font).unwrap_err().is_invalid_operation());
186
187 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}