///|
/// Flattened outline of one laid-out text run, in user-space coordinates.
pub(all) struct TextRunOutline {
/// Total advance width of the laid-out run in user units.
width : Double
/// Closed polygons for the run, outer contours and holes alike. Windings
/// come straight from the font, so a nonzero fill rule preserves holes.
polygons : Array[Array[(Double, Double)]]
} derive(Debug)
///|
pub extend TextRunOutline with Debug::{to_repr}
///|
/// Flatten every glyph contour of `text` laid out with `font_size` so that the
/// run starts at `origin_x` and sits on the baseline `baseline_y`. Composite
/// glyphs fall back to nothing (matching the rasterizer), so callers pairing
/// this with a bitmap fallback keep full coverage.
pub fn FontFace::text_run_outline(
self : FontFace,
text : String,
font_size : Double,
origin_x : Double,
baseline_y : Double,
target_length? : Double,
) -> TextRunOutline {
let layout = layout_text_face(
self,
text,
LayoutConfig::new(font_size~, alignment=Left),
)
let font = self.get_font()
let upem = self.get_units_per_em().to_double()
let scale = if upem > 0.0 { font_size / upem } else { 1.0 }
// SVG textLength="spacing" semantics: advances compress (or expand) so the
// run spans exactly the target length while glyph shapes stay unscaled.
let length_factor = match target_length {
Some(target) =>
if target > 0.0 && layout.get_width() > 0.0 {
target / layout.get_width()
} else {
1.0
}
None => 1.0
}
let polygons : Array[Array[(Double, Double)]] = []
// Layout positions runs below a top-of-box origin; re-anchor them so the
// caller's y is the text baseline itself.
let layout_baseline = layout.get_baseline()
for run in layout.get_runs() {
let run_baseline = baseline_y + (run.get_baseline_y() - layout_baseline)
for glyph in run.get_glyphs() {
let pen_x = origin_x + glyph.x * length_factor
match font.get_glyph(glyph.glyph_id) {
Simple(simple) =>
for contour in simple.contours {
let polygon = flatten_contour_polygon(
contour, pen_x, run_baseline, scale,
)
if polygon.length() >= 3 {
polygons.push(polygon)
}
}
_ => ()
}
}
}
{
width: match target_length {
Some(target) if target > 0.0 => target
_ => layout.get_width()
},
polygons,
}
}
///|
/// Flatten one TrueType contour into an ordered closed polygon in user space.
/// The contour ring is walked from its first on-curve point; quadratic
/// segments subdivide into a bounded number of chords and close back to the
/// anchor point, including the implicit-endpoint case where the ring ends on
/// an off-curve control point.
fn flatten_contour_polygon(
contour : GlyphContour,
origin_x : Double,
baseline_y : Double,
scale : Double,
) -> Array[(Double, Double)] {
let out : Array[(Double, Double)] = []
let count = contour.points.length()
if count < 2 {
return out
}
let points = contour.points
// Anchor the walk on the first on-curve point; a contour made entirely of
// off-curve points is promoted by inserting implicit on-curve midpoints.
let mut start = -1
for i in 0.. Unit {
out.push(
(origin_x + x.to_double() * scale, baseline_y - y.to_double() * scale),
)
}
/// Subdivide the quadratic with control point `ctrl` and on-curve end `end`,
/// emitting interior points; the caller emits `end` itself.
fn quad_steps(from : GlyphPoint, ctrl : GlyphPoint, end : GlyphPoint) -> Int {
let dx = (from.x - ctrl.x).to_double().abs() +
(ctrl.x - end.x).to_double().abs()
let dy = (from.y - ctrl.y).to_double().abs() +
(ctrl.y - end.y).to_double().abs()
let extent = if dx > dy { dx } else { dy }
let steps = (extent * 0.25).ceil().to_int()
if steps < 2 {
2
} else if steps > 12 {
12
} else {
steps
}
}
fn emit_quad(ctrl : GlyphPoint, end : GlyphPoint, from : GlyphPoint) -> Unit {
let steps = quad_steps(from, ctrl, end)
for s = 1; s < steps; s = s + 1 {
let t = s.to_double() / steps.to_double()
let inv = 1.0 - t
let x = inv * inv * from.x.to_double() +
2.0 * inv * t * ctrl.x.to_double() +
t * t * end.x.to_double()
let y = inv * inv * from.y.to_double() +
2.0 * inv * t * ctrl.y.to_double() +
t * t * end.y.to_double()
out.push((origin_x + x * scale, baseline_y - y * scale))
}
}
fn midpoint(a : GlyphPoint, b : GlyphPoint) -> GlyphPoint {
{ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, on_curve: true, }
}
let anchor = points[start]
emit(anchor.x, anchor.y)
let mut pending : GlyphPoint? = None
let mut previous = anchor
for step = 1; step <= count; step = step + 1 {
let point = points[(start + step) % count]
let is_anchor_wrap = step == count
match pending {
Some(ctrl) =>
if is_anchor_wrap {
emit_quad(ctrl, anchor, previous)
pending = None
} else if point.on_curve {
emit_quad(ctrl, point, previous)
emit(point.x, point.y)
pending = None
previous = point
} else {
let mid = midpoint(ctrl, point)
emit_quad(ctrl, mid, previous)
emit(mid.x, mid.y)
pending = Some(point)
previous = mid
}
None =>
if is_anchor_wrap {
// The ring closes with an explicit segment back to the anchor.
emit(point.x, point.y)
} else if point.on_curve {
emit(point.x, point.y)
previous = point
} else {
pending = Some(point)
}
}
}
out
}