///|
/// A subpixel rectangle. In text layout, units are pixels relative to the
/// baseline origin, with positive y down.
#derive(ToJson)
pub(all) struct TextRect {
x : Double
y : Double
width : Double
height : Double
}
///|
pub extend TextRect with ToJson::{to_json}
///|
/// A positioned glyph with a half-open UTF-16 range into the source text.
/// Whitespace has an advance but no ink rectangle.
pub(all) struct PositionedGlyphBounds {
codepoint : Int32
glyph_id : Int32
text_start : Int32
text_end : Int32
x : Double
y : Double
advance_width : Double
ink_bounds : TextRect?
}
///|
pub impl ToJson for PositionedGlyphBounds with fn to_json(self) {
{
"codepoint": self.codepoint,
"glyph_id": self.glyph_id,
"text_start": self.text_start,
"text_end": self.text_end,
"x": self.x,
"y": self.y,
"advance_width": self.advance_width,
"ink_bounds": rect_to_json(self.ink_bounds),
}
}
///|
pub extend PositionedGlyphBounds with ToJson::{to_json}
///|
/// Horizontal text metrics. Logical bounds use hhea ascent and descent; ink
/// bounds are the union of positioned glyph bounds, excluding whitespace.
/// For shaped clusters use shape_text; layout_text_bounds is basic LTR layout.
pub(all) struct TextBounds {
advance_width : Double
ascent : Double
descent : Double
line_gap : Double
logical_bounds : TextRect
ink_bounds : TextRect?
glyphs : Array[PositionedGlyphBounds]
}
///|
pub impl ToJson for TextBounds with fn to_json(self) {
{
"advance_width": self.advance_width,
"ascent": self.ascent,
"descent": self.descent,
"line_gap": self.line_gap,
"logical_bounds": self.logical_bounds,
"ink_bounds": rect_to_json(self.ink_bounds),
"glyphs": self.glyphs,
}
}
///|
pub extend TextBounds with ToJson::{to_json}
///|
fn rect_to_json(rect : TextRect?) -> Json {
match rect {
Some(rect) => rect.to_json()
None => null
}
}
///|
fn TextRect::union(self : TextRect, other : TextRect) -> TextRect {
let x = self.x.min(other.x)
let y = self.y.min(other.y)
let right = (self.x + self.width).max(other.x + other.width)
let bottom = (self.y + self.height).max(other.y + other.height)
{ x, y, width: right - x, height: bottom - y, }
}
///|
fn positioned_ink_bounds(
font : TTFont,
gid : Int32,
x : Double,
y : Double,
scale : Double,
) -> TextRect? {
positioned_ink_bounds_at(font, gid, x, y, scale, Map([]))
}
///|
fn positioned_ink_bounds_at(
font : TTFont,
gid : Int32,
x : Double,
y : Double,
scale : Double,
axes : Map[String, Double],
) -> TextRect? {
let outline = if axes.is_empty() {
font.glyph_outline(gid)
} else {
font.glyph_outline_at(gid, axes)
}
match compute_path_bounds(outline) {
Some(bounds) if bounds.width > 0.0 && bounds.height > 0.0 && scale > 0.0 =>
Some({
x: x + bounds.x * scale,
y: y - (bounds.y + bounds.height) * scale,
width: bounds.width * scale,
height: bounds.height * scale,
})
_ => None
}
}
///|
fn text_bounds_from_glyphs(
font : TTFont,
glyphs : Array[PositionedGlyphBounds],
advance_width : Double,
scale : Double,
) -> TextBounds {
let mut ink_bounds : TextRect? = None
for glyph in glyphs {
if glyph.ink_bounds is Some(ink) {
ink_bounds = Some(
match ink_bounds {
None => ink
Some(previous) => previous.union(ink)
},
)
}
}
let ascent = font.ascent.to_double() * scale
let descent = -font.descent.to_double() * scale
{
advance_width,
ascent,
descent,
line_gap: font.line_gap.to_double() * scale,
logical_bounds: {
x: 0.0,
y: -ascent,
width: advance_width,
height: ascent + descent,
},
ink_bounds,
glyphs,
}
}
///|
/// Measure a single run, preserving subpixel precision. `font_size` must be
/// finite and nonnegative. Bounds are baseline-relative, in pixels (y down).
pub fn TTFont::layout_text_bounds(
self : TTFont,
text : String,
font_size : Double,
) -> TextBounds {
let scale = font_size / self.units_per_em.to_double()
let glyphs : Array[PositionedGlyphBounds] = []
let mut text_start = 0
let mut advance_width = 0.0
for pos in self.layout_text(text, font_size) {
let text_end = text_start + (if pos.codepoint > 0xFFFF { 2 } else { 1 })
glyphs.push({
codepoint: pos.codepoint,
glyph_id: pos.glyph_id,
text_start,
text_end,
x: pos.x_offset,
y: pos.y_offset,
advance_width: pos.x_advance,
ink_bounds: positioned_ink_bounds(
self,
pos.glyph_id,
pos.x_offset,
pos.y_offset,
scale,
),
})
text_start = text_end
advance_width = pos.x_offset + pos.x_advance
}
text_bounds_from_glyphs(self, glyphs, advance_width, scale)
}