Repository: googlefonts/fontations
Pull request #1469: [read-fonts] parse trak table
Author: login: dfrg
Created: 2025-04-22T15:10:37Z | Merged: 2025-04-22T16:18:25Z

Just provides basic parsing of the table data. This required a few hand-written bits because it has nested tables that contain offsets from the root table. Rather than taking a raw `offset_data: FontData` parameter, these methods take a `&Trak` reference. This is a slight deviation from the rest of the `read-fonts` API but offers better type safety? Open to changing this for consistency.

Includes a few simple parsing tests. Also confirmed to pass the AAT trak tests in HarfRuzz.


Diff:
diff --git a/.typos.toml b/.typos.toml
index 660da77cb..ede72a592 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -40,6 +40,8 @@ wdth = "wdth"
 ot = "ot"
 vai = "vai"
 beng = "beng"
+trak = "trak"
+Trak = "Trak"
 
 # Possible typos that are more specific to this codebase.
 sftn = "sfnt"
diff --git a/read-fonts/generated/generated_trak.rs b/read-fonts/generated/generated_trak.rs
new file mode 100644
index 000000000..6c2d58a60
--- /dev/null
+++ b/read-fonts/generated/generated_trak.rs
@@ -0,0 +1,295 @@
+// THIS FILE IS AUTOGENERATED.
+// Any changes to this file will be overwritten.
+// For more information about how codegen works, see font-codegen/README.md
+
+#[allow(unused_imports)]
+use crate::codegen_prelude::*;
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[derive(Debug, Clone, Copy)]
+#[doc(hidden)]
+pub struct TrakMarker {}
+
+impl TrakMarker {
+    pub fn version_byte_range(&self) -> Range<usize> {
+        let start = 0;
+        start..start + MajorMinor::RAW_BYTE_LEN
+    }
+
+    pub fn format_byte_range(&self) -> Range<usize> {
+        let start = self.version_byte_range().end;
+        start..start + u16::RAW_BYTE_LEN
+    }
+
+    pub fn horiz_offset_byte_range(&self) -> Range<usize> {
+        let start = self.format_byte_range().end;
+        start..start + Offset16::RAW_BYTE_LEN
+    }
+
+    pub fn vert_offset_byte_range(&self) -> Range<usize> {
+        let start = self.horiz_offset_byte_range().end;
+        start..start + Offset16::RAW_BYTE_LEN
+    }
+
+    pub fn reserved_byte_range(&self) -> Range<usize> {
+        let start = self.vert_offset_byte_range().end;
+        start..start + u16::RAW_BYTE_LEN
+    }
+}
+
+impl MinByteRange for TrakMarker {
+    fn min_byte_range(&self) -> Range<usize> {
+        0..self.reserved_byte_range().end
+    }
+}
+
+impl TopLevelTable for Trak<'_> {
+    /// `trak`
+    const TAG: Tag = Tag::new(b"trak");
+}
+
+impl<'a> FontRead<'a> for Trak<'a> {
+    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
+        let mut cursor = data.cursor();
+        cursor.advance::<MajorMinor>();
+        cursor.advance::<u16>();
+        cursor.advance::<Offset16>();
+        cursor.advance::<Offset16>();
+        cursor.advance::<u16>();
+        cursor.finish(TrakMarker {})
+    }
+}
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+pub type Trak<'a> = TableRef<'a, TrakMarker>;
+
+#[allow(clippy::needless_lifetimes)]
+impl<'a> Trak<'a> {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    pub fn version(&self) -> MajorMinor {
+        let range = self.shape.version_byte_range();
+        self.data.read_at(range.start).unwrap()
+    }
+
+    /// Format of the tracking table (set to 0).
+    pub fn format(&self) -> u16 {
+        let range = self.shape.format_byte_range();
+        self.data.read_at(range.start).unwrap()
+    }
+
+    /// Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
+    pub fn horiz_offset(&self) -> Nullable<Offset16> {
+        let range = self.shape.horiz_offset_byte_range();
+        self.data.read_at(range.start).unwrap()
+    }
+
+    /// Attempt to resolve [`horiz_offset`][Self::horiz_offset].
+    pub fn horiz(&self) -> Option<Result<TrackData<'a>, ReadError>> {
+        let data = self.data;
+        self.horiz_offset().resolve(data)
+    }
+
+    /// Offset from start of tracking table to TrackData for vertical text (or 0 if none).
+    pub fn vert_offset(&self) -> Nullable<Offset16> {
+        let range = self.shape.vert_offset_byte_range();
+        self.data.read_at(range.start).unwrap()
+    }
+
+    /// Attempt to resolve [`vert_offset`][Self::vert_offset].
+    pub fn vert(&self) -> Option<Result<TrackData<'a>, ReadError>> {
+        let data = self.data;
+        self.vert_offset().resolve(data)
+    }
+}
+
+#[cfg(feature = "experimental_traverse")]
+impl<'a> SomeTable<'a> for Trak<'a> {
+    fn type_name(&self) -> &str {
+        "Trak"
+    }
+    fn get_field(&self, idx: usize) -> Option<Field<'a>> {
+        match idx {
+            0usize => Some(Field::new("version", self.version())),
+            1usize => Some(Field::new("format", self.format())),
+            2usize => Some(Field::new(
+                "horiz_offset",
+                FieldType::offset(self.horiz_offset(), self.horiz()),
+            )),
+            3usize => Some(Field::new(
+                "vert_offset",
+                FieldType::offset(self.vert_offset(), self.vert()),
+            )),
+            _ => None,
+        }
+    }
+}
+
+#[cfg(feature = "experimental_traverse")]
+#[allow(clippy::needless_lifetimes)]
+impl<'a> std::fmt::Debug for Trak<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        (self as &dyn SomeTable<'a>).fmt(f)
+    }
+}
+
+/// The tracking data table.
+#[derive(Debug, Clone, Copy)]
+#[doc(hidden)]
+pub struct TrackDataMarker {
+    track_table_byte_len: usize,
+}
+
+impl TrackDataMarker {
+    pub fn n_tracks_byte_range(&self) -> Range<usize> {
+        let start = 0;
+        start..start + u16::RAW_BYTE_LEN
+    }
+
+    pub fn n_sizes_byte_range(&self) -> Range<usize> {
+        let start = self.n_tracks_byte_range().end;
+        start..start + u16::RAW_BYTE_LEN
+    }
+
+    pub fn size_table_offset_byte_range(&self) -> Range<usize> {
+        let start = self.n_sizes_byte_range().end;
+        start..start + u32::RAW_BYTE_LEN
+    }
+
+    pub fn track_table_byte_range(&self) -> Range<usize> {
+        let start = self.size_table_offset_byte_range().end;
+        start..start + self.track_table_byte_len
+    }
+}
+
+impl MinByteRange for TrackDataMarker {
+    fn min_byte_range(&self) -> Range<usize> {
+        0..self.track_table_byte_range().end
+    }
+}
+
+impl<'a> FontRead<'a> for TrackData<'a> {
+    fn read(data: FontData<'a>) -> Result<Self, ReadError> {
+        let mut cursor = data.cursor();
+        let n_tracks: u16 = cursor.read()?;
+        cursor.advance::<u16>();
+        cursor.advance::<u32>();
+        let track_table_byte_len = (n_tracks as usize)
+            .checked_mul(TrackTableEntry::RAW_BYTE_LEN)
+            .ok_or(ReadError::OutOfBounds)?;
+        cursor.advance_by(track_table_byte_len);
+        cursor.finish(TrackDataMarker {
+            track_table_byte_len,
+        })
+    }
+}
+
+/// The tracking data table.
+pub type TrackData<'a> = TableRef<'a, TrackDataMarker>;
+
+#[allow(clippy::needless_lifetimes)]
+impl<'a> TrackData<'a> {
+    /// Number of separate tracks included in this table.
+    pub fn n_tracks(&self) -> u16 {
+        let range = self.shape.n_tracks_byte_range();
+        self.data.read_at(range.start).unwrap()
+    }
+
+    /// Number of point sizes included in this table.
+    pub fn n_sizes(&self) -> u16 {
+        let range = self.shape.n_sizes_byte_range();
+        self.data.read_at(range.start).unwrap()
+    }
+
+    /// Offset from the start of the tracking table to the start of the size subtable.
+    pub fn size_table_offset(&self) -> u32 {
+        let range = self.shape.size_table_offset_byte_range();
+        self.data.read_at(range.start).unwrap()
+    }
+
+    /// Array of TrackTableEntry records.
+    pub fn track_table(&self) -> &'a [TrackTableEntry] {
+        let range = self.shape.track_table_byte_range();
+        self.data.read_array(range).unwrap()
+    }
+}
+
+#[cfg(feature = "experimental_traverse")]
+impl<'a> SomeTable<'a> for TrackData<'a> {
+    fn type_name(&self) -> &str {
+        "TrackData"
+    }
+    fn get_field(&self, idx: usize) -> Option<Field<'a>> {
+        match idx {
+            0usize => Some(Field::new("n_tracks", self.n_tracks())),
+            1usize => Some(Field::new("n_sizes", self.n_sizes())),
+            2usize => Some(Field::new("size_table_offset", self.size_table_offset())),
+            3usize => Some(Field::new(
+                "track_table",
+                traversal::FieldType::array_of_records(
+                    stringify!(TrackTableEntry),
+                    self.track_table(),
+                    self.offset_data(),
+                ),
+            )),
+            _ => None,
+        }
+    }
+}
+
+#[cfg(feature = "experimental_traverse")]
+#[allow(clippy::needless_lifetimes)]
+impl<'a> std::fmt::Debug for TrackData<'a> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        (self as &dyn SomeTable<'a>).fmt(f)
+    }
+}
+
+/// Single entry in a tracking table.
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, bytemuck :: AnyBitPattern)]
+#[repr(C)]
+#[repr(packed)]
+pub struct TrackTableEntry {
+    /// Track value for this record.
+    pub track: BigEndian<Fixed>,
+    /// The 'name' table index for this track (a short word or phrase like \"loose\" or \"very tight\"). NameIndex has a value greater than 255 and less than 32768.
+    pub name_index: BigEndian<NameId>,
+    /// Offset from the start of the tracking table to per-size tracking values for this track.
+    pub offset: BigEndian<u16>,
+}
+
+impl TrackTableEntry {
+    /// Track value for this record.
+    pub fn track(&self) -> Fixed {
+        self.track.get()
+    }
+
+    /// The 'name' table index for this track (a short word or phrase like \"loose\" or \"very tight\"). NameIndex has a value greater than 255 and less than 32768.
+    pub fn name_index(&self) -> NameId {
+        self.name_index.get()
+    }
+
+    /// Offset from the start of the tracking table to per-size tracking values for this track.
+    pub fn offset(&self) -> u16 {
+        self.offset.get()
+    }
+}
+
+impl FixedSize for TrackTableEntry {
+    const RAW_BYTE_LEN: usize = Fixed::RAW_BYTE_LEN + NameId::RAW_BYTE_LEN + u16::RAW_BYTE_LEN;
+}
+
+#[cfg(feature = "experimental_traverse")]
+impl<'a> SomeRecord<'a> for TrackTableEntry {
+    fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> {
+        RecordResolver {
+            name: "TrackTableEntry",
+            get_field: Box::new(move |idx, _data| match idx {
+                0usize => Some(Field::new("track", self.track())),
+                1usize => Some(Field::new("name_index", self.name_index())),
+                2usize => Some(Field::new("offset", self.offset())),
+                _ => None,
+            }),
+            data,
+        }
+    }
+}
diff --git a/read-fonts/src/table_provider.rs b/read-fonts/src/table_provider.rs
index 1b45fd08e..c355f2d35 100644
--- a/read-fonts/src/table_provider.rs
+++ b/read-fonts/src/table_provider.rs
@@ -167,6 +167,10 @@ pub trait TableProvider<'a> {
         self.expect_table()
     }
 
+    fn trak(&self) -> Result<tables::trak::Trak<'a>, ReadError> {
+        self.expect_table()
+    }
+
     fn colr(&self) -> Result<tables::colr::Colr<'a>, ReadError> {
         self.expect_table()
     }
diff --git a/read-fonts/src/tables.rs b/read-fonts/src/tables.rs
index 45ef20bc8..d950bef24 100644
--- a/read-fonts/src/tables.rs
+++ b/read-fonts/src/tables.rs
@@ -41,6 +41,7 @@ pub mod postscript;
 pub mod sbix;
 pub mod stat;
 pub mod svg;
+pub mod trak;
 pub mod varc;
 pub mod variations;
 pub mod vhea;
diff --git a/read-fonts/src/tables/trak.rs b/read-fonts/src/tables/trak.rs
new file mode 100644
index 000000000..09499b821
--- /dev/null
+++ b/read-fonts/src/tables/trak.rs
@@ -0,0 +1,170 @@
+//! The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+
+include!("../../generated/generated_trak.rs");
+
+impl<'a> TrackData<'a> {
+    /// Returns the size table for this set of tracking data.
+    ///
+    /// The `offset_data` parameter comes from the [`Trak`] table.
+    pub fn size_table(
+        &self,
+        offset_data: FontData<'a>,
+    ) -> Result<&'a [BigEndian<Fixed>], ReadError> {
+        let mut cursor = offset_data
+            .split_off(self.size_table_offset() as usize)
+            .ok_or(ReadError::OutOfBounds)?
+            .cursor();
+        cursor.read_array(self.n_sizes() as usize)
+    }
+}
+
+impl TrackTableEntry {
+    /// Returns the list of per-size tracking values for this entry.
+    ///
+    /// The `offset_data` parameter comes from the [`Trak`] table and `n_sizes`
+    /// parameter comes from the parent [`TrackData`] table.
+    pub fn per_size_values<'a>(
+        &self,
+        offset_data: FontData<'a>,
+        n_sizes: u16,
+    ) -> Result<&'a [BigEndian<i16>], ReadError> {
+        let mut cursor = offset_data
+            .split_off(self.offset() as usize)
+            .ok_or(ReadError::OutOfBounds)?
+            .cursor();
+        cursor.read_array(n_sizes as usize)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use font_test_data::bebuffer::BeBuffer;
+
+    #[test]
+    fn parse_header() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        assert_eq!(trak.version(), MajorMinor::VERSION_1_0);
+        let _ = trak.horiz().unwrap().unwrap();
+        assert!(trak.vert().is_none());
+    }
+
+    #[test]
+    fn parse_tracks() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let track_table = horiz.track_table();
+        let expected_tracks = [
+            (Fixed::from_i32(-1), NameId::new(256), 52),
+            (Fixed::from_i32(0), NameId::new(258), 60),
+            (Fixed::from_i32(1), NameId::new(257), 56),
+        ];
+        let tracks = track_table
+            .iter()
+            .map(|track| (track.track(), track.name_index(), track.offset()))
+            .collect::<Vec<_>>();
+        assert_eq!(tracks, expected_tracks);
+        let expected_per_size_tracking_values = [[-15i16, -7], [0, 0], [50, 20]];
+        for (track, expected_values) in track_table.iter().zip(expected_per_size_tracking_values) {
+            let values = track
+                .per_size_values(trak.offset_data(), horiz.n_sizes())
+                .unwrap()
+                .iter()
+                .map(|v| v.get())
+                .collect::<Vec<_>>();
+            assert_eq!(values, expected_values);
+        }
+    }
+
+    #[test]
+    fn parse_per_size_values() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let track_table = horiz.track_table();
+        let expected_per_size_tracking_values = [[-15i16, -7], [0, 0], [50, 20]];
+        for (track, expected_values) in track_table.iter().zip(expected_per_size_tracking_values) {
+            let values = track
+                .per_size_values(trak.offset_data(), horiz.n_sizes())
+                .unwrap()
+                .iter()
+                .map(|v| v.get())
+                .collect::<Vec<_>>();
+            assert_eq!(values, expected_values);
+        }
+    }
+
+    #[test]
+    fn parse_sizes() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let size_table = horiz
+            .size_table(trak.offset_data())
+            .unwrap()
+            .iter()
+            .map(|v| v.get())
+            .collect::<Vec<_>>();
+        let expected_sizes = [Fixed::from_i32(12), Fixed::from_i32(24)];
+        assert_eq!(size_table, expected_sizes);
+    }
+
+    #[test]
+    fn insufficient_data() {
+        let mut table_data = example_track_table();
+        // drop the last byte from the final per size value entry
+        table_data.pop();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let track_table = horiz.track_table();
+        // The values for the second track will fail to parse
+        let expected_per_size_tracking_values = [Some([-15i16, -7]), None, Some([50, 20])];
+        for (track, expected_values) in track_table.iter().zip(expected_per_size_tracking_values) {
+            let values = track
+                .per_size_values(trak.offset_data(), horiz.n_sizes())
+                .ok()
+                .map(|values| values.iter().map(|v| v.get()).collect::<Vec<_>>());
+            assert_eq!(
+                values,
+                expected_values.map(|value| value.into_iter().collect())
+            );
+        }
+    }
+
+    #[test]
+    fn bad_offset() {
+        let mut table_data = example_track_table();
+        // modify offset of first track table entry to be OOB
+        table_data[26] = 255;
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let track_table = horiz.track_table();
+        assert!(matches!(
+            track_table[0].per_size_values(trak.offset_data(), horiz.n_sizes()),
+            Err(ReadError::OutOfBounds)
+        ));
+    }
+
+    /// From <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html>
+    fn example_track_table() -> Vec<u8> {
+        let mut buf = BeBuffer::new();
+        // header
+        buf = buf.push(MajorMinor::VERSION_1_0);
+        buf = buf.extend([0u16, 12, 0, 0]);
+        // TrackData
+        buf = buf.extend([3u16, 2]);
+        buf = buf.push(44u32);
+        // Three sorted TrackTableEntry records
+        buf = buf.push(0xFFFF0000u32).extend([256u16, 52]);
+        buf = buf.push(0x00000000u32).extend([258u16, 60]);
+        buf = buf.push(0x00010000u32).extend([257u16, 56]);
+        // Size subtable
+        buf = buf.push(0x000C0000u32);
+        buf = buf.push(0x00180000u32);
+        // Per-size tracking data values
+        buf = buf.extend([-15i16, -7, 50, 20, 0, 0]);
+        buf.to_vec()
+    }
+}
diff --git a/resources/codegen_inputs/trak.rs b/resources/codegen_inputs/trak.rs
new file mode 100644
index 000000000..9a0c667c5
--- /dev/null
+++ b/resources/codegen_inputs/trak.rs
@@ -0,0 +1,45 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    #[compile(MajorMinor::VERSION_1_0)]
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    #[compile(0)]
+    format: u16,
+    /// Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
+    #[nullable]
+    horiz_offset: Offset16<TrackData>,
+    /// Offset from start of tracking table to TrackData for vertical text (or 0 if none).
+    #[nullable]
+    vert_offset: Offset16<TrackData>,
+    /// Reserved. Set to 0.
+    #[skip_getter]
+    #[compile(0)]
+    reserved: u16,
+}
+
+/// The tracking data table.
+table TrackData {
+    /// Number of separate tracks included in this table.
+    n_tracks: u16,
+    /// Number of point sizes included in this table.
+    n_sizes: u16,
+    /// Offset from the start of the tracking table to the start of the size subtable.
+    size_table_offset: u32,
+    /// Array of TrackTableEntry records.
+    #[count($n_tracks)]
+    track_table: [TrackTableEntry],
+}
+
+/// Single entry in a tracking table.
+record TrackTableEntry {
+    /// Track value for this record.
+    track: Fixed,
+    /// The 'name' table index for this track (a short word or phrase like "loose" or "very tight"). NameIndex has a value greater than 255 and less than 32768.
+    name_index: NameId,
+    /// Offset from the start of the tracking table to per-size tracking values for this track.
+    offset: u16,
+}
diff --git a/resources/codegen_plan.toml b/resources/codegen_plan.toml
index 62b244b05..a65d1cd74 100644
--- a/resources/codegen_plan.toml
+++ b/resources/codegen_plan.toml
@@ -413,6 +413,11 @@ mode = "parse"
 source = "resources/codegen_inputs/ltag.rs"
 target = "read-fonts/generated/generated_ltag.rs"
 
+[[generate]]
+mode = "parse"
+source = "resources/codegen_inputs/trak.rs"
+target = "read-fonts/generated/generated_trak.rs"
+
 [[generate]]
 mode = "parse"
 source = "resources/codegen_inputs/ift.rs"


Reviews:
user: login: rsheeter
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:31:02Z
user: login: rsheeter
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:33:27Z
user: login: rsheeter
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-22T15:36:21Z
user: login: dfrg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T15:36:44Z
user: login: cmyr
body: looks good, I'm nerdsniped a bit by this `pass in the table directly` idea.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-22T15:55:29Z
user: login: dfrg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T16:06:38Z
user: login: dfrg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T16:10:56Z
user: login: rsheeter
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-22T17:27:43Z

Review comments:
diff_hunk: @@ -0,0 +1,43 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    format: u16,
path: resources/codegen_inputs/trak.rs
user: login: cmyr
body: might as well also add `#[compile(0)]` here? (and `#[compile(MajorMinor::VERSION_1_0)]` above?)
created_at: 2025-04-22T15:22:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,130 @@
+//! The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+
+include!("../../generated/generated_trak.rs");
+
+impl<'a> TrackData<'a> {
+    /// Returns the size table for this set of tracking data.
+    pub fn size_table(&self, trak: &Trak<'a>) -> Result<&'a [BigEndian<Fixed>], ReadError> {
+        let mut cursor = trak
+            .offset_data()
+            .split_off(self.size_table_offset() as usize)
+            .ok_or(ReadError::OutOfBounds)?
+            .cursor();
+        cursor.read_array(self.n_sizes() as usize)
+    }
+}
+
+impl TrackTableEntry {
+    /// Returns the list of per-size tracking values for this entry.
+    ///
+    /// The `n_sizes` parameter comes from the parent [`TrackData`] table.
+    pub fn per_size_values<'a>(
+        &self,
+        trak: &Trak<'a>,
+        n_sizes: u16,
+    ) -> Result<&'a [BigEndian<i16>], ReadError> {
+        let mut cursor = trak
+            .offset_data()
+            .split_off(self.offset() as usize)
+            .ok_or(ReadError::OutOfBounds)?
+            .cursor();
+        cursor.read_array(n_sizes as usize)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use font_test_data::bebuffer::BeBuffer;
+
+    #[test]
+    fn parse_header() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        assert_eq!(trak.version(), MajorMinor::VERSION_1_0);
+        let _ = trak.horiz().unwrap().unwrap();
+        assert!(trak.vert().is_none());
+    }
+
+    #[test]
+    fn parse_tracks() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let track_table = horiz.track_table();
+        let expected_tracks = [
+            (Fixed::from_i32(-1), NameId::new(256), 52),
+            (Fixed::from_i32(0), NameId::new(258), 60),
+            (Fixed::from_i32(1), NameId::new(257), 56),
+        ];
+        let tracks = track_table
+            .iter()
+            .map(|track| (track.track(), track.name_index(), track.offset()))
+            .collect::<Vec<_>>();
+        assert_eq!(tracks, expected_tracks);
+        let expected_per_size_tracking_values = [[-15i16, -7], [0, 0], [50, 20]];
+        for (track, expected_values) in track_table.iter().zip(expected_per_size_tracking_values) {
+            let values = track
+                .per_size_values(&trak, horiz.n_sizes())
+                .unwrap()
+                .iter()
+                .map(|v| v.get())
+                .collect::<Vec<_>>();
+            assert_eq!(values, expected_values);
+        }
+    }
+
+    #[test]
+    fn parse_per_size_values() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let track_table = horiz.track_table();
+        let expected_per_size_tracking_values = [[-15i16, -7], [0, 0], [50, 20]];
+        for (track, expected_values) in track_table.iter().zip(expected_per_size_tracking_values) {
+            let values = track
+                .per_size_values(&trak, horiz.n_sizes())
+                .unwrap()
+                .iter()
+                .map(|v| v.get())
+                .collect::<Vec<_>>();
+            assert_eq!(values, expected_values);
+        }
+    }
+
+    #[test]
+    fn parse_sizes() {
+        let table_data = example_track_table();
+        let trak = Trak::read(FontData::new(&table_data)).unwrap();
+        let horiz = trak.horiz().unwrap().unwrap();
+        let size_table = horiz
+            .size_table(&trak)
+            .unwrap()
+            .iter()
+            .map(|v| v.get())
+            .collect::<Vec<_>>();
+        let expected_sizes = [Fixed::from_i32(12), Fixed::from_i32(24)];
+        assert_eq!(size_table, expected_sizes);
+    }
+
+    /// From <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html>
+    fn example_track_table() -> Vec<u8> {
+        let mut buf = BeBuffer::new();
+        // header
+        buf = buf.push(MajorMinor::VERSION_1_0);
+        buf = buf.extend([0u16, 12, 0, 0]);
+        // TrackData
+        buf = buf.extend([3u16, 2]);
+        buf = buf.push(44u32);
+        // Three sorted TrackTableEntry records
+        buf = buf.push(0xFFFF0000u32).extend([256u16, 52]);
+        buf = buf.push(0x00000000u32).extend([258u16, 60]);
+        buf = buf.push(0x00010000u32).extend([257u16, 56]);
+        // Size subtable
+        buf = buf.push(0x000C0000u32);
+        buf = buf.push(0x00180000u32);
+        // Per-size tracking data values
+        buf = buf.extend([-15i16, -7, 50, 20, 0, 0]);
+        buf.to_vec()
+    }
+}
path: read-fonts/src/tables/trak.rs
user: login: rsheeter
body: Should demonstrate it's ok in the face of malformed and insufficient data
created_at: 2025-04-22T15:31:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    format: u16,
+    /// Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
+    #[nullable]
+    horiz_offset: Offset16<TrackData>,
+    /// Offset from start of tracking table to TrackData for vertical text (or 0 if none).
+    #[nullable]
+    vert_offset: Offset16<TrackData>,
+    /// Reserved. Set to 0.
+    #[skip_getter]
+    #[compile(0)]
+    reserved: u16,
+}
+
+/// The tracking data table.
+table TrackData {
+    /// Number of separate tracks included in this table.
+    n_tracks: u16,
+    /// Number of point sizes included in this table.
+    n_sizes: u16,
+    /// Offset from the start of the tracking table to the start of the size subtable.
+    size_table_offset: u32,
+    /// Array of TrackTableEntry records.
+    #[count($n_tracks)]
+    track_table: [TrackTableEntry],
+}
path: resources/codegen_inputs/trak.rs
user: login: rsheeter
body: Do we not support sizeTable[nSizes] as a subsequent array? Maybe we should? (could be deferred to an issue referring to this)
created_at: 2025-04-22T15:33:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,43 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    format: u16,
+    /// Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
+    #[nullable]
+    horiz_offset: Offset16<TrackData>,
+    /// Offset from start of tracking table to TrackData for vertical text (or 0 if none).
+    #[nullable]
+    vert_offset: Offset16<TrackData>,
+    /// Reserved. Set to 0.
+    #[skip_getter]
+    #[compile(0)]
+    reserved: u16,
+}
+
+/// The tracking data table.
+table TrackData {
+    /// Number of separate tracks included in this table.
+    n_tracks: u16,
+    /// Number of point sizes included in this table.
+    n_sizes: u16,
+    /// Offset from the start of the tracking table to the start of the size subtable.
+    size_table_offset: u32,
+    /// Array of TrackTableEntry records.
+    #[count($n_tracks)]
+    track_table: [TrackTableEntry],
+}
path: resources/codegen_inputs/trak.rs
user: login: dfrg
body: We do support that but this table contains an explicit offset to the size table and we've been bitten by this before when assuming we can ignore it. In particular, Apple seems to like tables that are aligned to 4 bytes so it's conceivable that the size table might not directly follow the tracks.
created_at: 2025-04-22T15:36:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    format: u16,
+    /// Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
+    #[nullable]
+    horiz_offset: Offset16<TrackData>,
+    /// Offset from start of tracking table to TrackData for vertical text (or 0 if none).
+    #[nullable]
+    vert_offset: Offset16<TrackData>,
+    /// Reserved. Set to 0.
+    #[skip_getter]
+    #[compile(0)]
+    reserved: u16,
+}
+
+/// The tracking data table.
+table TrackData {
+    /// Number of separate tracks included in this table.
+    n_tracks: u16,
+    /// Number of point sizes included in this table.
+    n_sizes: u16,
+    /// Offset from the start of the tracking table to the start of the size subtable.
+    size_table_offset: u32,
+    /// Array of TrackTableEntry records.
+    #[count($n_tracks)]
+    track_table: [TrackTableEntry],
+}
+
+/// Single entry in a tracking table.
+record TrackTableEntry {
+    /// Track value for this record.
+    track: Fixed,
+    /// The 'name' table index for this track (a short word or phrase like "loose" or "very tight"). NameIndex has a value greater than 255 and less than 32768.
+    name_index: NameId,
+    /// Offset from the start of the tracking table to per-size tracking values for this track.
+    offset: u16,
path: resources/codegen_inputs/trak.rs
user: login: cmyr
body: Is it like this because if you use `Offset16` it generates getters and the data source is ambiguous? I think this makes sense, given it's such a special case. I do now wonder if this should be general pattern, where we have a `#[data_source(SomeTable)]` annotation... ofc it would break a lot of code unless we got clever. (I think we could get clever, though... 🤔)
created_at: 2025-04-22T15:41:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    format: u16,
path: resources/codegen_inputs/trak.rs
user: login: dfrg
body: will do!
created_at: 2025-04-22T16:06:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    format: u16,
+    /// Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
+    #[nullable]
+    horiz_offset: Offset16<TrackData>,
+    /// Offset from start of tracking table to TrackData for vertical text (or 0 if none).
+    #[nullable]
+    vert_offset: Offset16<TrackData>,
+    /// Reserved. Set to 0.
+    #[skip_getter]
+    #[compile(0)]
+    reserved: u16,
+}
+
+/// The tracking data table.
+table TrackData {
+    /// Number of separate tracks included in this table.
+    n_tracks: u16,
+    /// Number of point sizes included in this table.
+    n_sizes: u16,
+    /// Offset from the start of the tracking table to the start of the size subtable.
+    size_table_offset: u32,
+    /// Array of TrackTableEntry records.
+    #[count($n_tracks)]
+    track_table: [TrackTableEntry],
+}
+
+/// Single entry in a tracking table.
+record TrackTableEntry {
+    /// Track value for this record.
+    track: Fixed,
+    /// The 'name' table index for this track (a short word or phrase like "loose" or "very tight"). NameIndex has a value greater than 255 and less than 32768.
+    name_index: NameId,
+    /// Offset from the start of the tracking table to per-size tracking values for this track.
+    offset: u16,
path: resources/codegen_inputs/trak.rs
user: login: dfrg
body: Yeah, I think we have encountered this pattern a few times (in bitmaps maybe)? I'm not sure if it's enough to justify supporting this in codegen but if you're sufficiently nerd sniped... :)
created_at: 2025-04-22T16:10:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+#![parse_module(read_fonts::tables::trak)]
+
+/// The [tracking (trak)](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html) table.
+#[tag = "trak"]
+table Trak {
+    /// Version number of the tracking table (0x00010000 for the current version).
+    version: MajorMinor,
+    /// Format of the tracking table (set to 0).
+    format: u16,
+    /// Offset from start of tracking table to TrackData for horizontal text (or 0 if none).
+    #[nullable]
+    horiz_offset: Offset16<TrackData>,
+    /// Offset from start of tracking table to TrackData for vertical text (or 0 if none).
+    #[nullable]
+    vert_offset: Offset16<TrackData>,
+    /// Reserved. Set to 0.
+    #[skip_getter]
+    #[compile(0)]
+    reserved: u16,
+}
+
+/// The tracking data table.
+table TrackData {
+    /// Number of separate tracks included in this table.
+    n_tracks: u16,
+    /// Number of point sizes included in this table.
+    n_sizes: u16,
+    /// Offset from the start of the tracking table to the start of the size subtable.
+    size_table_offset: u32,
+    /// Array of TrackTableEntry records.
+    #[count($n_tracks)]
+    track_table: [TrackTableEntry],
+}
+
+/// Single entry in a tracking table.
+record TrackTableEntry {
+    /// Track value for this record.
+    track: Fixed,
+    /// The 'name' table index for this track (a short word or phrase like "loose" or "very tight"). NameIndex has a value greater than 255 and less than 32768.
+    name_index: NameId,
+    /// Offset from the start of the tracking table to per-size tracking values for this track.
+    offset: u16,
path: resources/codegen_inputs/trak.rs
user: login: rsheeter
body: As per https://github.com/googlefonts/fontations/issues/1123 this routinely bites people trying to use our stuff so I think it's worth fixing. That said ... probably less urgent than making crater # go up.
created_at: 2025-04-22T17:27:43Z
author_association: COLLABORATOR

PR comments:
user: login: rsheeter
created_at: 2025-04-22T15:29:19Z
body: > This is a slight deviation from the rest of the read-fonts API but offers better type safety? 

I've almost always rued this type of inconsistency because it makes future refactors harder when things are inconsistent. Can we follow the normal patern and then, separately, upgrade all of read-fonts to have better type safety in this regard? - which FontData to pass is a long standing papercut.

EDIT: in reading the code ... it does feel much more natural this way. Maybe just comment that this is implemented this way to address the issue described in https://github.com/googlefonts/fontations/issues/1123.
author_association: COLLABORATOR
user: login: dfrg
created_at: 2025-04-22T15:35:05Z
body: > > This is a slight deviation from the rest of the read-fonts API but offers better type safety?
> 
> I've almost always rued this type of inconsistency because it makes future refactors harder when things are inconsistent. Can we follow the normal patern and then, separately, upgrade all of read-fonts to have better type safety in this regard? - which FontData to pass is a long standing papercut.

Sure, I'll change this to take FontData instead.
author_association: MEMBER