mos_layout/types.rs
1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use mos_core::Diagnostic;
5use mos_fonts::{Font, FontFamily, ShapedGlyph};
6
7/// A4 page width in PDF points (1pt = 1/72 inch). Kept as a public
8/// constant so external callers can still read the default; the layout
9/// engine now consults `PageStyle` instead of these directly.
10pub const A4_WIDTH_PT: f32 = 595.276;
11/// A4 page height in PDF points.
12pub const A4_HEIGHT_PT: f32 = 841.890;
13/// Default page margin in points (24mm × 72/25.4).
14pub const MARGIN_PT: f32 = 68.031;
15
16/// Default body font size (manifest §22.1).
17pub(crate) const BODY_SIZE_PT: f32 = 11.0;
18/// Default body leading multiplier (line height = size × leading).
19pub(crate) const BODY_LEADING: f32 = 1.35;
20
21/// Page geometry resolved from `#set page(...)`. `width`/`height`
22/// describe the full media box; `margin` is symmetric on all four
23/// sides for MVP 1.5 (per-side margins are deferred).
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct PageStyle {
26 pub width: f32,
27 pub height: f32,
28 pub margin: f32,
29}
30
31impl Default for PageStyle {
32 fn default() -> Self {
33 Self {
34 width: A4_WIDTH_PT,
35 height: A4_HEIGHT_PT,
36 margin: MARGIN_PT,
37 }
38 }
39}
40
41/// Body text style resolved from `#set text(...)`. `leading` applies
42/// to body paragraphs only; headings keep their own multiplier so a
43/// `#set text(leading: 2.0)` doesn't balloon section titles.
44///
45/// `family` is the resolved [`FontFamily`] from `#set text(font: ...)`.
46/// Headings use the family's bold cut; `*emphasis*` uses italic;
47/// `` `raw` `` uses monospace; everything else is `family.regular`.
48#[derive(Clone, Copy, Debug, PartialEq)]
49pub struct TextStyle {
50 pub size_pt: f32,
51 pub leading: f32,
52 pub family: FontFamily,
53}
54
55impl Default for TextStyle {
56 fn default() -> Self {
57 Self {
58 size_pt: BODY_SIZE_PT,
59 leading: BODY_LEADING,
60 family: FontFamily::noto_sans(),
61 }
62 }
63}
64
65/// Decoded raster image data shared between every page that places
66/// the same source image. Held by [`Arc`] so a single PNG referenced
67/// from multiple `#image(...)` directives shares one buffer end-to-end.
68#[derive(Clone, Debug)]
69pub struct ImageHandle {
70 /// Stable index assigned by the layout engine. The PDF backend
71 /// uses it as the suffix on the `/Im<n>` resource-dict key and the
72 /// `XObject`'s indirect ref allocation order, so callers don't have
73 /// to hash the path themselves.
74 pub id: u32,
75 /// Resolved absolute path from the lowerer. Used as the dedup key
76 /// across multiple `#image(...)` calls with the same source.
77 pub resolved_path: String,
78 /// Decoded pixel width.
79 pub pixel_width: u32,
80 /// Decoded pixel height.
81 pub pixel_height: u32,
82 /// Flat RGB8 pixel buffer (`3 * pixel_width * pixel_height` bytes).
83 /// Shared via `Arc<[u8]>` so cloning a handle (e.g. when the same
84 /// image is referenced from multiple `#image`/`#figure` directives,
85 /// or when a future caching layer hands the document back) is cheap.
86 /// The eval crate hands ownership over as an `Arc<[u8]>` already
87 /// (see `AttrValue::Bytes`), so the slice never gets copied here.
88 pub rgb8: Arc<[u8]>,
89}
90
91/// One image placement on a page. The PDF backend emits this as a
92/// `q ... cm /Im<id> Do Q` block in the content stream.
93#[derive(Clone, Debug)]
94pub struct ImagePlacement {
95 pub handle: ImageHandle,
96 /// X coordinate of the image's left edge, measured from the page's
97 /// left edge in points.
98 pub x_pt: f32,
99 /// Y coordinate of the image's **top** edge, measured from the page's
100 /// **top** edge in points. The PDF backend flips to bottom-origin
101 /// once when emitting (same convention as [`TextRun`]).
102 pub top_from_top_pt: f32,
103 /// Rendered width in points.
104 pub width_pt: f32,
105 /// Rendered height in points.
106 pub height_pt: f32,
107}
108
109/// A single horizontal run of text on a page. The MVP 0 emitter
110/// produces one run per word; coalescing same-font neighbours is an
111/// MVP 2 optimisation.
112#[derive(Clone, Debug)]
113pub struct TextRun {
114 /// X coordinate of the run's left edge, measured from the page's
115 /// left edge in points.
116 pub x_pt: f32,
117 /// Y coordinate of the run's baseline, measured from the page's
118 /// **top** edge in points. The PDF backend flips to bottom-origin
119 /// once when emitting.
120 pub baseline_from_top_pt: f32,
121 /// Font size in points.
122 pub size_pt: f32,
123 /// Font face for this run.
124 pub font: Font,
125 /// Original UTF-8 text. Used by the PDF backend's `/ToUnicode`
126 /// `CMap` (so copy-paste from the rendered PDF round-trips back to
127 /// the source) and by the Base14 emit path (which encodes through
128 /// `WinAnsiEncoding` + per-document `/Differences`).
129 pub text: String,
130 /// Optional semantic replacement text for PDF `/ActualText`.
131 /// Raw blocks use this when the painted text must differ from
132 /// source text, e.g. tabs painted as spaces but copied as tabs.
133 pub actual_text: Option<String>,
134 /// Shaped glyph stream for embedded-font runs. Empty for Base14
135 /// runs, which emit through the byte-encoded `WinAnsi` path
136 /// instead.
137 pub glyphs: Vec<ShapedGlyph>,
138}
139
140/// One laid-out page.
141#[derive(Clone, Debug)]
142pub struct Page {
143 pub number: u32,
144 pub width_pt: f32,
145 pub height_pt: f32,
146 pub runs: Vec<TextRun>,
147 /// Raster image placements on this page. Stored as a separate
148 /// vector so PDF emit can walk every placement without filtering
149 /// the text-run stream; the two streams are independent.
150 pub images: Vec<ImagePlacement>,
151}
152
153/// One PDF bookmark/outline entry captured during layout. Ordered in
154/// document order; nesting is reconstructed from `level` at emit time.
155#[derive(Clone, Debug)]
156pub struct OutlineEntry {
157 /// Heading level 1..=3 (clamped, mirrors `read_level`).
158 pub level: u8,
159 /// Bookmark title: section number (if any) + heading text,
160 /// e.g. "1.2 Introduction".
161 pub title: String,
162 /// 0-based index into `PageGraph::pages` of the page the heading's
163 /// first line lands on. Always in range by construction.
164 pub page_index: usize,
165 /// Y of the heading's glyph tops, measured from the page top in pt.
166 /// The PDF backend flips once: `top = page.height_pt - top_from_top_pt`
167 /// (same convention as `TextRun::baseline_from_top_pt`).
168 pub top_from_top_pt: f32,
169}
170
171/// The paginated output graph (manifest §6 stage 7).
172#[derive(Clone, Debug, Default)]
173pub struct PageGraph {
174 pub pages: Vec<Page>,
175 /// Master list of every unique image referenced anywhere in
176 /// `pages`, ordered by [`ImageHandle::id`]. The PDF backend walks
177 /// this once to emit `XObject`s; per-page [`ImagePlacement::handle`]
178 /// references are just thin pointers into the same table.
179 pub images: Vec<ImageHandle>,
180 /// Bookmark/outline entries in document order (issue: PDF outlines).
181 /// Empty when the document has no headings; the PDF backend then
182 /// emits no `/Outlines` tree. Lives on `PageGraph` because `emit`
183 /// consumes only `graph`.
184 pub outline: Vec<OutlineEntry>,
185}
186
187/// Result of laying out a [`mos_core::Document`]: a [`PageGraph`] plus
188/// any warnings the engine emitted. Mirrors `mos_eval::LowerResult` so
189/// the CLI can render diagnostics uniformly.
190#[derive(Debug)]
191pub struct LayoutResult {
192 pub graph: PageGraph,
193 pub diagnostics: Vec<Diagnostic>,
194 /// Map from a declared label to the 1-based number of the page its
195 /// target first lands on (issue #72). Built during layout as each
196 /// labelled block commits its first content; first placement wins, so a
197 /// label on a block that spans pages maps to its *start* page, and a
198 /// labelled block that produced no content is absent.
199 ///
200 /// This is the layout-side half of page-reference resolution: the
201 /// resolve↔layout fixpoint feeds this map back into the resolver so
202 /// `@page(label)` can render the target's printed page number. It lives on
203 /// the result rather than the [`PageGraph`] because it feeds the resolver,
204 /// not the PDF backend, which consumes only `graph`.
205 pub label_pages: BTreeMap<String, u32>,
206}