///|
// Deterministic font metrics for the layout engine. The bundled faces are
// parsed from the metrics-only sfnt data in data_*.mbt (regenerate with
// tools/font-metrics/gen_font_metrics.py); their glyph outlines, which only
// the PDF backend needs, live apart in `pagelayout/fontoutlines`.
//
// A caller's own faces come in through a `FontRegistry` (registry.mbt), an
// explicit value shared by whatever measures and whatever renders. The
// package-level functions here (`face`, `resolve_family`, `cjk_fallback`,
// `uncovered`) only ever see the bundled faces. The one process-wide state is
// the lazily filled cache of those bundled faces, which is written once, from
// constant data, and never replaced.
///|
/// Metrics of one face, bundled or registered. For a face parsed from an
/// sfnt the vertical metrics come from `hhea`; one built with
/// `FaceMetrics::new` carries what it was given (an AFM's, say). Advances
/// are raw font units scaled by `units_per_em` on lookup.
///
/// A face is immutable. Its glyph tables are private and read through
/// `glyph_id`, `cmap_entries` and `advance_pt`, because the same value is
/// shared by every lookup — the bundled cache, a `FontRegistry`, the layout
/// that measured with it and the renderer that draws with it — and a
/// change made through one of them would silently reach all the others.
pub struct FaceMetrics {
family : String
bold : Bool
italic : Bool
units_per_em : Int
ascender : Int
descender : Int
line_gap : Int
priv advances : Array[Int]
priv cmap : Map[Int, Int]
}
///|
/// Why `FaceMetrics::new` refused its arguments.
pub(all) suberror FaceMetricsError {
/// The metrics break an invariant every face keeps (see
/// `FaceMetrics::new`). The payload names the family and the fault.
InvalidFaceMetrics(String)
} derive(Eq)
///|
pub extend FaceMetricsError with Eq::{not_equal, equal}
///|
pub extend FaceMetricsError with Show::{to_string, output}
///|
pub impl Show for FaceMetricsError with fn output(self, logger) {
match self {
InvalidFaceMetrics(message) =>
logger.write_string("invalid face metrics for \{message}")
}
}
///|
/// Metrics given outright rather than parsed from an sfnt: for a face whose
/// metrics come from elsewhere, such as the AFM metrics of a PDF standard
/// font (see `FontRegistry::register_standard`). `advances` are indexed by
/// glyph, in units of `units_per_em`; `cmap` maps codepoints to glyphs.
/// Both tables are copied, so changing them afterwards cannot reach the
/// face.
///
/// Preconditions, checked (raising `FaceMetricsError` when one fails), so
/// that every measurement of the face is a finite number and no lookup can
/// fall outside a table:
///
/// - `units_per_em` is positive;
/// - `advances` is not empty, and no advance is negative;
/// - every `cmap` key is a codepoint (0 to 0x10FFFF), and every glyph it
/// maps to indexes `advances` (0 ≤ glyph < `advances.length()`).
///
/// A character the `cmap` lacks measures as glyph 0 (`.notdef`), as in any
/// face; `has_char` reports it missing. The vertical metrics are taken as
/// given (font units, `descender` negative below the baseline).
pub fn FaceMetrics::new(
family~ : String,
bold? : Bool = false,
italic? : Bool = false,
units_per_em~ : Int,
ascender~ : Int,
descender~ : Int,
line_gap~ : Int,
advances~ : Array[Int],
cmap~ : Map[Int, Int],
) -> FaceMetrics raise FaceMetricsError {
let invalid = fn(fault : String) -> FaceMetricsError {
InvalidFaceMetrics("\{family}: \{fault}")
}
if units_per_em <= 0 {
raise invalid("units_per_em \{units_per_em} is not positive")
}
if advances.is_empty() {
raise invalid("no advances")
}
for glyph, advance in advances {
if advance < 0 {
raise invalid("glyph \{glyph} advances by \{advance}")
}
}
for codepoint, glyph in cmap {
if codepoint < 0 || codepoint > 0x10FFFF {
raise invalid("\{codepoint} is not a codepoint")
}
if glyph < 0 || glyph >= advances.length() {
raise invalid(
"\{codepoint_label(codepoint)} maps to glyph \{glyph}, outside the \{advances.length()} advances",
)
}
}
{
family,
bold,
italic,
units_per_em,
ascender,
descender,
line_gap,
advances: advances.copy(),
cmap: cmap.copy(),
}
}
///|
/// `U+0041` and the like.
fn codepoint_label(codepoint : Int) -> String {
"U+" + codepoint.to_string(radix=16).to_upper().pad_start(4, '0')
}
///|
/// The glyph `codepoint` maps to in the face's `cmap`, or None when it maps
/// to none. A mapping to glyph 0 (`.notdef`) is reported as it is; see
/// `has_char` for whether the face can really draw the character.
pub fn FaceMetrics::glyph_id(self : FaceMetrics, codepoint : Int) -> Int? {
self.cmap.get(codepoint)
}
///|
/// Every (codepoint, glyph) pair of the face's `cmap`, in no particular
/// order. The pairs are read from the face; nothing done with them can
/// reach it.
pub fn FaceMetrics::cmap_entries(self : FaceMetrics) -> Iter2[Int, Int] {
self.cmap.iter2()
}
///|
/// Horizontal advance of `codepoint` at `size_pt`, in points. Codepoints
/// missing from the face use the `.notdef` (glyph 0) advance; glyphs past
/// `numberOfHMetrics` share the table's final advance, per TrueType.
pub fn FaceMetrics::advance_pt(
self : FaceMetrics,
codepoint : Int,
size_pt : Double,
) -> Double {
let glyph = self.cmap.get(codepoint).unwrap_or(0)
let index = if glyph >= self.advances.length() {
self.advances.length() - 1
} else {
glyph
}
self.advances[index].to_double() * size_pt / self.units_per_em.to_double()
}
///|
/// Whether the face maps `codepoint` to a real glyph.
pub fn FaceMetrics::has_char(self : FaceMetrics, codepoint : Int) -> Bool {
self.cmap.get(codepoint) is Some(glyph) && glyph != 0
}
///|
/// Baseline-to-top extent at `size_pt`, in points (positive).
pub fn FaceMetrics::ascent_pt(self : FaceMetrics, size_pt : Double) -> Double {
self.ascender.to_double() * size_pt / self.units_per_em.to_double()
}
///|
/// Baseline-to-bottom extent at `size_pt`, in points (negative, per hhea).
pub fn FaceMetrics::descent_pt(self : FaceMetrics, size_pt : Double) -> Double {
self.descender.to_double() * size_pt / self.units_per_em.to_double()
}
///|
/// Single line spacing at `size_pt`: ascender − descender + line gap.
pub fn FaceMetrics::line_height_pt(
self : FaceMetrics,
size_pt : Double,
) -> Double {
(self.ascender - self.descender + self.line_gap).to_double() *
size_pt /
self.units_per_em.to_double()
}
///|
fn parse_face(
family : String,
bold : Bool,
italic : Bool,
chunks : Array[Bytes],
) -> FaceMetrics raise {
let joined = Buffer()
for chunk in chunks {
joined.write_bytes(chunk)
}
let data : Bytes = @flate.pdf_flate_decode_view(joined.contents())
parse_sfnt(family, bold, italic, data)
}
///|
fn parse_sfnt(
family : String,
bold : Bool,
italic : Bool,
data : Bytes,
) -> FaceMetrics raise {
let metrics = @pdflite.pdf_truetype_metrics(data)
let cmap : Map[Int, Int] = Map([])
// later subtables replace earlier mappings, matching cpdf's reader
for entry in @pdflite.pdf_truetype_cmap_glyphs(data) {
cmap[entry.codepoint] = entry.glyph_index
}
guard @pdflite.pdf_truetype_table(data, "hhea") is Some(hhea) else {
fail("face \{family} lacks an hhea table")
}
let ascender = read_i16(data, hhea.offset + 4)
let descender = read_i16(data, hhea.offset + 6)
let line_gap = read_i16(data, hhea.offset + 8)
{
family,
bold,
italic,
units_per_em: metrics.units_per_em,
ascender,
descender,
line_gap,
advances: metrics.advance_widths,
cmap,
}
}
///|
fn read_i16(data : Bytes, offset : Int) -> Int {
let hi = data[offset].to_int()
let lo = data[offset + 1].to_int()
let value = (hi << 8) | lo
if value >= 0x8000 {
value - 0x10000
} else {
value
}
}
///|
let face_cache : Map[String, FaceMetrics] = Map([])
///|
fn face_key(family : String, bold : Bool, italic : Bool) -> String {
"\{family}|\{bold}|\{italic}"
}
///|
fn load_faces() -> Unit {
if face_cache.is_empty() {
for entry in bundled_faces() {
let (family, bold, italic, chunks) = entry
let face = try! parse_face(family, bold, italic, chunks)
face_cache[face_key(family, bold, italic)] = face
}
}
}
///|
/// Every bundled family, in registry order (deduplicated).
pub fn bundled_families() -> Array[String] {
load_faces()
let seen : Map[String, Unit] = Map([])
let families : Array[String] = []
for entry in bundled_faces() {
let (family, _, _, _) = entry
if !seen.contains(family) {
seen[family] = ()
families.push(family)
}
}
families
}
///|
/// The bundled face for a *resolved* family name (see `resolve_family`).
/// Falls back to the family's regular face when the exact bold/italic face
/// is not bundled (Noto Sans SC ships regular only), then to Carlito. The
/// face returned says which one it is (`family`, `bold`, `italic`), so a
/// caller that needs the matching font program can ask for exactly that
/// face. For registered faces see `FontRegistry::face`.
pub fn face(
family : String,
bold? : Bool = false,
italic? : Bool = false,
) -> FaceMetrics {
load_faces()
match face_cache.get(face_key(family, bold, italic)) {
Some(found) => found
None =>
match face_cache.get(face_key(family, false, false)) {
Some(regular) => regular
None =>
match face_cache.get(face_key("Carlito", bold, italic)) {
Some(default_face) => default_face
None => try! fail("bundled font registry is empty")
}
}
}
}
///|
/// The face used for CJK codepoints the Latin faces lack.
pub fn cjk_fallback() -> FaceMetrics {
face("Noto Sans SC")
}
///|
/// Codepoints in `text` that the face this run is set in cannot draw,
/// counted.
///
/// This asks the same question layout asks, and in the same order:
/// the requested face, then the CJK fallback. Asking the whole bundle
/// instead would under-report, because layout never reaches a third
/// family — U+05D0 lives in Liberation but not in Carlito or Noto Sans
/// SC, so Hebrew in a Carlito run is dropped even though *some* bundled
/// face has the glyph.
///
/// A character neither face covers is measured with `.notdef` and then
/// dropped by the backends: a simple font cannot encode it, and a
/// composite font would draw a blank box at the wrong width. That is a
/// silent loss worth reporting — a document whose evidence ratings or
/// equation variables are missing still renders, still validates, and is
/// still wrong.
pub fn uncovered(
text : String,
family : String,
bold? : Bool = false,
italic? : Bool = false,
) -> Array[(Int, Int)] {
load_faces()
uncovered_by(
text,
face(resolve_family(family), bold~, italic~),
cjk_fallback(),
)
}
///|
/// Codepoints in `text` that neither `requested` nor `fallback` can draw,
/// counted, in order of first appearance.
fn uncovered_by(
text : String,
requested : FaceMetrics,
fallback : FaceMetrics,
) -> Array[(Int, Int)] {
let counts : Map[Int, Int] = Map([])
let order : Array[Int] = []
let mut i = 0
while i < text.length() {
let unit = text[i].to_int()
// decode surrogate pairs so astral characters report their real
// codepoint rather than two halves
let codepoint = if unit >= 0xD800 && unit <= 0xDBFF && i + 1 < text.length() {
let low = text[i + 1].to_int()
if low >= 0xDC00 && low <= 0xDFFF {
i = i + 1
0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00)
} else {
unit
}
} else {
unit
}
i = i + 1
// control characters are layout instructions, not glyphs
if codepoint < 0x20 {
continue
}
match counts.get(codepoint) {
Some(n) => counts[codepoint] = n + 1
None =>
if !requested.has_char(codepoint) && !fallback.has_char(codepoint) {
counts[codepoint] = 1
order.push(codepoint)
}
}
}
[..order.map(fn(cp) { (cp, counts.get(cp).unwrap_or(0)) })]
}