1use std::collections::BTreeMap;
9
10use mos_core::{AttrValue, Node, NodeKind, display_path};
11use serde::Serialize;
12
13use crate::{ImagePlacement, Page as LayoutPage, PageStyle, TextRun, ascent, descent};
14
15#[derive(Clone, Debug, Serialize)]
17pub struct Report {
18 pub schema_version: u32,
19 pub units: &'static str,
20 pub origin: &'static str,
21 pub pages: Vec<Page>,
22}
23
24#[derive(Clone, Debug, Serialize)]
26pub struct Page {
27 pub number: u32,
28 pub bounds: Rect,
29 pub content_bounds: Rect,
30 pub blocks: Vec<Block>,
31 pub lines: Vec<Line>,
32 pub images: Vec<Image>,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
37pub struct Rect {
38 pub x_pt: f32,
39 pub y_pt: f32,
40 pub width_pt: f32,
41 pub height_pt: f32,
42}
43
44impl Rect {
45 fn union(self, other: Self) -> Self {
46 let x = self.x_pt.min(other.x_pt);
47 let y = self.y_pt.min(other.y_pt);
48 Self {
49 x_pt: x,
50 y_pt: y,
51 width_pt: (self.x_pt + self.width_pt).max(other.x_pt + other.width_pt) - x,
52 height_pt: (self.y_pt + self.height_pt).max(other.y_pt + other.height_pt) - y,
53 }
54 }
55}
56
57#[derive(Clone, Debug, Serialize)]
60pub struct Source {
61 pub node_id: u64,
62 pub kind: &'static str,
63 pub file: String,
64 pub byte_start: usize,
65 pub byte_end: usize,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub label: Option<String>,
68}
69
70impl Source {
71 fn from_node(node: &Node) -> Self {
72 Self {
73 node_id: node.id.0,
74 kind: match node.kind {
75 NodeKind::Section => "section",
76 NodeKind::Paragraph => "paragraph",
77 NodeKind::Image => "image",
78 NodeKind::Figure => "figure",
79 NodeKind::List => "list",
80 NodeKind::ListItem => "list_item",
81 NodeKind::Bibliography => "bibliography",
82 NodeKind::Raw => "raw",
83 _ => "other",
84 },
85 file: display_path(&node.span.file),
86 byte_start: node.span.start(),
87 byte_end: node.span.end(),
88 label: match node.attributes.get("label") {
89 Some(AttrValue::Str(label)) => Some(label.clone()),
90 _ => None,
91 },
92 }
93 }
94}
95
96#[derive(Clone, Debug, Serialize)]
98pub struct Block {
99 pub source: Source,
100 pub bounds: Rect,
101}
102
103#[derive(Clone, Debug, Serialize)]
105pub struct Line {
106 pub source: Option<Source>,
107 pub bounds: Rect,
108 pub baseline_from_top_pt: f32,
109 pub runs: Vec<Run>,
110}
111
112#[derive(Clone, Debug, Serialize)]
114pub struct Run {
115 pub run_index: usize,
116 pub text: String,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub actual_text: Option<String>,
119 pub font: String,
120 pub size_pt: f32,
121 pub bounds: Rect,
122}
123
124#[derive(Clone, Debug, Serialize)]
126pub struct Image {
127 pub source: Option<Source>,
128 pub image_index: usize,
129 pub image_id: u32,
130 pub pixel_width: u32,
131 pub pixel_height: u32,
132 pub bounds: Rect,
133}
134
135#[derive(Default)]
136struct PageTrace {
137 blocks: BTreeMap<u64, Block>,
138 lines: Vec<Line>,
139 images: Vec<Image>,
140}
141
142#[derive(Default)]
143pub(super) struct Recorder {
144 active: Vec<Source>,
145 pages: BTreeMap<u32, PageTrace>,
146 pending_runs: Vec<Run>,
147}
148
149impl Recorder {
150 pub(super) fn begin_block(&mut self, node: &Node) {
151 self.active.push(Source::from_node(node));
152 }
153
154 pub(super) fn end_block(&mut self) {
155 self.active.pop();
156 }
157
158 pub(super) fn run(&mut self, run_index: usize, run: &TextRun, advance_pt: f32) {
159 let ascender = ascent(run.font, run.size_pt);
160 self.pending_runs.push(Run {
161 run_index,
162 text: run.text.clone(),
163 actual_text: run.actual_text.clone(),
164 font: run.font.pdf_base_name().to_owned(),
165 size_pt: run.size_pt,
166 bounds: Rect {
167 x_pt: run.x_pt,
168 y_pt: run.baseline_from_top_pt - ascender,
169 width_pt: advance_pt,
170 height_pt: ascender + descent(run.font, run.size_pt),
171 },
172 });
173 }
174
175 pub(super) fn line(&mut self, page: u32, baseline_from_top_pt: f32) {
176 let runs = std::mem::take(&mut self.pending_runs);
177 let Some(bounds) = runs.iter().map(|run| run.bounds).reduce(Rect::union) else {
178 return;
179 };
180 self.extend_blocks(page, bounds);
181 self.pages.entry(page).or_default().lines.push(Line {
182 source: self.active.last().cloned(),
183 bounds,
184 baseline_from_top_pt,
185 runs,
186 });
187 }
188
189 pub(super) fn image(&mut self, page: u32, image_index: usize, image: &ImagePlacement) {
190 let bounds = Rect {
191 x_pt: image.x_pt,
192 y_pt: image.top_from_top_pt,
193 width_pt: image.width_pt,
194 height_pt: image.height_pt,
195 };
196 self.extend_blocks(page, bounds);
197 self.pages.entry(page).or_default().images.push(Image {
198 source: self.active.last().cloned(),
199 image_index,
200 image_id: image.handle.id,
201 pixel_width: image.handle.pixel_width,
202 pixel_height: image.handle.pixel_height,
203 bounds,
204 });
205 }
206
207 fn extend_blocks(&mut self, page: u32, bounds: Rect) {
208 let blocks = &mut self.pages.entry(page).or_default().blocks;
209 for source in &self.active {
210 blocks
211 .entry(source.node_id)
212 .and_modify(|block| {
213 block.bounds = block.bounds.union(bounds);
214 })
215 .or_insert_with(|| Block {
216 source: source.clone(),
217 bounds,
218 });
219 }
220 }
221
222 pub(super) fn finish(mut self, pages: &[LayoutPage], style: PageStyle) -> Report {
223 Report {
224 schema_version: 1,
225 units: "pt",
226 origin: "top-left",
227 pages: pages
228 .iter()
229 .map(|page| {
230 let trace = self.pages.remove(&page.number).unwrap_or_default();
231 Page {
232 number: page.number,
233 bounds: Rect {
234 x_pt: 0.0,
235 y_pt: 0.0,
236 width_pt: page.width_pt,
237 height_pt: page.height_pt,
238 },
239 content_bounds: Rect {
240 x_pt: style.margin,
241 y_pt: style.margin,
242 width_pt: page.width_pt - 2.0 * style.margin,
243 height_pt: page.height_pt - 2.0 * style.margin,
244 },
245 blocks: trace.blocks.into_values().collect(),
246 lines: trace.lines,
247 images: trace.images,
248 }
249 })
250 .collect(),
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests;