Skip to main content

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}