Skip to content

Commit cba82d5

Browse files
committed
Add support for InstanceRecord in fvar table
1 parent 6e75b3c commit cba82d5

1 file changed

Lines changed: 194 additions & 1 deletion

File tree

src/tables/fvar.rs

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,180 @@ impl VariationAxis {
6262
}
6363
}
6464

65+
/// An [instance record](https://docs.microsoft.com/en-us/typography/opentype/spec/fvar#instancerecord).
66+
#[derive(Clone, Copy, Debug)]
67+
pub struct Instance<'a> {
68+
/// The name ID for entries in the 'name' table that provide subfamily names for this instance.
69+
pub subfamily_name_id: u16,
70+
/// Reserved for future use — set to 0.
71+
pub flags: u16,
72+
/// The coordinate array for this instance (length = axisCount).
73+
pub coordinates: LazyArray16<'a, Fixed>,
74+
/// The name ID for entries in the 'name' table that provide PostScript names for this instance.
75+
pub post_script_name_id: Option<u16>,
76+
}
77+
78+
impl<'a> Instance<'a> {
79+
/// Returns an iterator over the coordinate values as `f32`.
80+
///
81+
/// This is a convenience method that converts the internal `Fixed` representation
82+
/// to floating-point values.
83+
#[inline]
84+
pub fn coordinates_f32(&self) -> impl Iterator<Item = f32> + 'a {
85+
self.coordinates.into_iter().map(|fixed| fixed.0)
86+
}
87+
88+
#[inline]
89+
fn parse_from_record(
90+
record: &'a [u8],
91+
axis_count: u16,
92+
has_post_script_name_id: bool,
93+
) -> Option<Self> {
94+
let mut s = Stream::new(record);
95+
let subfamily_name_id = s.read::<u16>()?;
96+
let flags = s.read::<u16>()?;
97+
let coordinates = s.read_array16::<Fixed>(axis_count)?;
98+
let post_script_name_id = if has_post_script_name_id {
99+
Some(s.read::<u16>()?)
100+
} else {
101+
None
102+
};
103+
Some(Self {
104+
subfamily_name_id,
105+
flags,
106+
coordinates,
107+
post_script_name_id,
108+
})
109+
}
110+
}
111+
112+
/// A view over the `InstanceRecord` array.
113+
#[derive(Clone, Copy, Debug)]
114+
pub struct Instances<'a> {
115+
data: &'a [u8],
116+
record_len: u16,
117+
axis_count: u16,
118+
count: u16,
119+
}
120+
121+
/// An iterator over [instance records](Instance).
122+
#[derive(Clone, Copy, Debug)]
123+
pub struct InstancesIter<'a> {
124+
instances: Instances<'a>,
125+
index: u16,
126+
}
127+
128+
impl<'a> Iterator for InstancesIter<'a> {
129+
type Item = Instance<'a>;
130+
131+
fn next(&mut self) -> Option<Self::Item> {
132+
if self.index < self.instances.count {
133+
let instance = self.instances.get(self.index)?;
134+
self.index += 1;
135+
Some(instance)
136+
} else {
137+
None
138+
}
139+
}
140+
141+
fn size_hint(&self) -> (usize, Option<usize>) {
142+
let remaining = (self.instances.count - self.index) as usize;
143+
(remaining, Some(remaining))
144+
}
145+
}
146+
147+
impl<'a> ExactSizeIterator for InstancesIter<'a> {
148+
fn len(&self) -> usize {
149+
(self.instances.count - self.index) as usize
150+
}
151+
}
152+
153+
impl<'a> IntoIterator for Instances<'a> {
154+
type Item = Instance<'a>;
155+
type IntoIter = InstancesIter<'a>;
156+
157+
fn into_iter(self) -> Self::IntoIter {
158+
InstancesIter {
159+
instances: self,
160+
index: 0,
161+
}
162+
}
163+
}
164+
165+
impl<'a> Instances<'a> {
166+
/// Returns an iterator over the instance records.
167+
///
168+
/// # Examples
169+
///
170+
/// ```
171+
/// # use ttf_parser::fvar::Table;
172+
/// # let data = &[/* font data */];
173+
/// # if let Some(table) = Table::parse(data) {
174+
/// for instance in table.instances.iter() {
175+
/// println!("Subfamily name ID: {}", instance.subfamily_name_id);
176+
/// }
177+
/// # }
178+
/// ```
179+
#[inline]
180+
pub fn iter(&self) -> InstancesIter<'a> {
181+
(*self).into_iter()
182+
}
183+
184+
#[inline]
185+
fn new(data: &'a [u8], record_len: u16, axis_count: u16, count: u16) -> Self {
186+
Self {
187+
data,
188+
record_len,
189+
axis_count,
190+
count,
191+
}
192+
}
193+
194+
/// Total number of instance records.
195+
#[inline]
196+
pub fn len(&self) -> u16 {
197+
self.count
198+
}
199+
200+
/// Returns true when there are no instance records.
201+
#[inline]
202+
pub fn is_empty(&self) -> bool {
203+
self.count == 0
204+
}
205+
206+
/// Returns `true` when the `postScriptNameID` field is present in records.
207+
#[inline]
208+
pub fn has_post_script_name_id(&self) -> bool {
209+
// The base size is 4 bytes (subfamilyNameID + flags) + 4 bytes per axis coordinate.
210+
// If record_len is at least base + 2, the optional postScriptNameID field is present.
211+
let axis_count = usize::from(self.axis_count);
212+
let base = 4 + 4 * axis_count;
213+
usize::from(self.record_len) >= base + 2
214+
}
215+
216+
/// Returns the instance at the given index.
217+
///
218+
/// Returns `None` if the index is out of bounds.
219+
pub fn get(&self, index: u16) -> Option<Instance<'a>> {
220+
if index >= self.count {
221+
return None;
222+
}
223+
let len = usize::from(self.record_len);
224+
let start = usize::from(index) * len;
225+
let end = start + len;
226+
let record = self.data.get(start..end)?;
227+
Instance::parse_from_record(record, self.axis_count, self.has_post_script_name_id())
228+
}
229+
}
230+
65231
/// A [Font Variations Table](
66232
/// https://docs.microsoft.com/en-us/typography/opentype/spec/fvar).
67233
#[derive(Clone, Copy, Debug)]
68234
pub struct Table<'a> {
69235
/// A list of variation axes.
70236
pub axes: LazyArray16<'a, VariationAxis>,
237+
/// A list of instance records.
238+
pub instances: Instances<'a>,
71239
}
72240

73241
impl<'a> Table<'a> {
@@ -82,6 +250,13 @@ impl<'a> Table<'a> {
82250
let axes_array_offset = s.read::<Offset16>()?;
83251
s.skip::<u16>(); // reserved
84252
let axis_count = s.read::<u16>()?;
253+
let axis_size = s.read::<u16>()?;
254+
let instance_count = s.read::<u16>()?;
255+
let instance_size = s.read::<u16>()?;
256+
257+
if axis_size as usize != VariationAxis::SIZE {
258+
return None;
259+
}
85260

86261
// 'If axisCount is zero, then the font is not functional as a variable font,
87262
// and must be treated as a non-variable font;
@@ -91,6 +266,24 @@ impl<'a> Table<'a> {
91266
let mut s = Stream::new_at(data, axes_array_offset.to_usize())?;
92267
let axes = s.read_array16::<VariationAxis>(axis_count.get())?;
93268

94-
Some(Table { axes })
269+
// Instance records follow the axes array immediately.
270+
let instances_offset = axes_array_offset
271+
.to_usize()
272+
.checked_add(usize::from(axis_count.get()) * VariationAxis::SIZE)?;
273+
274+
// Validate instance record size: must be base or base + 2 (for psNameID).
275+
let base = 4usize.checked_add(4usize.checked_mul(usize::from(axis_count.get()))?)?;
276+
let inst_size = usize::from(instance_size);
277+
if inst_size < base {
278+
return None;
279+
}
280+
281+
let total_instances_len =
282+
usize::from(instance_count).checked_mul(usize::from(instance_size))?;
283+
let mut inst_stream = Stream::new_at(data, instances_offset)?;
284+
let inst_data = inst_stream.read_bytes(total_instances_len)?;
285+
let instances = Instances::new(inst_data, instance_size, axis_count.get(), instance_count);
286+
287+
Some(Table { axes, instances })
95288
}
96289
}

0 commit comments

Comments
 (0)