// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Outline module.
///
/// Ported from `fontations/skrifa/src/outline/*` (Apache-2.0 OR MIT).
///
/// This package targets API/behavior parity with upstream `skrifa::outline`.
const TAG_GLYF : UInt = 0x676C7966 // "glyf"
///|
const TAG_CFF : UInt = 0x43464620 // "CFF "
///|
const TAG_CFF2 : UInt = 0x43464632 // "CFF2"
///|
const TAG_FPGM : UInt = 0x6670676D // "fpgm"
///|
const TAG_PREP : UInt = 0x70726570 // "prep"
///|
/// Source format for an outline glyph.
pub(all) enum OutlineGlyphFormat {
Glyf
Cff
Cff2
}
///|
/// Specifies the hinting strategy for memory size calculations.
pub(all) enum Hinting {
None_
Embedded
}
///|
pub fn Hinting::default() -> Hinting {
None_
}
///|
pub fn Hinting::none() -> Hinting {
None_
}
///|
/// Information and adjusted metrics generated while drawing an outline glyph.
pub struct AdjustedMetrics {
has_overlaps : Bool
lsb : Double?
advance_width : Double?
}
///|
pub fn AdjustedMetrics::default() -> AdjustedMetrics {
{ has_overlaps: false, lsb: None, advance_width: None }
}
///|
pub struct OutlineGlyphCollection {
priv font : @moon_skrifa.FontRef
priv kind : OutlineGlyphFormat?
}
///|
pub fn OutlineGlyphCollection::OutlineGlyphCollection(
font : @moon_skrifa.FontRef,
) -> OutlineGlyphCollection {
// Upstream selection order: glyf first, then CFF2, then CFF.
let kind = if font.table(TAG_GLYF) is Some(_) {
Some(OutlineGlyphFormat::Glyf)
} else if font.table(TAG_CFF2) is Some(_) {
Some(Cff2)
} else if font.table(TAG_CFF) is Some(_) {
Some(Cff)
} else {
None
}
{ font, kind }
}
///|
pub fn OutlineGlyphCollection::from_font(
font : @moon_skrifa.FontRef,
) -> OutlineGlyphCollection {
OutlineGlyphCollection(font)
}
///|
/// Creates a new outline collection for the given font and outline format.
///
/// Returns `None` if the font does not contain outlines in the requested
/// format.
pub fn OutlineGlyphCollection::with_format(
font : @moon_skrifa.FontRef,
format : OutlineGlyphFormat,
) -> OutlineGlyphCollection? {
match format {
Glyf =>
if font.table(TAG_GLYF) is Some(_) {
Some({ font, kind: Some(format) })
} else {
None
}
Cff =>
if font.table(TAG_CFF) is Some(_) {
Some({ font, kind: Some(format) })
} else {
None
}
Cff2 =>
if font.table(TAG_CFF2) is Some(_) {
Some({ font, kind: Some(format) })
} else {
None
}
}
}
///|
priv enum DrawInstance {
Unhinted(@moon_skrifa.Size, @moon_skrifa.LocationRef)
Hinted(HintingInstance, Bool)
}
///|
/// Options that define how a glyph outline is drawn to a pen.
pub struct DrawSettings {
priv instance : DrawInstance
priv memory : Array[Byte]?
priv path_style : PathStyle
}
///|
pub fn DrawSettings::default() -> DrawSettings {
DrawSettings::unhinted(
@moon_skrifa.Size::unscaled(),
@moon_skrifa.LocationRef::default(),
)
}
///|
pub fn DrawSettings::unhinted(
size : @moon_skrifa.Size,
location : @moon_skrifa.LocationRef,
) -> DrawSettings {
{
instance: Unhinted(size, location),
memory: None,
path_style: PathStyle::default(),
}
}
///|
pub fn DrawSettings::hinted(
instance : HintingInstance,
is_pedantic : Bool,
) -> DrawSettings {
{
instance: Hinted(instance, is_pedantic),
memory: None,
path_style: PathStyle::default(),
}
}
///|
pub fn DrawSettings::with_memory(
self : DrawSettings,
memory : Array[Byte]?,
) -> DrawSettings {
{ instance: self.instance, memory, path_style: self.path_style }
}
///|
pub fn DrawSettings::with_path_style(
self : DrawSettings,
path_style : PathStyle,
) -> DrawSettings {
{ instance: self.instance, memory: self.memory, path_style }
}
///|
pub fn DrawSettings::path_style(self : DrawSettings) -> PathStyle {
self.path_style
}
///|
pub fn DrawSettings::size(self : DrawSettings) -> @moon_skrifa.Size {
match self.instance {
Unhinted(size, _) => size
Hinted(h, _) => h.size()
}
}
///|
pub fn DrawSettings::location(self : DrawSettings) -> @moon_skrifa.LocationRef {
match self.instance {
Unhinted(_, loc) => loc
Hinted(h, _) => h.location()
}
}
///|
pub fn DrawSettings::hinting_instance(self : DrawSettings) -> HintingInstance? {
match self.instance {
Hinted(h, _) => Some(h)
_ => None
}
}
///|
pub fn DrawSettings::is_pedantic(self : DrawSettings) -> Bool {
match self.instance {
Hinted(_, p) => p
_ => false
}
}
///|
/// A scalable glyph outline.
///
/// This can be sourced from the `glyf`, `CFF` or `CFF2` tables.
pub struct OutlineGlyph {
priv font : @moon_skrifa.FontRef
priv glyph_id : @moon_skrifa.GlyphId
priv kind : OutlineGlyphFormat
}
///|
pub fn OutlineGlyph::format(self : OutlineGlyph) -> OutlineGlyphFormat {
self.kind
}
///|
pub fn OutlineGlyph::glyph_id(self : OutlineGlyph) -> @moon_skrifa.GlyphId {
self.glyph_id
}
///|
/// Returns a value indicating if the outline may contain overlapping contours
/// or components.
///
/// For CFF outlines, returns `None` since this information is unavailable.
pub fn OutlineGlyph::has_overlaps(self : OutlineGlyph) -> Bool? {
match self.kind {
Glyf => Some(glyf_has_overlaps(self.font, self.glyph_id).unwrap_or(false))
_ => None
}
}
///|
/// Returns a value indicating whether the outline has hinting instructions.
///
/// For CFF outlines, returns `None` since this is unknown prior to loading the
/// outline.
pub fn OutlineGlyph::has_hinting(self : OutlineGlyph) -> Bool? {
match self.kind {
Glyf => Some(glyf_has_hinting(self.font, self.glyph_id).unwrap_or(false))
_ => None
}
}
///|
/// Returns the size (in bytes) of the temporary memory required to draw this
/// outline.
pub fn OutlineGlyph::draw_memory_size(
self : OutlineGlyph,
hinting : Hinting,
) -> Int {
match self.kind {
Glyf =>
glyf_draw_memory_size(self.font, self.glyph_id, hinting).unwrap_or(0)
_ => 0
}
}
///|
/// Draws the outline glyph with the given settings and emits the resulting
/// path commands to the specified pen.
pub fn[P : OutlinePen] OutlineGlyph::draw(
self : OutlineGlyph,
settings : DrawSettings,
pen : P,
) -> Result[AdjustedMetrics, DrawError] {
if settings.hinting_instance() is Some(h) {
let (metrics, path) = match
h.hinted_path(self, settings.is_pedantic(), settings.path_style()) {
Err(e) => return Err(e)
Ok(v) => v
}
for e in path.iter() {
match e {
MoveTo(x, y) => OutlinePen::move_to(pen, x, y)
LineTo(x, y) => OutlinePen::line_to(pen, x, y)
QuadTo(cx0, cy0, x, y) => OutlinePen::quad_to(pen, cx0, cy0, x, y)
CurveTo(cx0, cy0, cx1, cy1, x, y) =>
OutlinePen::curve_to(pen, cx0, cy0, cx1, cy1, x, y)
Close => OutlinePen::close(pen)
}
}
return Ok(metrics)
}
let metrics0 = match self.kind {
Glyf => {
let has_overlaps = self.has_overlaps().unwrap_or(false)
let gm = @moon_skrifa.FontRef::glyph_metrics(
self.font,
settings.size(),
settings.location(),
)
let lsb0 = gm.left_side_bearing(self.glyph_id)
let advance_width0 = gm.advance_width(self.glyph_id)
let (lsb, advance_width) = if settings.path_style() is FreeType {
(
match lsb0 {
None => None
Some(v) => Some(outline_quantize_freetype(v))
},
match advance_width0 {
None => None
Some(v) => Some(outline_quantize_freetype(v))
},
)
} else {
(lsb0, advance_width0)
}
AdjustedMetrics::{ has_overlaps, lsb, advance_width }
}
_ => AdjustedMetrics::default()
}
let metrics = metrics0
let path = match self.path(settings) {
Err(e) => return Err(e)
Ok(p) => p
}
for e in path.iter() {
match e {
MoveTo(x, y) => OutlinePen::move_to(pen, x, y)
LineTo(x, y) => OutlinePen::line_to(pen, x, y)
QuadTo(cx0, cy0, x, y) => OutlinePen::quad_to(pen, cx0, cy0, x, y)
CurveTo(cx0, cy0, cx1, cy1, x, y) =>
OutlinePen::curve_to(pen, cx0, cy0, cx1, cy1, x, y)
Close => OutlinePen::close(pen)
}
}
Ok(metrics)
}
///|
/// Draws the outline glyph and returns a flat list of path elements.
pub fn OutlineGlyph::path(
self : OutlineGlyph,
settings : DrawSettings,
) -> Result[Array[PathElement], DrawError] {
if settings.hinting_instance() is Some(h) {
let (_, path) = match
h.hinted_path(self, settings.is_pedantic(), settings.path_style()) {
Err(e) => return Err(e)
Ok(v) => v
}
Ok(path)
} else {
let upem = outline_units_per_em(self.font)
let scale = settings.size().linear_scale(upem)
let path_style = settings.path_style()
let coords = settings.location().effective_coords()
match self.kind {
Glyf =>
match
glyf_outline_path(
self.font,
self.glyph_id,
coords,
path_style,
settings.size(),
false,
) {
None => Err(GlyphNotFound(self.glyph_id))
Some(p) => Ok(p)
}
Cff =>
match cff_outline_path(self.font, self.glyph_id) {
None => Err(GlyphNotFound(self.glyph_id))
// Upstream uses fixed-point rounding when scaling PostScript outlines.
Some(p) => Ok(outline_scale_path(p, scale))
}
Cff2 =>
match cff2_outline_path(self.font, self.glyph_id, coords) {
None => Err(GlyphNotFound(self.glyph_id))
Some(p) => Ok(outline_scale_path(p, scale))
}
}
}
}
///|
pub fn OutlineGlyphCollection::format(
self : OutlineGlyphCollection,
) -> OutlineGlyphFormat? {
self.kind
}
///|
/// Returns the outline for the given glyph identifier.
pub fn OutlineGlyphCollection::get(
self : OutlineGlyphCollection,
gid : @moon_skrifa.GlyphId,
) -> OutlineGlyph? {
match self.kind {
None => None
Some(kind) => {
let idx = gid.to_uint64().to_int()
let n = self.glyph_count()
if idx < 0 || idx >= n {
None
} else {
Some({ font: self.font, glyph_id: gid, kind })
}
}
}
}
///|
/// Returns an iterator-like list of all outline glyphs in the collection.
pub fn OutlineGlyphCollection::iter(
self : OutlineGlyphCollection,
) -> Array[(@moon_skrifa.GlyphId, OutlineGlyph)] {
let out : Array[(@moon_skrifa.GlyphId, OutlineGlyph)] = Array::new()
let n = self.glyph_count()
for i in 0.. ()
Some(g) => out.push((gid, g))
}
}
out
}
///|
/// Returns the number of glyphs addressable by this outline source.
pub fn OutlineGlyphCollection::glyph_count(
self : OutlineGlyphCollection,
) -> Int {
match self.kind {
None => 0
Some(Glyf) => glyf_glyph_count(self.font).unwrap_or(0)
Some(Cff) => cff_glyph_count(self.font).unwrap_or(0)
Some(Cff2) => cff2_glyph_count(self.font).unwrap_or(0)
}
}
///|
/// Returns true when the interpreter engine must be used for hinting this set
/// of outlines to produce correct results.
pub fn OutlineGlyphCollection::require_interpreter(
self : OutlineGlyphCollection,
) -> Bool {
match self.kind {
None => false
_ => require_interpreter(self.font)
}
}
///|
/// Returns true when the font supports hinting at fractional sizes.
pub fn OutlineGlyphCollection::fractional_size_hinting(
self : OutlineGlyphCollection,
) -> Bool {
match self.kind {
Some(Glyf) =>
// head.flags bit 3 (0x0008) == FORCE_INTEGER_PPEM
match self.font.table(TAG_HEAD) {
None => true
Some(head) =>
if outline_read_u16_be(head, 16) is Some(flags) {
(flags & 0x0008) == 0
} else {
true
}
}
_ => true
}
}
///|
fn table_is_non_empty(font : @moon_skrifa.FontRef, tag : UInt) -> Bool {
match font.table(tag) {
None => false
Some(view) => view.length() > 0
}
}
///|
/// Returns true if FreeType-style logic would prefer running the TrueType
/// instruction interpreter.
///
/// Upstream computes this using:
/// `!(max_instructions == 0 && fpgm.is_empty() && prep.is_empty())`.
fn glyf_prefer_interpreter(font : @moon_skrifa.FontRef) -> Bool {
let max_instructions = match font.table(TAG_MAXP) {
None => 0
// maxp.max_size_of_instructions() is a u16 at offset 26 in the v1.0 table.
Some(maxp) => outline_read_u16_be(maxp, 26).unwrap_or(0)
}
let fpgm_empty = !table_is_non_empty(font, TAG_FPGM)
let prep_empty = !table_is_non_empty(font, TAG_PREP)
!(max_instructions == 0 && fpgm_empty && prep_empty)
}
///|
/// Returns `true` if the embedded instruction interpreter is preferred.
///
/// This matches the high-level selection logic used by upstream:
/// - CFF/CFF2 prefers the (PostScript) interpreter
/// - glyf follows FreeType-style preference rules based on max instructions
/// and the presence of `fpgm`/`prep`
pub fn OutlineGlyphCollection::prefer_interpreter(
self : OutlineGlyphCollection,
) -> Bool {
match self.format() {
Some(Glyf) => glyf_prefer_interpreter(self.font)
// Upstream prefers the interpreter for all non-glyf outline formats.
_ => true
}
}
///|
/// Single path command emitted when drawing a glyph outline.
///
/// Coordinate units are raw font units (design units).
pub(all) enum PathElement {
MoveTo(Double, Double)
LineTo(Double, Double)
QuadTo(Double, Double, Double, Double)
CurveTo(Double, Double, Double, Double, Double, Double)
Close
}
///|
/// Reads a big-endian u16 from a BytesView.
fn outline_read_u16_be(view : BytesView, offset : Int) -> Int? {
if offset < 0 || offset + 2 > view.length() {
None
} else {
let b0 = view.at(offset).to_int()
let b1 = view.at(offset + 1).to_int()
Some((b0 << 8) | b1)
}
}
///|
fn outline_units_per_em(font : @moon_skrifa.FontRef) -> UInt16 {
let mut upem : UInt16 = 0
match font.table(TAG_HEAD) {
None => ()
Some(head) => {
let v = outline_read_u16_be(head, 18).unwrap_or(0)
upem = v.to_uint16()
}
}
upem
}
///|
///|
fn outline_round_ties_even_pos(x : Double) -> Int {
let i = x.to_int()
let frac = x - i.to_double()
let diff = frac - 0.5
if diff.abs() <= 0.000000000001 {
if i % 2 == 0 {
i
} else {
i + 1
}
} else if frac < 0.5 {
i
} else {
i + 1
}
}
///|
fn outline_quantize_freetype(v : Double) -> Double {
let sign = if v < 0.0 { -1.0 } else { 1.0 }
let scaled = v.abs() * 64.0
sign * outline_round_ties_even_pos(scaled).to_double() / 64.0
}
///|
///|
fn outline_scale_path(
path : Array[PathElement],
scale : Double,
) -> Array[PathElement] {
if scale == 1.0 {
return path
}
let out : Array[PathElement] = Array::new()
for e in path.iter() {
match e {
MoveTo(x, y) =>
out.push(
MoveTo(
outline_quantize_freetype(x * scale),
outline_quantize_freetype(y * scale),
),
)
LineTo(x, y) =>
out.push(
LineTo(
outline_quantize_freetype(x * scale),
outline_quantize_freetype(y * scale),
),
)
QuadTo(cx0, cy0, x, y) =>
out.push(
QuadTo(
outline_quantize_freetype(cx0 * scale),
outline_quantize_freetype(cy0 * scale),
outline_quantize_freetype(x * scale),
outline_quantize_freetype(y * scale),
),
)
CurveTo(cx0, cy0, cx1, cy1, x, y) =>
out.push(
CurveTo(
outline_quantize_freetype(cx0 * scale),
outline_quantize_freetype(cy0 * scale),
outline_quantize_freetype(cx1 * scale),
outline_quantize_freetype(cy1 * scale),
outline_quantize_freetype(x * scale),
outline_quantize_freetype(y * scale),
),
)
Close => out.push(Close)
}
}
out
}
///|
/// Draws the outline of a glyph and returns a flat list of path elements.
pub fn OutlineGlyphCollection::path(
self : OutlineGlyphCollection,
gid : @moon_skrifa.GlyphId,
settings : DrawSettings,
) -> Array[PathElement]? {
match self.get(gid) {
None => None
Some(g) =>
match g.path(settings) {
Err(_) => None
Ok(p) => Some(p)
}
}
}
///|
/// Convenience wrapper for the previous unscaled API.
pub fn OutlineGlyphCollection::path_unscaled(
self : OutlineGlyphCollection,
gid : @moon_skrifa.GlyphId,
) -> Array[PathElement]? {
self.path(gid, DrawSettings::default())
}
///|
/// Draws the outline of a glyph into a pen.
pub fn[P : OutlinePen] OutlineGlyphCollection::draw(
self : OutlineGlyphCollection,
gid : @moon_skrifa.GlyphId,
settings : DrawSettings,
pen : P,
) -> Bool {
match self.get(gid) {
None => false
Some(g) =>
match g.draw(settings, pen) {
Err(_) => false
Ok(_) => true
}
}
}
///|
/// Convenience wrapper for the previous unscaled API.
pub fn[P : OutlinePen] OutlineGlyphCollection::draw_unscaled(
self : OutlineGlyphCollection,
gid : @moon_skrifa.GlyphId,
pen : P,
) -> Bool {
self.draw(gid, DrawSettings::default(), pen)
}
///|
/// Draws the outline of a glyph into an SVG path string.
pub fn OutlineGlyphCollection::svg_path(
self : OutlineGlyphCollection,
gid : @moon_skrifa.GlyphId,
settings : DrawSettings,
) -> String? {
let pen = SvgPen()
if self.draw(gid, settings, pen) {
Some(pen.to_string())
} else {
None
}
}
///|
pub fn OutlineGlyphCollection::svg_path_unscaled(
self : OutlineGlyphCollection,
gid : @moon_skrifa.GlyphId,
) -> String? {
self.svg_path(gid, DrawSettings::default())
}