1use canvas::{
2 document::TextDocument,
3 layout::TextEngine,
4 outline::{Arrange, OutlineLayout},
5};
6use onestore::page::{Outline, Page, PageObject};
7use onestore::{RevisionIndex, Store, document::Document};
8use serde_json::json;
9use std::{env, fs, io, sync::Arc, time::Instant};
10
11fn main() -> Result<(), Box<dyn std::error::Error>> {
12 let mut args = env::args().skip(1);
13 let path = args
14 .next()
15 .ok_or("Expected a section snapshot and exact page title")?;
16 let title = args.next().ok_or("Expected an exact page title")?;
17 let mut substitutes = Vec::new();
18 while let Some(option) = args.next() {
19 if option != "--substitute-font" {
20 return Err(format!("Unknown option: {option}").into());
21 }
22 substitutes.push(
23 args.next()
24 .ok_or("Provide a font file after --substitute-font.")?,
25 );
26 }
27 let start = Instant::now();
28 let bytes = fs::read(path)?;
29 let store = Store::parse(&bytes)?;
30 let index = RevisionIndex::parse(&store)?;
31 let document = Document::parse(&index)?;
32 let page = Page::from_document(&document, &title)?;
33 drop(document);
34 drop(index);
35 drop(store);
36 drop(bytes);
37 let import_ms = start.elapsed().as_secs_f64() * 1000.0;
38 let start = Instant::now();
39 let mut engine = TextEngine::default();
40 for path in substitutes {
41 engine.register_substitute(parley::fontique::Blob::new(Arc::new(fs::read(path)?)))?;
42 }
43 let mut objects = Vec::new();
44 let mut title_areas = Vec::new();
45 for object in &page.objects {
46 match object {
47 PageObject::Outline(outline) => objects.push(outline_json(
48 outline,
49 &outline.layout(&mut engine, &page.definitions)?,
50 None,
51 )?),
52 PageObject::Title(title) => {
53 title_areas.push(
54 json!({"id": title.id, "layout": title.layout, "date_outline": title.date,
55 "outlines": title.outlines.iter().map(|o| o.id).collect::<Vec<_>>()}),
56 );
57 for (outline, (origin, layout)) in title
58 .outlines
59 .iter()
60 .zip(title.layout(&mut engine, &page.definitions)?)
61 {
62 objects.push(outline_json(outline, &layout, Some(origin))?);
63 }
64 }
65 PageObject::Image(image) => objects.push(json!({"kind": "image", "id": image.id,
66 "layout": image.layout, "bytes": image.bytes.as_ref().map(|b| b.len()),
67 "alt": image.alt, "background": image.background})),
68 PageObject::Attachment(file) => objects.push(json!({"kind": "file", "id": file.id,
69 "layout": file.layout, "filename": file.filename,
70 "bytes": file.bytes.as_ref().map(|b| b.len())})),
71 PageObject::Ink(ink) => objects.push(json!({"kind": "ink", "id": ink.id,
72 "bounds": ink.bounds(), "strokes": ink.strokes.len(), "groups": ink.groups.len()})),
73 PageObject::Unsupported(object) => objects.push(json!({"kind": "unsupported",
74 "id": object.id, "layout": object.layout, "jcid": object.jcid})),
75 }
76 }
77 serde_json::to_writer_pretty(
78 io::stdout().lock(),
79 &json!({
80 "title": page.title, "created_filetime": page.created, "title_areas": title_areas, "margin_origin": page.margin_origin, "objects": objects,
81 "definitions": page.definitions.iter().map(|(id, definition)| (id.to_string(), json!({
82 "kind": definition.kind, "format": definition.format,
83 }))).collect::<serde_json::Map<_, _>>(), "import_ms": import_ms,
84 "layout_ms": start.elapsed().as_secs_f64() * 1000.0,
85 "measurement": "Outline layout applies indentation, paragraph spacing, collapsed descendants and bullet height; titles use retained parent geometry and stacked child outlines; tags and numbered markers are separate",
86 }),
87 )?;
88 Ok(())
89}
90
91fn outline_json(
92 outline: &Outline,
93 laid_out: &OutlineLayout,
94 title_origin: Option<[f32; 2]>,
95) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
96 let mut paragraphs = Vec::new();
97 let document = TextDocument::from_nodes(outline.paragraphs.clone())?;
98 for paragraph in document.text_nodes() {
99 let placed = laid_out.paragraphs.iter().find(|p| p.id == paragraph.id);
100 let source = &paragraph
101 .text()
102 .ok_or("Unsupported paragraph content")?
103 .text;
104 let projection;
105 let visible = if let Some(placed) = placed {
106 placed.projection.text()
107 } else {
108 projection = source.project()?;
109 projection.text()
110 };
111 let layout = placed.map(|p| &p.text);
112 let lines: Vec<_> = layout
113 .as_ref()
114 .into_iter()
115 .flat_map(|layout| layout.lines())
116 .map(|(line, bounds)| {
117 let range = bounds.source.clone();
118 json!({
119 "start_utf16": visible.utf16_offset(range.start).unwrap(),
120 "end_utf16": visible.utf16_offset(range.end).unwrap(),
121 "text": &visible.text()[range],
122 "advance": line.metrics().advance,
123 "baseline": bounds.baseline,
124 "height": bounds.height,
125 })
126 })
127 .collect();
128 paragraphs.push(json!({
129 "id": paragraph.id, "parent": paragraph.parent, "level": paragraph.level,
130 "date_fields": paragraph.text().into_iter().filter_map(|text| text.date_field).map(|field| format!("{field:?}")).collect::<Vec<_>>(),
131 "format": paragraph.format, "collapsed": paragraph.collapsed,
132 "lists": paragraph.lists,
133 "tags": paragraph.tags.iter().chain(paragraph.text().into_iter().flat_map(|t| &t.tags)).collect::<Vec<_>>(),
134 "source_utf16": source.text().encode_utf16().count(),
135 "visible_utf16": visible.text().encode_utf16().count(),
136 "visible_text": visible.text(), "spans": visible.spans().iter().map(|s| json!({"end_utf8": s.end, "format": s.format})).collect::<Vec<_>>(),
137 "height": layout.as_ref().map(|l| l.height()), "lines": lines,
138 "origin": placed.map(|p| p.origin),
139 "tag_icons": placed.map(|p| p.tags.iter().map(|tag| json!({
140 "icon": format!("{:?}", tag.icon), "label": tag.label, "disabled": tag.disabled,
141 "origin": [tag.origin[0], tag.origin[1] + p.origin[1]],
142 })).collect::<Vec<_>>()),
143 "markers": placed.map(|p| p.markers.iter().map(|(layout, origin)| json!({
144 "origin": [origin[0], origin[1] + p.origin[1]], "height": layout.height(),
145 "baseline": layout.lines().next().unwrap().1.baseline,
146 "advance": layout.lines().next().unwrap().0.metrics().advance,
147 })).collect::<Vec<_>>()),
148 }));
149 }
150 Ok(
151 json!({"kind": "outline", "id": outline.id, "layout": outline.layout,
152 "indents": outline.indents, "is_title": title_origin.is_some(), "is_title_text": outline.title, "minimum_width": outline.min_width, "title_origin": title_origin, "paragraphs": paragraphs,
153 "size": laid_out.size,
154 "tables": laid_out.tables.iter().map(|table| json!({
155 "id": table.id, "borders": table.borders,
156 "cells": table.cells.iter().map(|cell| json!({"id": cell.id, "rect": cell.rect})).collect::<Vec<_>>()
157 })).collect::<Vec<_>>(),
158 "unsupported": outline.unsupported.iter().map(|u| u.jcid).collect::<Vec<_>>() }),
159 )
160}