///|
/// Parse a font from raw bytes, auto-detecting format (TTF, OTF, or WOFF1)
pub fn parse_font(data : Bytes) -> TTFont? {
// Minimum header: sfnt = 12 bytes, WOFF1 = 44 bytes
if data.length() < 12 {
return None
}
let magic = (data[0].to_int() << 24) |
(data[1].to_int() << 16) |
(data[2].to_int() << 8) |
data[3].to_int()
if magic == 0x774F4646 {
// wOFF (WOFF1)
parse_woff1(data)
} else if magic == 0x774F4632 {
// wOF2 (WOFF2)
parse_woff2(data)
} else if magic == 0x74746366 {
// ttcf (TTC/OTC font collection) — return first font
parse_font_at(data, 0)
} else {
// 0x00010000 (TrueType), 0x4F54544F (OTTO/CFF), 0x74727565 (true)
parse_ttf(data)
}
}
///|
/// Parse a TrueType/OpenType font from raw bytes starting at a given offset
fn parse_ttf_at_offset(data : Bytes, base_offset : Int) -> TTFont? {
let reader = BinaryReader::at(data, base_offset)
let _sfnt_version = reader.read_uint32()
let num_tables = reader.read_uint16()
let _search_range = reader.read_uint16()
let _entry_selector = reader.read_uint16()
let _range_shift = reader.read_uint16()
let tables : Map[String, TableRecord] = Map([])
for i = 0; i < num_tables; i = i + 1 {
let tag = reader.read_tag()
let _checksum = reader.read_uint32()
let offset = reader.read_uint32()
let _length = reader.read_uint32()
tables[tag] = { offset, }
ignore(i)
}
guard tables.get("head") is Some(head_rec) else { return None }
guard tables.get("maxp") is Some(maxp_rec) else { return None }
guard tables.get("hhea") is Some(hhea_rec) else { return None }
guard tables.get("hmtx") is Some(hmtx_rec) else { return None }
guard tables.get("cmap") is Some(cmap_rec) else { return None }
let has_glyf = tables.get("glyf") is Some(_)
let has_cff = tables.get("CFF ") is Some(_)
let has_cff2 = tables.get("CFF2") is Some(_)
let (units_per_em, index_to_loc_format) = parse_head(reader, head_rec.offset)
let num_glyphs = parse_maxp(reader, maxp_rec.offset)
let (ascent, descent, line_gap, num_h_metrics) = parse_hhea(
reader,
hhea_rec.offset,
)
let (advance_widths, left_side_bearings) = parse_hmtx(
reader,
hmtx_rec.offset,
num_glyphs,
num_h_metrics,
)
let cmap = parse_cmap(reader, cmap_rec.offset)
let var_axes : Array[VarAxis] = match tables.get("fvar") {
Some(fvar_rec) => parse_fvar(reader, fvar_rec.offset)
None => []
}
let kern_pairs : Map[Int, Int] = match tables.get("kern") {
Some(kern_rec) => parse_kern(reader, kern_rec.offset)
None => {}
}
let avar_segments : Array[Array[(Double, Double)]] = match
tables.get("avar") {
Some(avar_rec) => parse_avar(reader, avar_rec.offset, var_axes.length())
None => []
}
let name_records : Map[Int, String] = match tables.get("name") {
Some(name_rec) => parse_name(reader, name_rec.offset)
None => {}
}
let vertical : VerticalMetrics? = match
(tables.get("vhea"), tables.get("vmtx")) {
(Some(vhea_rec), Some(vmtx_rec)) => {
let (v_ascent, v_descent, num_v_metrics) = parse_vhea(
reader,
vhea_rec.offset,
)
let (advance_heights, top_side_bearings) = parse_vmtx(
reader,
vmtx_rec.offset,
num_glyphs,
num_v_metrics,
)
Some({ v_ascent, v_descent, advance_heights, top_side_bearings })
}
_ => None
}
let os2 : OS2Table? = tables
.get("OS/2")
.map(fn(os2_rec) { parse_os2(reader, os2_rec.offset) })
let post : PostTable? = tables
.get("post")
.map(fn(post_rec) { parse_post(reader, post_rec.offset, num_glyphs) })
let gasp : Array[GaspRange] = match tables.get("gasp") {
Some(gasp_rec) => parse_gasp(reader, gasp_rec.offset)
None => []
}
let vorg : VorgData? = tables
.get("VORG")
.map(fn(vorg_rec) { parse_vorg(reader, vorg_rec.offset) })
if has_glyf {
guard tables.get("loca") is Some(loca_rec) else { return None }
guard tables.get("glyf") is Some(glyf_rec) else { return None }
let loca = parse_loca(
reader,
loca_rec.offset,
num_glyphs,
index_to_loc_format,
)
let gvar_data : GvarData? = tables
.get("gvar")
.map(fn(gvar_rec) { parse_gvar(reader, gvar_rec.offset, num_glyphs) })
.bind(fn(x) { x })
Some({
units_per_em,
index_to_loc_format,
num_glyphs,
ascent,
descent,
line_gap,
num_h_metrics,
glyf_offset: glyf_rec.offset,
data,
cmap,
loca,
advance_widths,
left_side_bearings,
cff: None,
gvar: gvar_data,
var_axes,
kern_pairs,
avar_segments,
name_records,
os2,
post,
vertical,
gasp,
vorg,
})
} else if has_cff {
guard tables.get("CFF ") is Some(cff_rec) else { return None }
parse_cff_table(data, cff_rec.offset).map(fn(cff) {
{
units_per_em,
index_to_loc_format,
num_glyphs,
ascent,
descent,
line_gap,
num_h_metrics,
glyf_offset: 0,
data,
cmap,
loca: [],
advance_widths,
left_side_bearings,
cff: Some(cff),
gvar: None,
var_axes,
kern_pairs,
avar_segments,
name_records,
os2,
post,
vertical,
gasp,
vorg,
}
})
} else if has_cff2 {
guard tables.get("CFF2") is Some(cff2_rec) else { return None }
parse_cff2_table(data, cff2_rec.offset).map(fn(cff) {
{
units_per_em,
index_to_loc_format,
num_glyphs,
ascent,
descent,
line_gap,
num_h_metrics,
glyf_offset: 0,
data,
cmap,
loca: [],
advance_widths,
left_side_bearings,
cff: Some(cff),
gvar: None,
var_axes,
kern_pairs,
avar_segments,
name_records,
os2,
post,
vertical,
gasp,
vorg,
}
})
} else {
None
}
}
///|
/// Parse a TrueType/OpenType font from raw bytes
pub fn parse_ttf(data : Bytes) -> TTFont? {
parse_ttf_at_offset(data, 0)
}
///|
pub fn TTFont::glyph_index(self : TTFont, codepoint : Int) -> Int {
self.cmap.get(codepoint).unwrap_or(0)
}
///|
pub fn TTFont::glyph_outline(
self : TTFont,
glyph_id : Int,
) -> Array[@svg.PathCommand] {
glyph_outline_by_id(self, glyph_id)
}
///|
pub fn TTFont::char_outline(
self : TTFont,
codepoint : Int,
) -> Array[@svg.PathCommand] {
let gid = self.glyph_index(codepoint)
self.glyph_outline(gid)
}
///|
pub fn TTFont::glyph_metrics(self : TTFont, glyph_id : Int) -> GlyphMetrics {
let advance = if glyph_id < self.advance_widths.length() {
self.advance_widths[glyph_id]
} else {
0
}
let lsb = if glyph_id < self.left_side_bearings.length() {
self.left_side_bearings[glyph_id]
} else {
0
}
let bbox : GlyphBBox = match self.cff {
Some(_) => {
// CFF fonts: compute bbox from outline
let outline = self.glyph_outline(glyph_id)
compute_path_bbox(outline)
}
None =>
if glyph_id < self.num_glyphs && glyph_id < self.loca.length() - 1 {
let glyf_off = self.loca[glyph_id]
let next_off = self.loca[glyph_id + 1]
if glyf_off != next_off {
let offset = self.glyf_offset + glyf_off
let r = BinaryReader::at(self.data, offset)
let _num_contours = r.read_int16()
let x_min = r.read_int16()
let y_min = r.read_int16()
let x_max = r.read_int16()
let y_max = r.read_int16()
{ x_min, y_min, x_max, y_max }
} else {
{ x_min: 0, y_min: 0, x_max: 0, y_max: 0 }
}
} else {
{ x_min: 0, y_min: 0, x_max: 0, y_max: 0 }
}
}
{ advance_width: advance, left_side_bearing: lsb, bbox }
}
///|
pub fn TTFont::get_glyph(self : TTFont, codepoint : Int) -> Glyph {
let gid = self.glyph_index(codepoint)
let metrics = self.glyph_metrics(gid)
let outline = self.glyph_outline(gid)
{ glyph_id: gid, metrics, outline }
}
///|
/// Get glyph metrics with variable font interpolation.
/// For glyf+gvar fonts, advance width is interpolated via phantom points.
/// For CFF2 fonts, bbox is computed from the variable outline (advance uses static value).
pub fn TTFont::glyph_metrics_at(
self : TTFont,
glyph_id : Int,
axis_values : Map[String, Double],
) -> GlyphMetrics {
if self.var_axes.is_empty() {
return self.glyph_metrics(glyph_id)
}
// Normalize coordinates with avar mapping
let coords : Array[Double] = []
for i, axis in self.var_axes {
let user_val = axis_values.get(axis.tag).unwrap_or(axis.default_value)
let mut coord = normalize_axis_coord(user_val, axis)
if i < self.avar_segments.length() {
coord = apply_avar(coord, self.avar_segments[i])
}
coords.push(coord)
}
let advance = if glyph_id < self.advance_widths.length() {
self.advance_widths[glyph_id]
} else {
0
}
let lsb = if glyph_id < self.left_side_bearings.length() {
self.left_side_bearings[glyph_id]
} else {
0
}
// glyf+gvar: interpolate metrics via phantom points
if self.gvar is Some(gvar) && self.cff is None {
let (var_advance, var_lsb, var_bbox) = gvar_interpolate_metrics(
self, gvar, glyph_id, coords, advance, lsb,
)
return {
advance_width: var_advance,
left_side_bearing: var_lsb,
bbox: var_bbox,
}
}
// CFF2: compute bbox from variable outline, advance stays static
if self.cff is Some(cff) && cff.ivs is Some(ivs) {
let scalars = precompute_scalars(ivs, 0, coords)
let outline = glyph_outline_by_id_var(self, glyph_id, scalars)
let bbox = compute_path_bbox(outline)
return { advance_width: advance, left_side_bearing: lsb, bbox }
}
self.glyph_metrics(glyph_id)
}
///|
pub fn TTFont::glyph_outline_at(
self : TTFont,
glyph_id : Int,
axis_values : Map[String, Double],
) -> Array[@svg.PathCommand] {
if self.var_axes.is_empty() {
return self.glyph_outline(glyph_id)
}
// Normalize coordinates with avar mapping
let coords : Array[Double] = []
for i, axis in self.var_axes {
let user_val = match axis_values.get(axis.tag) {
Some(v) => v
None => axis.default_value
}
let mut coord = normalize_axis_coord(user_val, axis)
if i < self.avar_segments.length() {
coord = apply_avar(coord, self.avar_segments[i])
}
coords.push(coord)
}
// CFF2 variable font path
if self.cff is Some(cff) && cff.ivs is Some(ivs) {
let scalars = precompute_scalars(ivs, 0, coords)
return glyph_outline_by_id_var(self, glyph_id, scalars)
}
// glyf+gvar variable font path
if self.gvar is Some(_) {
return glyf_outline_by_id_var_gvar(self, glyph_id, coords)
}
self.glyph_outline(glyph_id)
}
///|
pub fn TTFont::char_outline_at(
self : TTFont,
codepoint : Int,
axis_values : Map[String, Double],
) -> Array[@svg.PathCommand] {
let gid = self.glyph_index(codepoint)
self.glyph_outline_at(gid, axis_values)
}
///|
pub fn TTFont::scaled_outline(
self : TTFont,
codepoint : Int,
font_size : Double,
) -> Array[@svg.PathCommand] {
let outline = self.char_outline(codepoint)
let s = font_size / self.units_per_em.to_double()
scale_commands(outline, s, true)
}
///|
/// Get kerning value for a pair of glyph IDs
pub fn TTFont::kern_pair(self : TTFont, left_gid : Int, right_gid : Int) -> Int {
let key = (left_gid << 16) | right_gid
self.kern_pairs.get(key).unwrap_or(0)
}
///|
/// Get font name by nameID (1=family, 2=subfamily, 4=full name, 6=postscript name)
pub fn TTFont::font_name(self : TTFont, name_id : Int) -> String? {
self.name_records.get(name_id)
}
///|
/// Get weight class from OS/2 table (400=Regular, 700=Bold)
pub fn TTFont::weight_class(self : TTFont) -> Int {
match self.os2 {
Some(os2) => os2.us_weight_class
None => 400
}
}
///|
/// Check if font is fixed-pitch from post table
pub fn TTFont::is_fixed_pitch(self : TTFont) -> Bool {
match self.post {
Some(post) => post.is_fixed_pitch
None => false
}
}
///|
/// Get italic angle from post table
pub fn TTFont::italic_angle(self : TTFont) -> Double {
match self.post {
Some(post) => post.italic_angle
None => 0.0
}
}
///|
/// Get x-height from OS/2 table
pub fn TTFont::x_height(self : TTFont) -> Int {
match self.os2 {
Some(os2) => os2.sx_height
None => 0
}
}
///|
/// Get cap-height from OS/2 table
pub fn TTFont::cap_height(self : TTFont) -> Int {
match self.os2 {
Some(os2) => os2.s_cap_height
None => 0
}
}
///|
/// Get glyph name from post table
pub fn TTFont::glyph_name(self : TTFont, glyph_id : Int) -> String {
match self.post {
Some(post) =>
if glyph_id < post.glyph_names.length() {
post.glyph_names[glyph_id]
} else {
""
}
None => ""
}
}
///|
/// Apply avar segment mapping (piecewise linear interpolation)
fn apply_avar(coord : Double, segment : Array[(Double, Double)]) -> Double {
if segment.is_empty() {
return coord
}
// Below first point
let (first_from, first_to) = segment[0]
if coord <= first_from {
return first_to
}
// Above last point
let (last_from, last_to) = segment[segment.length() - 1]
if coord >= last_from {
return last_to
}
// Find segment and interpolate
for i = 1; i < segment.length(); i = i + 1 {
let (from1, to1) = segment[i - 1]
let (from2, to2) = segment[i]
if coord <= from2 {
if from2 == from1 {
return to2
}
let t = (coord - from1) / (from2 - from1)
return to1 + t * (to2 - to1)
}
}
coord
}