///|
// The page-model IR: the layout engine's output and the backends' only
// input. Pages hold positioned items in painter's order; coordinates are
// points, y-down, origin at the page's top-left corner; text is positioned
// by its baseline. Glyph runs carry their own advances so backends never
// re-measure text.
//
// Derived `Eq` on these types is exact structural (IEEE) equality, meant
// for snapshot tests on models built from the same code path; tolerance-
// aware comparison of rendered output belongs to the svgdiff golden layer.
// Structural invariants the types cannot express are checked by
// `PageModel::validate` (validate.mbt).
///|
/// A font face a glyph run references, by index into `PageModel::fonts`.
/// `family` is the resolved (post font-mapping) family name; `bold` and
/// `italic` select the face within it.
pub(all) struct FontSpec {
family : String
bold : Bool
italic : Bool
} derive(Eq, Debug, ToJson)
///|
/// Solid sRGB color; each channel is 0..=255 (checked by
/// `PageModel::validate`, clamped on entry by `Color::rgb`). No alpha in
/// v1: solid colors keep the SVG output inside svgdiff's deterministic
/// subset and match common document text.
pub(all) struct Color {
r : Int
g : Int
b : Int
} derive(Eq, Debug, ToJson)
///|
/// Default document text color.
pub let black : Color = { r: 0, g: 0, b: 0, }
///|
/// Build a color, clamping each channel into 0..=255. Frontends convert
/// untrusted document values through this so every encoding of the model
/// (Eq, JSON, hex) agrees.
pub fn Color::rgb(r : Int, g : Int, b : Int) -> Color {
fn clamp(v : Int) -> Int {
if v < 0 {
0
} else if v > 255 {
255
} else {
v
}
}
{ r: clamp(r), g: clamp(g), b: clamp(b), }
}
///|
/// Lowercase `#rrggbb`, the form both the SVG emitter and tests use.
/// Channels are formatted modulo the valid range (see `Color::rgb`).
pub fn Color::to_hex(self : Color) -> String {
let clamped = Color::rgb(self.r, self.g, self.b)
let packed = (clamped.r << 16) | (clamped.g << 8) | clamped.b
"#" + packed.to_string(radix=16).pad_start(6, '0')
}
///|
/// A run of text on one baseline in one font/size/color. `advances_pt[i]`
/// is the horizontal advance of the char at index `i` of `text` (UTF-16
/// code units; the trailing surrogate of a pair carries advance 0).
pub(all) struct GlyphRun {
font : Int
size_pt : Double
x_pt : Double
baseline_pt : Double
text : String
advances_pt : Array[Double]
color : Color
} derive(Eq, Debug, ToJson)
///|
/// Total advance of the run: where the next run on the same baseline starts.
pub fn GlyphRun::width_pt(self : GlyphRun) -> Double {
let mut w = 0.0
for a in self.advances_pt {
w += a
}
w
}
///|
/// Axis-aligned filled and/or stroked rectangle. Doubles as underlines,
/// table borders, and shading (thin or filled).
pub(all) struct RectItem {
x_pt : Double
y_pt : Double
w_pt : Double
h_pt : Double
fill : Color?
stroke : Color?
stroke_w_pt : Double
} derive(Eq, Debug, ToJson)
///|
/// A placed raster image; `data` is the encoded file, not pixels.
pub(all) struct ImageItem {
x_pt : Double
y_pt : Double
w_pt : Double
h_pt : Double
data : Bytes
mime : String
} derive(Eq, Debug, ToJson)
///|
/// An interactive region (hyperlink); geometry only, painted by nothing.
pub(all) struct LinkRegion {
x_pt : Double
y_pt : Double
w_pt : Double
h_pt : Double
target : String
} derive(Eq, Debug, ToJson)
///|
/// One positioned item on a page, in painter's order.
pub(all) enum PageItem {
Text(GlyphRun)
Rect(RectItem)
Image(ImageItem)
Link(LinkRegion)
} derive(Eq, Debug, ToJson)
///|
/// A single laid-out page: physical size plus its positioned items.
pub(all) struct Page {
width_pt : Double
height_pt : Double
items : Array[PageItem]
} derive(Eq, Debug, ToJson)
///|
/// The layout engine's complete output: interned fonts plus pages.
pub(all) struct PageModel {
fonts : Array[FontSpec]
pages : Array[Page]
} derive(Eq, Debug, ToJson)
///|
/// Intern `spec`, returning its index for `GlyphRun::font`.
pub fn PageModel::add_font(self : PageModel, spec : FontSpec) -> Int {
for i, existing in self.fonts {
if existing == spec {
return i
}
}
self.fonts.push(spec)
self.fonts.length() - 1
}
///|
/// An empty model to accumulate fonts and pages into.
pub fn PageModel::new() -> PageModel {
{ fonts: [], pages: [], }
}