Skip to main content

mos_pdf/
debug.rs

1//! Vector overlays for the geometry recorded by layout. No layout policy.
2
3use 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
17// Colors and stroke patterns are shared by the geometry and its legend.
18struct 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    // Keep the legend outside the original paper even for zero-margin pages.
107    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}