///|
/// Parse raw glyph point data: coordinates, flags, and contour endpoints
fn parse_glyph_points(
reader : BinaryReader,
num_contours : Int,
) -> (Array[Int], Array[Int], Array[Int], Array[Int]) {
let end_points = Array::new(capacity=num_contours)
for i = 0; i < num_contours; i = i + 1 {
end_points.push(reader.read_uint16())
ignore(i)
}
let num_points = if num_contours > 0 {
end_points[num_contours - 1] + 1
} else {
0
}
let instruction_length = reader.read_uint16()
reader.skip(instruction_length)
let flags = Array::new(capacity=num_points)
let mut i = 0
while i < num_points {
let flag = reader.read_uint8()
flags.push(flag)
if (flag & 8) != 0 {
let repeat_count = reader.read_uint8()
for _j = 0; _j < repeat_count; _j = _j + 1 {
flags.push(flag)
}
i = i + repeat_count
}
i = i + 1
}
let x_coords = Array::new(capacity=num_points)
let mut x = 0
for i = 0; i < num_points; i = i + 1 {
let flag = flags[i]
let x_short = (flag & 2) != 0
let x_same = (flag & 16) != 0
if x_short {
let dx = reader.read_uint8()
if x_same {
x = x + dx
} else {
x = x - dx
}
} else if x_same {
()
} else {
x = x + reader.read_int16()
}
x_coords.push(x)
}
let y_coords = Array::new(capacity=num_points)
let mut y = 0
for i = 0; i < num_points; i = i + 1 {
let flag = flags[i]
let y_short = (flag & 4) != 0
let y_same = (flag & 32) != 0
if y_short {
let dy = reader.read_uint8()
if y_same {
y = y + dy
} else {
y = y - dy
}
} else if y_same {
()
} else {
y = y + reader.read_int16()
}
y_coords.push(y)
}
(x_coords, y_coords, flags, end_points)
}
///|
/// Assemble contours from raw point data
fn assemble_contours(
x_coords : Array[Int],
y_coords : Array[Int],
flags : Array[Int],
end_points : Array[Int],
) -> Array[Array[GlyphPoint]] {
let contours : Array[Array[GlyphPoint]] = []
let mut start = 0
for ep in end_points {
let contour : Array[GlyphPoint] = []
for j = start; j <= ep; j = j + 1 {
contour.push({
x: x_coords[j],
y: y_coords[j],
on_curve: (flags[j] & 1) != 0,
})
}
contours.push(contour)
start = ep + 1
}
contours
}
///|
/// Parse a simple glyph from the glyf table
fn parse_simple_glyph(
reader : BinaryReader,
num_contours : Int,
) -> Array[Array[GlyphPoint]] {
let (x_coords, y_coords, flags, end_points) = parse_glyph_points(
reader, num_contours,
)
assemble_contours(x_coords, y_coords, flags, end_points)
}
///|
fn contours_to_path_commands(
contours : Array[Array[GlyphPoint]],
) -> Array[@svg.PathCommand] {
let commands : Array[@svg.PathCommand] = []
for contour in contours {
if contour.is_empty() {
continue
}
contour_to_path(contour, commands)
}
commands
}
///|
fn contour_to_path(
contour : Array[GlyphPoint],
commands : Array[@svg.PathCommand],
) -> Unit {
let n = contour.length()
if n == 0 {
return
}
let first = contour[0]
let last = contour[n - 1]
let (start_x, start_y, start_idx) = if first.on_curve {
(first.x.to_double(), first.y.to_double(), 0)
} else if last.on_curve {
(last.x.to_double(), last.y.to_double(), 0)
} else {
let mx = (first.x + last.x).to_double() / 2.0
let my = (first.y + last.y).to_double() / 2.0
(mx, my, 0)
}
commands.push(@svg.MoveTo(start_x, start_y))
let mut i = start_idx
while i < n {
let p = contour[i]
if p.on_curve {
commands.push(@svg.LineTo(p.x.to_double(), p.y.to_double()))
i = i + 1
} else {
let ctrl_x = p.x.to_double()
let ctrl_y = p.y.to_double()
let next_idx = (i + 1) % n
let next = contour[next_idx]
if next.on_curve {
commands.push(
@svg.QuadraticCurveTo(
ctrl_x,
ctrl_y,
next.x.to_double(),
next.y.to_double(),
),
)
i = i + 2
} else {
let mid_x = (ctrl_x + next.x.to_double()) / 2.0
let mid_y = (ctrl_y + next.y.to_double()) / 2.0
commands.push(@svg.QuadraticCurveTo(ctrl_x, ctrl_y, mid_x, mid_y))
i = i + 1
}
}
}
commands.push(@svg.ClosePath)
}
///|
fn parse_compound_glyph(
font : TTFont,
reader : BinaryReader,
) -> Array[@svg.PathCommand] {
let commands : Array[@svg.PathCommand] = []
let mut has_more = true
while has_more {
let flags = reader.read_uint16()
let glyph_index = reader.read_uint16()
let mut dx = 0.0
let mut dy = 0.0
let arg1_and_2_are_words = (flags & 1) != 0
let args_are_xy = (flags & 2) != 0
if arg1_and_2_are_words {
if args_are_xy {
dx = reader.read_int16().to_double()
dy = reader.read_int16().to_double()
} else {
let _p1 = reader.read_uint16()
let _p2 = reader.read_uint16()
}
} else if args_are_xy {
dx = reader.read_int8().to_double()
dy = reader.read_int8().to_double()
} else {
let _p1 = reader.read_uint8()
let _p2 = reader.read_uint8()
}
let mut sx = 1.0
let mut sy = 1.0
let we_have_a_scale = (flags & 8) != 0
let we_have_an_x_and_y_scale = (flags & 64) != 0
let we_have_a_two_by_two = (flags & 128) != 0
if we_have_a_scale {
sx = reader.read_int16().to_double() / 16384.0
sy = sx
} else if we_have_an_x_and_y_scale {
sx = reader.read_int16().to_double() / 16384.0
sy = reader.read_int16().to_double() / 16384.0
} else if we_have_a_two_by_two {
sx = reader.read_int16().to_double() / 16384.0
let _sxy = reader.read_int16().to_double() / 16384.0
let _syx = reader.read_int16().to_double() / 16384.0
sy = reader.read_int16().to_double() / 16384.0
}
let component_cmds = glyph_outline_by_id(font, glyph_index)
let transformed = transform_commands(component_cmds, dx, dy, sx, sy)
for cmd in transformed {
commands.push(cmd)
}
has_more = (flags & 32) != 0
}
commands
}
///|
fn transform_commands(
commands : Array[@svg.PathCommand],
dx : Double,
dy : Double,
sx : Double,
sy : Double,
) -> Array[@svg.PathCommand] {
let result : Array[@svg.PathCommand] = []
for cmd in commands {
let transformed : @svg.PathCommand = match cmd {
@svg.MoveTo(x, y) => @svg.MoveTo(x * sx + dx, y * sy + dy)
@svg.LineTo(x, y) => @svg.LineTo(x * sx + dx, y * sy + dy)
@svg.QuadraticCurveTo(cx, cy, x, y) =>
@svg.QuadraticCurveTo(
cx * sx + dx,
cy * sy + dy,
x * sx + dx,
y * sy + dy,
)
@svg.CurveTo(cx1, cy1, cx2, cy2, x, y) =>
@svg.CurveTo(
cx1 * sx + dx,
cy1 * sy + dy,
cx2 * sx + dx,
cy2 * sy + dy,
x * sx + dx,
y * sy + dy,
)
@svg.ClosePath => @svg.ClosePath
other => other
}
result.push(transformed)
}
result
}
///|
pub fn scale_commands(
commands : Array[@svg.PathCommand],
scale : Double,
flip_y : Bool,
) -> Array[@svg.PathCommand] {
let result : Array[@svg.PathCommand] = []
let ys = if flip_y { -scale } else { scale }
for cmd in commands {
let scaled : @svg.PathCommand = match cmd {
@svg.MoveTo(x, y) => @svg.MoveTo(x * scale, y * ys)
@svg.LineTo(x, y) => @svg.LineTo(x * scale, y * ys)
@svg.QuadraticCurveTo(cx, cy, x, y) =>
@svg.QuadraticCurveTo(cx * scale, cy * ys, x * scale, y * ys)
@svg.CurveTo(cx1, cy1, cx2, cy2, x, y) =>
@svg.CurveTo(
cx1 * scale,
cy1 * ys,
cx2 * scale,
cy2 * ys,
x * scale,
y * ys,
)
@svg.ClosePath => @svg.ClosePath
other => other
}
result.push(scaled)
}
result
}
///|
fn glyph_outline_by_id(
font : TTFont,
glyph_id : Int,
) -> Array[@svg.PathCommand] {
if glyph_id < 0 || glyph_id >= font.num_glyphs {
return []
}
match font.cff {
Some(cff) =>
// Variable CFF2 font: use var path with default (zero) scalars
match cff.ivs {
Some(ivs) => {
let scalars = precompute_scalars(ivs, 0, [])
cff_glyph_outline_var(font.data, cff, glyph_id, scalars)
}
None => cff_glyph_outline(font.data, cff, glyph_id)
}
None => glyf_outline_by_id(font, glyph_id)
}
}
///|
fn glyph_outline_by_id_var(
font : TTFont,
glyph_id : Int,
scalars : Array[Double],
) -> Array[@svg.PathCommand] {
if glyph_id < 0 || glyph_id >= font.num_glyphs {
return []
}
match font.cff {
Some(cff) => cff_glyph_outline_var(font.data, cff, glyph_id, scalars)
None => glyf_outline_by_id(font, glyph_id)
}
}
///|
/// Get glyf outline with gvar variation applied
fn glyf_outline_by_id_var_gvar(
font : TTFont,
glyph_id : Int,
coords : Array[Double],
) -> Array[@svg.PathCommand] {
let glyf_offset = font.loca[glyph_id]
let next_offset = font.loca[glyph_id + 1]
if glyf_offset == next_offset {
return []
}
let offset = font.glyf_offset + glyf_offset
let reader = BinaryReader::at(font.data, offset)
let num_contours = reader.read_int16()
let _x_min = reader.read_int16()
let _y_min = reader.read_int16()
let _x_max = reader.read_int16()
let _y_max = reader.read_int16()
if num_contours < 0 {
// Compound glyph: no gvar support yet, fall back to static
return parse_compound_glyph(font, reader)
}
let (x_coords, y_coords, flags, end_points) = parse_glyph_points(
reader, num_contours,
)
guard font.gvar is Some(gvar) else {
return contours_to_path_commands(
assemble_contours(x_coords, y_coords, flags, end_points),
)
}
let (dx, dy) = apply_gvar_deltas(
font.data,
gvar,
glyph_id,
coords,
x_coords,
y_coords,
end_points,
)
// Apply deltas
let var_x : Array[Int] = []
let var_y : Array[Int] = []
for i = 0; i < x_coords.length(); i = i + 1 {
var_x.push(x_coords[i] + dx[i])
var_y.push(y_coords[i] + dy[i])
}
let contours = assemble_contours(var_x, var_y, flags, end_points)
contours_to_path_commands(contours)
}
///|
/// Interpolate glyph metrics for glyf+gvar variable fonts using phantom points.
/// Phantom points are appended after the real glyph points:
/// phantom[0] = (0, 0) — origin
/// phantom[1] = (advance, 0) — advance width
/// phantom[2] = (lsb, 0) — left side bearing
/// phantom[3] = (0, 0) — reserved
/// Returns (interpolated_advance, interpolated_lsb, bbox).
fn gvar_interpolate_metrics(
font : TTFont,
gvar : GvarData,
glyph_id : Int,
coords : Array[Double],
advance : Int,
lsb : Int,
) -> (Int, Int, GlyphBBox) {
let default_bbox : GlyphBBox = { x_min: 0, y_min: 0, x_max: 0, y_max: 0 }
if glyph_id >= font.loca.length() - 1 {
return (advance, lsb, default_bbox)
}
let glyf_off = font.loca[glyph_id]
let next_off = font.loca[glyph_id + 1]
if glyf_off == next_off {
// Empty glyph — still apply phantom point deltas
let x_coords : Array[Int] = [0, advance, lsb, 0]
let y_coords : Array[Int] = [0, 0, 0, 0]
let end_points : Array[Int] = []
let (dx, _dy) = apply_gvar_deltas(
font.data,
gvar,
glyph_id,
coords,
x_coords,
y_coords,
end_points,
)
let var_advance = if dx.length() > 1 { advance + dx[1] } else { advance }
let var_lsb = if dx.length() > 2 { lsb + dx[2] } else { lsb }
return (var_advance, var_lsb, default_bbox)
}
let offset = font.glyf_offset + glyf_off
let reader = BinaryReader::at(font.data, offset)
let num_contours = reader.read_int16()
let x_min = reader.read_int16()
let y_min = reader.read_int16()
let x_max = reader.read_int16()
let y_max = reader.read_int16()
if num_contours < 0 {
// Compound glyph — return static metrics with table bbox
return (advance, lsb, { x_min, y_min, x_max, y_max })
}
let (x_coords, y_coords, flags, end_points) = parse_glyph_points(
reader, num_contours,
)
// Append 4 phantom points
let x_with_phantom = x_coords.copy()
let y_with_phantom = y_coords.copy()
x_with_phantom.push(0) // phantom 0: origin x
y_with_phantom.push(0) // phantom 0: origin y
x_with_phantom.push(advance) // phantom 1: advance x
y_with_phantom.push(0) // phantom 1: advance y
x_with_phantom.push(lsb) // phantom 2: lsb x
y_with_phantom.push(0) // phantom 2: lsb y
x_with_phantom.push(0) // phantom 3: reserved
y_with_phantom.push(0) // phantom 3: reserved
let (dx, dy) = apply_gvar_deltas(
font.data,
gvar,
glyph_id,
coords,
x_with_phantom,
y_with_phantom,
end_points,
)
let num_real = x_coords.length()
let var_advance = if dx.length() > num_real + 1 {
advance + dx[num_real + 1]
} else {
advance
}
let var_lsb = if dx.length() > num_real + 2 {
lsb + dx[num_real + 2]
} else {
lsb
}
// Compute variable bbox from interpolated outline
let var_x : Array[Int] = []
let var_y : Array[Int] = []
for i = 0; i < num_real; i = i + 1 {
var_x.push(x_coords[i] + dx[i])
var_y.push(y_coords[i] + dy[i])
}
let contours = assemble_contours(var_x, var_y, flags, end_points)
let outline = contours_to_path_commands(contours)
let bbox = compute_path_bbox(outline)
(var_advance, var_lsb, bbox)
}
///|
fn glyf_outline_by_id(font : TTFont, glyph_id : Int) -> Array[@svg.PathCommand] {
let glyf_offset = font.loca[glyph_id]
let next_offset = font.loca[glyph_id + 1]
if glyf_offset == next_offset {
return []
}
let offset = font.glyf_offset + glyf_offset
let reader = BinaryReader::at(font.data, offset)
let num_contours = reader.read_int16()
let _x_min = reader.read_int16()
let _y_min = reader.read_int16()
let _x_max = reader.read_int16()
let _y_max = reader.read_int16()
if num_contours >= 0 {
let contours = parse_simple_glyph(reader, num_contours)
contours_to_path_commands(contours)
} else {
parse_compound_glyph(font, reader)
}
}