1use mos_core::{CoreError, Diagnostic, Result, codes};
4use mos_layout::{
5 PageGraph,
6 debug::{Page, Rect, Report},
7};
8use pdf_writer::{Content, Name, Str};
9
10pub(crate) const LEGEND_HEIGHT_PT: f32 = 36.0;
11pub(crate) const FONT_NAME: Name<'_> = Name(b"LayoutDebug");
12
13pub(crate) fn canvas_width(width: f32) -> f32 {
14 width.max(420.0)
15}
16
17struct Style {
19 rgb: [f32; 3],
20 width: f32,
21 dash: &'static [f32],
22}
23
24const CONTENT: Style = Style {
25 rgb: [0.4, 0.4, 0.4],
26 width: 0.6,
27 dash: &[4.0, 3.0],
28};
29const BLOCK: Style = Style {
30 rgb: [0.0, 0.3, 0.85],
31 width: 0.9,
32 dash: &[3.0, 2.0],
33};
34const LINE: Style = Style {
35 rgb: [0.0, 0.55, 0.3],
36 width: 0.4,
37 dash: &[],
38};
39const RUN: Style = Style {
40 rgb: [0.65, 0.2, 0.7],
41 width: 0.4,
42 dash: &[1.0, 1.5],
43};
44const IMAGE: Style = Style {
45 rgb: [0.9, 0.4, 0.0],
46 width: 1.0,
47 dash: &[],
48};
49const BASELINE: Style = Style {
50 rgb: [0.9, 0.1, 0.15],
51 width: 0.6,
52 dash: &[],
53};
54
55pub(crate) fn validate(graph: &PageGraph, report: &Report) -> Result<()> {
56 if report.schema_version != 1
57 || report.units != "pt"
58 || report.origin != "top-left"
59 || graph.pages.len() != report.pages.len()
60 || graph.pages.iter().zip(&report.pages).any(|(page, trace)| {
61 page.number != trace.number
62 || trace.bounds
63 != (Rect {
64 x_pt: 0.0,
65 y_pt: 0.0,
66 width_pt: page.width_pt,
67 height_pt: page.height_pt,
68 })
69 })
70 {
71 return Err(CoreError::Diagnostic(Box::new(Diagnostic::simple(
72 &codes::MOS0051,
73 None,
74 "debug layout report must come from the same layout result as the page graph",
75 ))));
76 }
77 Ok(())
78}
79
80impl Style {
81 fn apply(&self, content: &mut Content) {
82 let [r, g, b] = self.rgb;
83 content
84 .set_stroke_rgb(r, g, b)
85 .set_line_width(self.width)
86 .set_dash_pattern(self.dash.iter().copied(), 0.0);
87 }
88}
89
90fn rectangle(content: &mut Content, height: f32, bounds: Rect, style: &Style) {
91 style.apply(content);
92 content
93 .rect(
94 bounds.x_pt,
95 height - bounds.y_pt - bounds.height_pt,
96 bounds.width_pt,
97 bounds.height_pt,
98 )
99 .stroke();
100}
101
102pub(crate) fn overlay(page: &Page) -> Vec<u8> {
103 let height = page.bounds.height_pt;
104 let mut content = Content::new();
105 content.save_state();
106 content
108 .set_fill_rgb(0.96, 0.97, 0.99)
109 .rect(
110 0.0,
111 height,
112 canvas_width(page.bounds.width_pt),
113 LEGEND_HEIGHT_PT,
114 )
115 .fill_nonzero();
116 content
117 .set_stroke_rgb(0.5, 0.5, 0.5)
118 .set_line_width(0.5)
119 .rect(0.25, 0.25, page.bounds.width_pt - 0.5, height - 0.5)
120 .stroke();
121 legend(&mut content, page);
122
123 rectangle(&mut content, height, page.content_bounds, &CONTENT);
124 for block in &page.blocks {
125 rectangle(&mut content, height, block.bounds, &BLOCK);
126 }
127 for line in &page.lines {
128 rectangle(&mut content, height, line.bounds, &LINE);
129 for run in &line.runs {
130 rectangle(&mut content, height, run.bounds, &RUN);
131 }
132 BASELINE.apply(&mut content);
133 let y = height - line.baseline_from_top_pt;
134 content
135 .move_to(line.bounds.x_pt, y)
136 .line_to(line.bounds.x_pt + line.bounds.width_pt, y)
137 .stroke();
138 }
139 for image in &page.images {
140 rectangle(&mut content, height, image.bounds, &IMAGE);
141 }
142 content.restore_state();
143 content.finish().to_vec()
144}
145
146fn legend(content: &mut Content, page: &Page) {
147 let font = FONT_NAME;
148 content
149 .set_fill_rgb(0.15, 0.2, 0.3)
150 .begin_text()
151 .set_font(font, 8.0)
152 .set_text_matrix([1.0, 0.0, 0.0, 1.0, 12.0, page.bounds.height_pt + 23.0])
153 .show(Str(format!(
154 "LAYOUT DEBUG | page {} | units: pt",
155 page.number
156 )
157 .as_bytes()))
158 .end_text();
159 let mut x = 12.0;
160 for (label, style) in [
161 ("content", &CONTENT),
162 ("block", &BLOCK),
163 ("line", &LINE),
164 ("run", &RUN),
165 ("image", &IMAGE),
166 ("baseline", &BASELINE),
167 ] {
168 style.apply(content);
169 let y = page.bounds.height_pt + 10.0;
170 content
171 .move_to(x, y + 2.0)
172 .line_to(x + 12.0, y + 2.0)
173 .stroke();
174 content
175 .begin_text()
176 .set_font(font, 7.0)
177 .set_text_matrix([1.0, 0.0, 0.0, 1.0, x + 16.0, y])
178 .show(Str(label.as_bytes()))
179 .end_text();
180 x += 64.0;
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn rejects_incompatible_reports() -> std::result::Result<(), Box<dyn std::error::Error>> {
190 let layout = mos_layout::LayoutEngine::new()
191 .layout_with_debug(&mos_core::Document::new("debug.mos".into()));
192 let report = layout.debug.ok_or("tracing requested")?;
193 validate(&layout.graph, &report)?;
194 let mut incompatible = [report.clone(), report.clone(), report.clone(), report];
195 incompatible[0].pages.clear();
196 incompatible[1].pages[0].number += 1;
197 incompatible[2].pages[0].bounds.height_pt += 1.0;
198 incompatible[3].schema_version += 1;
199 for report in incompatible {
200 if validate(&layout.graph, &report).is_ok() {
201 return Err("incompatible report accepted".into());
202 }
203 }
204 Ok(())
205 }
206}