///|
/// Subset a TrueType font to include only the given codepoints.
/// Returns a new sfnt binary containing only the required glyphs.
/// Only supports glyf-based TrueType fonts (not CFF/CFF2).
pub fn subset_font(data : Bytes, codepoints : Array[Int]) -> Bytes? {
// Parse original font
guard parse_font(data) is Some(font) else { return None }
// Only glyf-based fonts supported
if font.cff is Some(_) {
return None
}
if font.loca.is_empty() {
return None
}
let reader = BinaryReader::new(data)
// Step 1: Build glyph ID set from codepoints
let glyph_set : Map[Int, Bool] = {}
glyph_set[0] = true // always include .notdef
for cp in codepoints {
let gid = font.glyph_index(cp)
if gid > 0 {
glyph_set[gid] = true
}
}
// Step 2: Resolve compound glyph dependencies
scan_compound_refs(font, glyph_set)
// Step 3: Build sorted list of old glyph IDs
let old_gids : Array[Int] = []
glyph_set.each(fn(gid, _) { old_gids.push(gid) })
old_gids.sort()
let new_num_glyphs = old_gids.length()
// Step 4: Build old->new glyph ID mapping
let gid_map : Map[Int, Int] = {}
for i, old_gid in old_gids {
gid_map[old_gid] = i
}
// Step 5: Build new glyf + loca tables
let (new_glyf, new_loca) = build_glyf_loca(font, old_gids, gid_map)
// Step 6: Build new cmap
let new_cmap = build_cmap_format12(font, codepoints, gid_map)
// Step 7: Build new hmtx
let new_hmtx = build_hmtx(font, old_gids)
// Step 8: Build new maxp
let new_maxp = build_maxp(reader, font, new_num_glyphs)
// Step 9: Copy head, hhea, name, OS/2, post tables from original
let tables : Array[(String, Bytes)] = []
guard analyze_tables(data) is Some(orig_tables) else { return None }
let copy_tags = ["hhea", "name", "OS/2", "post"]
for orig in orig_tables {
if copy_tags.contains(orig.tag) {
tables.push((orig.tag, bytes_slice(data, orig.offset, orig.length)))
}
if orig.tag == "head" {
// Copy head and set indexToLocFormat = 1 (long) since we use long loca
let head_data = bytes_slice(data, orig.offset, orig.length)
let head_out : Array[Byte] = Array::make(head_data.length(), b'\x00')
for j = 0; j < head_data.length(); j = j + 1 {
head_out[j] = head_data[j]
}
write_be16(head_out, 50, 1) // indexToLocFormat = 1 (long)
tables.push(("head", Bytes::from_array(head_out[:])))
}
if orig.tag == "hhea" {
// Update numOfLongHorMetrics in hhea (offset 34)
let idx = tables.length() - 1
let (tag, hhea_bytes) = tables[idx]
let hhea_out : Array[Byte] = Array::make(hhea_bytes.length(), b'\x00')
for j = 0; j < hhea_bytes.length(); j = j + 1 {
hhea_out[j] = hhea_bytes[j]
}
write_be16(hhea_out, 34, new_num_glyphs) // all glyphs have long metrics
tables[idx] = (tag, Bytes::from_array(hhea_out[:]))
}
}
// Add rebuilt tables
tables.push(("cmap", new_cmap))
tables.push(("glyf", new_glyf))
tables.push(("hmtx", new_hmtx))
tables.push(("loca", new_loca))
tables.push(("maxp", new_maxp))
// Sort tables by tag for proper sfnt ordering
tables.sort_by(fn(a, b) { a.0.compare(b.0) })
// Rebuild sfnt with TrueType flavor
Some(rebuild_sfnt(0x00010000, tables))
}
///|
/// Scan compound glyphs and add referenced component glyph IDs
fn scan_compound_refs(font : TTFont, glyph_set : Map[Int, Bool]) -> Unit {
let mut changed = true
while changed {
changed = false
let current_gids : Array[Int] = []
glyph_set.each(fn(gid, _) { current_gids.push(gid) })
for gid in current_gids {
if gid >= font.loca.length() - 1 {
continue
}
let glyf_off = font.loca[gid]
let next_off = font.loca[gid + 1]
if glyf_off == next_off {
continue
}
let offset = font.glyf_offset + glyf_off
let r = BinaryReader::at(font.data, offset)
let num_contours = r.read_int16()
if num_contours >= 0 {
continue // simple glyph
}
// Compound glyph: skip bbox (4 x int16 = 8 bytes)
r.skip(8)
let mut has_more = true
while has_more {
let flags = r.read_uint16()
let component_gid = r.read_uint16()
if glyph_set.get(component_gid) is None {
glyph_set[component_gid] = true
changed = true
}
// Skip arguments
let arg1_and_2_are_words = (flags & 1) != 0
if arg1_and_2_are_words {
r.skip(4)
} else {
r.skip(2)
}
// Skip transform data
let we_have_a_scale = (flags & 8) != 0
let we_have_xy_scale = (flags & 64) != 0
let we_have_2x2 = (flags & 128) != 0
if we_have_a_scale {
r.skip(2)
} else if we_have_xy_scale {
r.skip(4)
} else if we_have_2x2 {
r.skip(8)
}
has_more = (flags & 32) != 0
}
}
}
}
///|
/// Build new glyf and loca tables with remapped glyph IDs
fn build_glyf_loca(
font : TTFont,
old_gids : Array[Int],
gid_map : Map[Int, Int],
) -> (Bytes, Bytes) {
let glyf_parts : Array[Bytes] = []
let loca_offsets : Array[Int] = []
let mut current_offset = 0
for old_gid in old_gids {
loca_offsets.push(current_offset)
if old_gid >= font.loca.length() - 1 {
// empty glyph
} else {
let glyf_off = font.loca[old_gid]
let next_off = font.loca[old_gid + 1]
if glyf_off == next_off {
// empty glyph
} else {
let glyph_len = next_off - glyf_off
let abs_offset = font.glyf_offset + glyf_off
let glyph_data = bytes_slice(font.data, abs_offset, glyph_len)
// Check if compound glyph and rewrite component indices
let r = BinaryReader::new(glyph_data)
let num_contours = r.read_int16()
let final_data = if num_contours < 0 {
rewrite_compound_glyph(glyph_data, gid_map)
} else {
glyph_data
}
glyf_parts.push(final_data)
let padded = pad4(final_data.length())
current_offset = current_offset + padded
}
}
}
// Final loca entry
loca_offsets.push(current_offset)
// Build glyf bytes
let glyf_out : Array[Byte] = Array::make(current_offset, b'\x00')
let mut pos = 0
for part in glyf_parts {
for j = 0; j < part.length(); j = j + 1 {
glyf_out[pos + j] = part[j]
}
pos += pad4(part.length())
}
// Build loca table (long format)
let loca_len = loca_offsets.length() * 4
let loca_out : Array[Byte] = Array::make(loca_len, b'\x00')
for i, off in loca_offsets {
write_be32(loca_out, i * 4, off)
}
(Bytes::from_array(glyf_out[:]), Bytes::from_array(loca_out[:]))
}
///|
/// Rewrite compound glyph component indices to new glyph IDs
fn rewrite_compound_glyph(glyph_data : Bytes, gid_map : Map[Int, Int]) -> Bytes {
let out : Array[Byte] = Array::make(glyph_data.length(), b'\x00')
for i = 0; i < glyph_data.length(); i = i + 1 {
out[i] = glyph_data[i]
}
// Skip numContours(2) + bbox(8) = 10 bytes
let mut pos = 10
let mut has_more = true
while has_more && pos + 4 <= glyph_data.length() {
let flags = (glyph_data[pos].to_int() << 8) | glyph_data[pos + 1].to_int()
let old_gid = (glyph_data[pos + 2].to_int() << 8) |
glyph_data[pos + 3].to_int()
let new_gid = gid_map.get(old_gid).unwrap_or(0)
// Write new glyph ID
out[pos + 2] = ((new_gid >> 8) & 0xFF).to_byte()
out[pos + 3] = (new_gid & 0xFF).to_byte()
pos += 4 // flags + glyphIndex
let arg1_and_2_are_words = (flags & 1) != 0
if arg1_and_2_are_words {
pos += 4
} else {
pos += 2
}
if (flags & 8) != 0 {
pos += 2
} else if (flags & 64) != 0 {
pos += 4
} else if (flags & 128) != 0 {
pos += 8
}
has_more = (flags & 32) != 0
}
Bytes::from_array(out[:])
}
///|
/// Build a cmap table with Format 12
fn build_cmap_format12(
font : TTFont,
codepoints : Array[Int],
gid_map : Map[Int, Int],
) -> Bytes {
// Build sorted (codepoint, new_gid) pairs
let pairs : Array[(Int, Int)] = []
for cp in codepoints {
let old_gid = font.glyph_index(cp)
if old_gid > 0 {
match gid_map.get(old_gid) {
Some(new_gid) => pairs.push((cp, new_gid))
None => ()
}
}
}
pairs.sort_by(fn(a, b) { a.0.compare(b.0) })
// Build sequential groups
let groups : Array[(Int, Int, Int)] = [] // (startCharCode, endCharCode, startGlyphID)
for pair in pairs {
let (cp, gid) = pair
let can_extend = if groups.length() > 0 {
let (_, end_cp, start_gid) = groups[groups.length() - 1]
cp == end_cp + 1 &&
gid == start_gid + (cp - groups[groups.length() - 1].0)
} else {
false
}
if can_extend {
let idx = groups.length() - 1
let (start_cp, _, start_gid) = groups[idx]
groups[idx] = (start_cp, cp, start_gid)
} else {
groups.push((cp, cp, gid))
}
}
let num_groups = groups.length()
// Format 12 subtable: 16 + 12*numGroups bytes
let subtable_length = 16 + num_groups * 12
// cmap header: version(2) + numTables(1=2) + encoding record(8) = 12 bytes
let total = 12 + subtable_length
let out : Array[Byte] = Array::make(total, b'\x00')
// cmap header
write_be16(out, 0, 0) // version
write_be16(out, 2, 1) // numTables
// encoding record: platformID=3, encodingID=10, offset=12
write_be16(out, 4, 3)
write_be16(out, 6, 10)
write_be32(out, 8, 12)
// Format 12 subtable
let s = 12 // subtable start
write_be16(out, s, 12) // format
write_be16(out, s + 2, 0) // reserved
write_be32(out, s + 4, subtable_length) // length
write_be32(out, s + 8, 0) // language
write_be32(out, s + 12, num_groups)
for i, group in groups {
let (start_cp, end_cp, start_gid) = group
let off = s + 16 + i * 12
write_be32(out, off, start_cp)
write_be32(out, off + 4, end_cp)
write_be32(out, off + 8, start_gid)
}
Bytes::from_array(out[:])
}
///|
/// Build hmtx table for subset glyphs
fn build_hmtx(font : TTFont, old_gids : Array[Int]) -> Bytes {
let num_glyphs = old_gids.length()
// All entries as longHorMetric (4 bytes each)
let out : Array[Byte] = Array::make(num_glyphs * 4, b'\x00')
for i, old_gid in old_gids {
let advance = if old_gid < font.advance_widths.length() {
font.advance_widths[old_gid]
} else {
0
}
let lsb = if old_gid < font.left_side_bearings.length() {
font.left_side_bearings[old_gid]
} else {
0
}
write_be16(out, i * 4, advance)
// lsb as signed int16
write_be16(out, i * 4 + 2, lsb & 0xFFFF)
}
Bytes::from_array(out[:])
}
///|
/// Build maxp table (update numGlyphs)
fn build_maxp(
_reader : BinaryReader,
font : TTFont,
new_num_glyphs : Int,
) -> Bytes {
// Find original maxp table
guard analyze_tables(font.data) is Some(tables) else {
// Fallback: minimal maxp
let out : Array[Byte] = Array::make(6, b'\x00')
write_be32(out, 0, 0x00010000) // version 1.0
write_be16(out, 4, new_num_glyphs)
return Bytes::from_array(out[:])
}
let mut maxp_offset = 0
let mut maxp_length = 0
for t in tables {
if t.tag == "maxp" {
maxp_offset = t.offset
maxp_length = t.length
}
}
if maxp_length == 0 {
let out : Array[Byte] = Array::make(6, b'\x00')
write_be32(out, 0, 0x00010000)
write_be16(out, 4, new_num_glyphs)
return Bytes::from_array(out[:])
}
// Copy original maxp and update numGlyphs
let maxp_data = bytes_slice(font.data, maxp_offset, maxp_length)
let out : Array[Byte] = Array::make(maxp_data.length(), b'\x00')
for i = 0; i < maxp_data.length(); i = i + 1 {
out[i] = maxp_data[i]
}
write_be16(out, 4, new_num_glyphs)
Bytes::from_array(out[:])
}