1use crate::context::Ctx;
5use crate::engine::catalog::{display_name, Locale};
6use crate::kernels::geometry::Rect;
7use crate::store::Store;
8use crate::tiles::decode_png;
9use crate::value::{fingerprint, read_rect, Js, Obj};
10use crate::Diagnostic;
11use std::collections::BTreeMap;
12use std::io::{self, Write};
13
14#[derive(Clone, Debug, PartialEq)]
16pub struct CanvasInfo {
17 pub id: String,
18 pub layer: String,
20 pub name: String,
23 pub kind: String,
25 pub bounds: Rect,
27 pub tile_count: u64,
28 pub observed_pixels: f64,
29 pub fragment: u32,
31 pub attached_to: Option<String>,
33 pub presents: Option<String>,
35}
36
37impl CanvasInfo {
38 fn from_row(o: &Obj) -> CanvasInfo {
39 CanvasInfo {
40 id: o.str("id").unwrap_or_default().to_string(),
41 layer: o.str("layer").unwrap_or_default().to_string(),
42 name: o.str("name").unwrap_or_default().to_string(),
43 kind: o.str("kind").unwrap_or_default().to_string(),
44 bounds: o.get("bounds").and_then(read_rect).unwrap_or(Rect {
45 x: 0.0,
46 y: 0.0,
47 width: 0.0,
48 height: 0.0,
49 }),
50 tile_count: o.num("tileCount").max(0.0) as u64,
51 observed_pixels: o.num("observedPixels"),
52 fragment: o.num("fragment").max(0.0) as u32,
53 attached_to: o.str("attachedTo").map(String::from),
54 presents: o
55 .get("presentation")
56 .and_then(Js::as_obj)
57 .and_then(|p| p.str("sourceCanvas"))
58 .map(String::from),
59 }
60 }
61}
62
63pub struct RunResult {
65 store: Store,
66 run_id: String,
67 project: Obj,
68 diagnostics: Vec<Diagnostic>,
69 tile_size: usize,
70 seconds: f64,
71 locale: Locale,
72}
73
74impl std::fmt::Debug for RunResult {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.debug_struct("RunResult")
77 .field("status", &self.status())
78 .field("frames", &self.frames())
79 .field("rows", &self.store.len())
80 .finish()
81 }
82}
83
84impl RunResult {
85 pub(crate) fn new(ctx: Ctx, seconds: f64) -> RunResult {
86 let project = ctx.project();
87 RunResult {
88 run_id: ctx.run_id.clone(),
89 tile_size: ctx.tiles.size,
90 diagnostics: ctx.journal.shown,
91 store: ctx.store,
92 project,
93 seconds,
94 locale: ctx.locale,
95 }
96 }
97
98 pub fn status(&self) -> &str {
100 self.project.str("status").unwrap_or("error")
101 }
102
103 pub fn error(&self) -> Option<&str> {
105 self.project.str("error")
106 }
107
108 pub fn frames(&self) -> u32 {
110 self.project.num("frames") as u32
111 }
112
113 pub fn rendered_frames(&self) -> u32 {
114 let n = self.project.num("renderedFrames");
115 if n.is_finite() {
116 n as u32
117 } else {
118 0
119 }
120 }
121
122 pub fn seconds(&self) -> f64 {
124 self.seconds
125 }
126
127 pub fn project(&self) -> &Obj {
129 &self.project
130 }
131
132 pub fn diagnostics(&self) -> &[Diagnostic] {
134 &self.diagnostics
135 }
136
137 pub fn display_name(&self, canvas: &CanvasInfo) -> String {
139 display_name(self.locale, &canvas.name)
140 }
141
142 pub fn store(&self) -> &Store {
144 &self.store
145 }
146
147 fn row(&self, key: &str) -> Option<Js> {
148 self.store.get(&format!("run/{}/{key}", self.run_id))
149 }
150
151 pub fn get(&self, key: &str) -> Option<Js> {
153 self.row(key)
154 }
155
156 pub fn canvases(&self) -> Vec<CanvasInfo> {
158 let base = format!("run/{}/canvas/", self.run_id);
159 self.store
160 .keys(&base)
161 .iter()
162 .filter_map(|k| self.store.get(k))
163 .filter_map(|v| v.as_obj().map(CanvasInfo::from_row))
164 .collect()
165 }
166
167 pub fn prints(&self) -> Vec<CanvasInfo> {
172 let framed = self
173 .project
174 .get("settings")
175 .and_then(Js::as_obj)
176 .and_then(|s| s.str("framing"))
177 == Some("context");
178 printable(self.canvases(), framed)
179 }
180
181 pub fn print(&self) -> Option<CanvasInfo> {
183 self.prints().into_iter().next()
184 }
185
186 pub fn fragments(&self) -> Vec<CanvasInfo> {
189 fragments_of(&self.prints())
190 }
191
192 pub fn tile(&self, canvas: &str, x: i32, y: i32) -> Option<Vec<u8>> {
194 let row = self.row(&format!("tile/{canvas}/0/{x}_{y}"))?;
195 let blob = row.as_obj()?.get("blob")?.bytes()?.clone();
196 decode_png(&blob).ok().map(|(_, _, rgba)| rgba)
197 }
198
199 pub fn tile_size(&self) -> usize {
200 self.tile_size
201 }
202
203 fn extent(&self, canvas: &str) -> io::Result<(i64, i64, u32, u32)> {
205 let info = self
206 .canvases()
207 .into_iter()
208 .find(|c| c.id == canvas)
209 .ok_or_else(|| {
210 io::Error::new(io::ErrorKind::NotFound, format!("no canvas {canvas}"))
211 })?;
212 let b = info.bounds;
213 let (x0, y0) = (b.x.floor() as i64, b.y.floor() as i64);
214 let (w, h) = (
215 b.width.ceil().max(0.0) as u32,
216 b.height.ceil().max(0.0) as u32,
217 );
218 if w == 0 || h == 0 {
219 return Err(io::Error::new(
220 io::ErrorKind::InvalidData,
221 "the canvas is empty",
222 ));
223 }
224 Ok((x0, y0, w, h))
225 }
226
227 pub fn canvas_size(&self, canvas: &str) -> io::Result<(u32, u32)> {
229 self.extent(canvas).map(|(_, _, w, h)| (w, h))
230 }
231
232 pub fn read_rows(
235 &self,
236 canvas: &str,
237 mut row: impl FnMut(&[u8]) -> io::Result<()>,
238 ) -> io::Result<(u32, u32)> {
239 let (x0, y0, w, h) = self.extent(canvas)?;
240 let size = self.tile_size as i64;
241 let mut line = vec![0u8; w as usize * 4];
242 let mut y = 0i64;
243 while y < h as i64 {
244 let wy = y0 + y;
245 let ty = wy.div_euclid(size);
246 let band_end = ((ty + 1) * size - y0).min(h as i64);
247 let tx0 = x0.div_euclid(size);
248 let tx1 = (x0 + w as i64 - 1).div_euclid(size);
249 let tiles: Vec<Option<Vec<u8>>> = (tx0..=tx1)
250 .map(|tx| self.tile(canvas, tx as i32, ty as i32))
251 .collect();
252 while y < band_end {
253 let ly = (y0 + y - ty * size) as usize;
254 line.fill(0);
255 for (i, tile) in tiles.iter().enumerate() {
256 let Some(tile) = tile else { continue };
257 let tx = tx0 + i as i64;
258 let from = (tx * size).max(x0);
259 let to = ((tx + 1) * size).min(x0 + w as i64);
260 let src = ((ly * self.tile_size) + (from - tx * size) as usize) * 4;
261 let dst = (from - x0) as usize * 4;
262 let n = (to - from) as usize * 4;
263 line[dst..dst + n].copy_from_slice(&tile[src..src + n]);
264 }
265 row(&line)?;
266 y += 1;
267 }
268 }
269 Ok((w, h))
270 }
271
272 pub fn write_png<W: Write>(&self, canvas: &str, out: W) -> io::Result<(u32, u32)> {
275 let (w, h) = self.canvas_size(canvas)?;
276 let mut encoder = png::Encoder::new(out, w, h);
277 encoder.set_color(png::ColorType::Rgba);
278 encoder.set_depth(png::BitDepth::Eight);
279 encoder.set_compression(png::Compression::Fast);
280 let mut writer = encoder.write_header().map_err(io::Error::other)?;
281 let mut stream = writer.stream_writer().map_err(io::Error::other)?;
282 self.read_rows(canvas, |line| stream.write_all(line))?;
283 stream.finish().map_err(io::Error::other)?;
284 Ok((w, h))
285 }
286
287 pub fn fingerprint(&self) -> (BTreeMap<String, String>, String) {
290 let mut rows = BTreeMap::new();
291 for (key, _) in self.store.iter() {
292 let value = self.store.get(key).unwrap();
293 let k = key.replace(&self.run_id, "<run>");
294 rows.insert(k, fingerprint(&value, &self.run_id));
295 }
296 let project = fingerprint(&Js::Obj(self.project.clone()), &self.run_id);
297 (rows, project)
298 }
299}
300
301fn printable(canvases: Vec<CanvasInfo>, framed: bool) -> Vec<CanvasInfo> {
306 let rank = |c: &CanvasInfo| match c.kind.as_str() {
307 "presentation" if framed => -1,
308 "fixed" => 1,
309 _ => 0,
310 };
311 let mut out: Vec<CanvasInfo> = canvases
312 .into_iter()
313 .filter(|c| {
314 c.tile_count > 0
315 && c.attached_to.is_none()
316 && c.bounds.width.is_finite()
317 && c.bounds.height.is_finite()
318 })
319 .collect();
320 out.sort_by(|a, b| {
322 rank(a).cmp(&rank(b)).then_with(|| {
323 (b.observed_pixels - a.observed_pixels)
324 .partial_cmp(&0.0)
325 .unwrap_or(std::cmp::Ordering::Equal)
326 })
327 });
328 out
329}
330
331fn fragments_of(prints: &[CanvasInfo]) -> Vec<CanvasInfo> {
333 let main = prints
334 .first()
335 .map(|p| p.presents.clone().unwrap_or_else(|| p.id.clone()));
336 prints
337 .iter()
338 .filter(|c| c.kind == "moving" && Some(&c.id) != main.as_ref())
339 .cloned()
340 .collect()
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 fn canvas(id: &str, kind: &str, observed: f64) -> CanvasInfo {
348 CanvasInfo {
349 id: id.into(),
350 layer: "body".into(),
351 name: id.into(),
352 kind: kind.into(),
353 bounds: Rect {
354 x: 0.0,
355 y: 0.0,
356 width: 100.0,
357 height: 100.0,
358 },
359 tile_count: 1,
360 observed_pixels: observed,
361 fragment: 0,
362 attached_to: None,
363 presents: None,
364 }
365 }
366
367 fn framed(id: &str, of: &str, observed: f64) -> CanvasInfo {
368 CanvasInfo {
369 presents: Some(of.into()),
370 ..canvas(id, "presentation", observed)
371 }
372 }
373
374 fn ids(canvases: &[CanvasInfo]) -> Vec<&str> {
375 canvases.iter().map(|c| c.id.as_str()).collect()
376 }
377
378 #[test]
379 fn a_framed_main_canvas_outranks_a_bigger_fragment_that_was_not_framed() {
380 let rows = vec![
383 canvas("main", "moving", 100.0),
384 framed("main-framed", "main", 150.0),
385 CanvasInfo {
386 fragment: 1,
387 ..canvas("piece", "moving", 200.0)
388 },
389 ];
390 let prints = printable(rows, true);
391 assert_eq!(ids(&prints), ["main-framed", "piece", "main"]);
392 assert_eq!(ids(&fragments_of(&prints)), ["piece"]);
393 }
394
395 #[test]
396 fn presentations_rank_by_their_own_observed_pixels() {
397 let rows = vec![
399 canvas("a", "moving", 100.0),
400 canvas("b", "moving", 200.0),
401 framed("a-framed", "a", 300.0),
402 framed("b-framed", "b", 250.0),
403 ];
404 let prints = printable(rows, true);
405 assert_eq!(ids(&prints), ["a-framed", "b-framed", "b", "a"]);
406 assert_eq!(ids(&fragments_of(&prints)), ["b"]);
407 }
408
409 #[test]
410 fn fragments_follow_observed_pixels_not_key_order() {
411 let rows = vec![
412 canvas("a", "moving", 300.0),
413 canvas("b", "moving", 10.0),
414 canvas("c", "moving", 20.0),
415 ];
416 let prints = printable(rows, false);
417 assert_eq!(ids(&prints), ["a", "c", "b"]);
418 assert_eq!(ids(&fragments_of(&prints)), ["c", "b"]);
419 }
420
421 #[test]
422 fn fixed_parts_come_last_and_empty_merged_or_unbounded_canvases_are_left_out() {
423 let rows = vec![
424 canvas("chrome", "fixed", 1e6),
425 canvas("page", "moving", 100.0),
426 CanvasInfo {
427 tile_count: 0,
428 attached_to: Some("page".into()),
429 ..canvas("merged", "moving", 500.0)
430 },
431 CanvasInfo {
432 tile_count: 0,
433 ..canvas("empty", "moving", 0.0)
434 },
435 CanvasInfo {
436 bounds: Rect {
437 x: 0.0,
438 y: 0.0,
439 width: f64::NAN,
440 height: 100.0,
441 },
442 ..canvas("unbounded", "moving", 900.0)
443 },
444 ];
445 assert_eq!(ids(&printable(rows, true)), ["page", "chrome"]);
446 }
447
448 #[test]
449 fn without_framing_a_presentation_ranks_with_the_content() {
450 let rows = vec![
452 canvas("page", "moving", 100.0),
453 framed("page-framed", "page", 50.0),
454 ];
455 assert_eq!(ids(&printable(rows, false)), ["page", "page-framed"]);
456 }
457
458 #[test]
459 fn ties_keep_key_order() {
460 let rows = vec![canvas("a", "moving", 100.0), canvas("b", "moving", 100.0)];
461 assert_eq!(ids(&printable(rows, false)), ["a", "b"]);
462 }
463
464 #[test]
465 fn only_fixed_interface_still_gives_a_print() {
466 let prints = printable(vec![canvas("chrome", "fixed", 10.0)], true);
468 assert_eq!(ids(&prints), ["chrome"]);
469 assert!(fragments_of(&prints).is_empty());
470 }
471}