1//! How many pages a reading view reflows into a phone's column, across the active pages of
2//! every distinct section under the given directories, as Markdown; with `--json FILE`, one
3//! line per page with its blocks in reading order. `--render SECTION INDEX PREFIX` draws page
4//! INDEX of SECTION whole (`PREFIX-page.png`) and reflowed (`PREFIX-column.png`).
5//!
6//! `cargo run --release -p canvas --features gpu --example reading_inventory -- [--column 230] corpus`
7
8use canvas::{
9 gpu::{Paper, Viewport, page::PageScene},
10 layout::TextEngine,
11 reading::{self, Kept, Kind, Refusal, Verdict},
12};
13use draw::Renderer;
14use onestore::{Arena, Section, page::Page};
15use std::{
16 collections::{BTreeMap, BTreeSet, hash_map::DefaultHasher},
17 fmt::Write as _,
18 hash::{Hash, Hasher},
19 io::Write as _,
20 path::{Path, PathBuf},
21 time::Duration,
22};
23
24/// Host points per page point at the iOS app's 100%: OneNote's 96 dpi enlarged 15%.
25const POINT: f32 = 96.0 / 72.0 * 1.15;
26
27fn sections(directory: &Path, out: &mut Vec<PathBuf>) {
28 let Ok(entries) = std::fs::read_dir(directory) else {
29 return;
30 };
31 let mut entries: Vec<PathBuf> = entries.flatten().map(|entry| entry.path()).collect();
32 entries.sort();
33 for path in entries {
34 if path.is_dir() {
35 sections(&path, out);
36 } else if path.extension().is_some_and(|e| e == "one") {
37 out.push(path);
38 }
39 }
40}
41
42fn label(verdict: &Verdict) -> String {
43 match verdict {
44 Verdict::Fits => "fits".into(),
45 Verdict::Reflows => "reflows".into(),
46 Verdict::Partial(kept) => format!(
47 "partial: {}",
48 kept.iter()
49 .map(|k| kept_name(*k))
50 .collect::<Vec<_>>()
51 .join(", ")
52 ),
53 Verdict::Refused(refusal) => format!(
54 "refused: {}",
55 match refusal {
56 Refusal::RightToLeft => "right to left",
57 Refusal::Drawn => "mostly ink",
58 Refusal::Arranged => "mostly overlapping",
59 }
60 ),
61 }
62}
63
64fn kept_name(kept: Kept) -> &'static str {
65 match kept {
66 Kept::Asides => "asides",
67 Kept::SideBySide => "side by side",
68 Kept::Group => "overlap kept whole",
69 Kept::Wide => "wider than the column",
70 }
71}
72
73fn kind_name(kind: Kind) -> &'static str {
74 match kind {
75 Kind::Text => "text",
76 Kind::Picture => "picture",
77 Kind::File => "file",
78 Kind::Drawing => "drawing",
79 Kind::Group => "group",
80 }
81}
82
83fn open(path: &Path) -> Option<Vec<(String, Page)>> {
84 let image = std::fs::read(path).ok()?;
85 let arena = Arena::default();
86 let mut section = Section::open(&arena, image).ok()?;
87 let pages = section.pages().ok()?;
88 Some(
89 pages
90 .into_iter()
91 .filter_map(|(space, title, _)| section.page(space).ok().map(|page| (title, page)))
92 .collect(),
93 )
94}
95
96fn main() -> Result<(), Box<dyn std::error::Error>> {
97 let mut args: Vec<String> = std::env::args().skip(1).collect();
98 let mut column = 230.0;
99 let mut json = None;
100 if let Some(at) = args.iter().position(|a| a == "--column") {
101 column = args[at + 1].parse()?;
102 args.drain(at..at + 2);
103 }
104 if let Some(at) = args.iter().position(|a| a == "--json") {
105 json = Some(std::fs::File::create(&args[at + 1])?);
106 args.drain(at..at + 2);
107 }
108 let mut engine = TextEngine::default();
109 if args.first().is_some_and(|a| a == "--render") {
110 let [_, section, index, prefix] = &args[..] else {
111 return Err("--render SECTION INDEX PREFIX".into());
112 };
113 let pages = open(Path::new(section)).ok_or("section does not open")?;
114 let (_, page) = pages
115 .into_iter()
116 .nth(index.parse()?)
117 .ok_or("no such page")?;
118 let read = reading::read(&page, column, &mut engine)?;
119 let mut gpu = Gpu::new()?;
120 gpu.whole(&page, &mut engine, &format!("{prefix}-page.png"))?;
121 if let Some(reflowed) = &read.page {
122 gpu.column(
123 reflowed,
124 column,
125 &mut engine,
126 &format!("{prefix}-column.png"),
127 )?;
128 }
129 println!("{}", label(&read.verdict));
130 return Ok(());
131 }
132 let mut seen = BTreeSet::new();
133 let mut distinct = BTreeSet::new();
134 let mut verdicts: BTreeMap<String, Vec<String>> = BTreeMap::new();
135 let (mut pages, mut failed) = (0, 0);
136 for root in &args {
137 let mut paths = Vec::new();
138 sections(Path::new(root), &mut paths);
139 for path in paths {
140 let Ok(bytes) = std::fs::read(&path) else {
141 continue;
142 };
143 let mut hasher = DefaultHasher::new();
144 bytes.hash(&mut hasher);
145 if !seen.insert(hasher.finish()) {
146 continue;
147 }
148 let Some(section) = open(&path) else {
149 continue;
150 };
151 for (index, (title, page)) in section.into_iter().enumerate() {
152 // Fixtures repeat pages across their runs; each page counts once.
153 let mut hasher = DefaultHasher::new();
154 serde_json::to_string(&page)?.hash(&mut hasher);
155 if !distinct.insert(hasher.finish()) {
156 continue;
157 }
158 pages += 1;
159 let place = format!("{} #{index} — {title}", path.display());
160 let read = match reading::read(&page, column, &mut engine) {
161 Ok(read) => read,
162 Err(_) => {
163 failed += 1;
164 continue;
165 }
166 };
167 let verdict = label(&read.verdict);
168 if let Some(json) = &mut json {
169 let blocks: Vec<_> = read
170 .blocks
171 .iter()
172 .map(|b| serde_json::json!({"kind": kind_name(b.kind), "members": b.members.len(), "bounds": b.bounds}))
173 .collect();
174 let line = serde_json::json!({
175 "section": path.display().to_string(), "index": index, "title": title,
176 "verdict": verdict, "offered": read.verdict.offered(), "blocks": blocks,
177 });
178 writeln!(json, "{line}")?;
179 }
180 verdicts.entry(verdict).or_default().push(place);
181 }
182 }
183 }
184 let mut out = String::new();
185 writeln!(out, "# Reading view eligibility\n")?;
186 writeln!(
187 out,
188 "{} distinct sections, {pages} distinct pages ({failed} failed to lay out), column {column} pt ({:.0} pt on screen at 100%).\n",
189 seen.len(),
190 column * POINT
191 )?;
192 writeln!(
193 out,
194 "| Verdict | Pages | Share | Examples |\n| --- | ---: | ---: | --- |"
195 )?;
196 let mut rows: Vec<_> = verdicts.iter().collect();
197 rows.sort_by_key(|row| std::cmp::Reverse(row.1.len()));
198 for (verdict, places) in rows {
199 let examples: Vec<String> = places.iter().take(3).map(|p| format!("`{p}`")).collect();
200 writeln!(
201 out,
202 "| {verdict} | {} | {:.1}% | {} |",
203 places.len(),
204 100.0 * places.len() as f64 / pages.max(1) as f64,
205 examples.join("<br>").replace('|', "\\|")
206 )?;
207 }
208 print!("{out}");
209 Ok(())
210}
211
212struct Gpu {
213 renderer: Renderer,
214}
215
216impl Gpu {
217 fn new() -> Result<Self, Box<dyn std::error::Error>> {
218 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
219 let adapter = pollster::block_on(instance.request_adapter(&Default::default()))?;
220 let (device, queue) = pollster::block_on(adapter.request_device(&Default::default()))?;
221 Ok(Self {
222 renderer: Renderer::new(device, queue, wgpu::TextureFormat::Rgba8UnormSrgb),
223 })
224 }
225
226 /// The whole page, 804 pixels across: a phone's width at 2x.
227 fn whole(
228 &mut self,
229 page: &Page,
230 engine: &mut TextEngine,
231 path: &str,
232 ) -> Result<(), Box<dyn std::error::Error>> {
233 let (scene, bounds) = scene(page, engine)?;
234 let [x0, y0, x1, y1] = bounds;
235 let scale = 780.0 / (x1 - x0);
236 let height = (((y1 - y0) * scale) as u32 + 24).min(4000);
237 self.draw(
238 scene,
239 Viewport {
240 size: [804, height],
241 scale,
242 origin: [12.0 - x0 * scale, 12.0 - y0 * scale],
243 },
244 path,
245 )
246 }
247
248 /// The reflowed page as the phone shows it at 100%, 34 points of tag room on its left.
249 fn column(
250 &mut self,
251 page: &Page,
252 column: f32,
253 engine: &mut TextEngine,
254 path: &str,
255 ) -> Result<(), Box<dyn std::error::Error>> {
256 let (scene, [x0, y0, _, y1]) = scene(page, engine)?;
257 let scale = 2.0 * POINT;
258 let _ = column;
259 let height = (((y1 - y0) * scale) as u32 + 48).min(6000);
260 self.draw(
261 scene,
262 Viewport {
263 size: [804, height],
264 scale,
265 origin: [68.0 - x0 * scale, 24.0 - y0 * scale],
266 },
267 path,
268 )
269 }
270
271 fn draw(
272 &mut self,
273 mut scene: PageScene,
274 viewport: Viewport,
275 path: &str,
276 ) -> Result<(), Box<dyn std::error::Error>> {
277 scene.settle(None, viewport.scale, Paper::WHITE);
278 let mut primitives = Vec::new();
279 scene.append_primitives(&mut primitives, [0.0; 2], Paper::WHITE)?;
280 let size = viewport.size;
281 let device = self.renderer.device().clone();
282 let target = device.create_texture(&wgpu::TextureDescriptor {
283 label: None,
284 size: wgpu::Extent3d {
285 width: size[0],
286 height: size[1],
287 depth_or_array_layers: 1,
288 },
289 mip_level_count: 1,
290 sample_count: 1,
291 dimension: wgpu::TextureDimension::D2,
292 format: wgpu::TextureFormat::Rgba8UnormSrgb,
293 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
294 view_formats: &[],
295 });
296 let row_bytes = (size[0] * 4).next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
297 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
298 label: None,
299 size: u64::from(row_bytes) * u64::from(size[1]),
300 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
301 mapped_at_creation: false,
302 });
303 self.renderer
304 .draw(
305 &target.create_view(&Default::default()).into(),
306 size,
307 [1.0; 4],
308 &[viewport.layer(&primitives)],
309 )
310 .map_err(|e| format!("{e:?}"))?;
311 let mut encoder = device.create_command_encoder(&Default::default());
312 encoder.copy_texture_to_buffer(
313 target.as_image_copy(),
314 wgpu::TexelCopyBufferInfo {
315 buffer: &buffer,
316 layout: wgpu::TexelCopyBufferLayout {
317 offset: 0,
318 bytes_per_row: Some(row_bytes),
319 rows_per_image: Some(size[1]),
320 },
321 },
322 wgpu::Extent3d {
323 width: size[0],
324 height: size[1],
325 depth_or_array_layers: 1,
326 },
327 );
328 self.renderer.queue().submit([encoder.finish()]);
329 let (sender, receiver) = std::sync::mpsc::channel();
330 buffer.map_async(wgpu::MapMode::Read, .., move |result| {
331 let _ = sender.send(result);
332 });
333 device.poll(wgpu::PollType::Wait {
334 submission_index: None,
335 timeout: Some(Duration::from_secs(10)),
336 })?;
337 receiver.recv_timeout(Duration::from_secs(10))??;
338 let mapped = buffer.get_mapped_range(..)?;
339 let pixels: Vec<u8> = mapped
340 .chunks_exact(row_bytes as usize)
341 .flat_map(|row| row[..size[0] as usize * 4].iter().copied())
342 .collect();
343 let mut encoder = png::Encoder::new(std::fs::File::create(path)?, size[0], size[1]);
344 encoder.set_color(png::ColorType::Rgba);
345 encoder.set_depth(png::BitDepth::Eight);
346 encoder.write_header()?.write_image_data(&pixels)?;
347 Ok(())
348 }
349}
350
351/// The page's scene, pictures decoded, and its content's bounds with the title.
352fn scene(
353 page: &Page,
354 engine: &mut TextEngine,
355) -> Result<(PageScene, [f32; 4]), Box<dyn std::error::Error>> {
356 let editor = canvas::editor::CanvasEditor::from_page(page.clone(), engine)?;
357 let mut bounds = [
358 f32::INFINITY,
359 f32::INFINITY,
360 f32::NEG_INFINITY,
361 f32::NEG_INFINITY,
362 ];
363 for outline in editor.outlines() {
364 let b = outline.bounds();
365 bounds = [
366 bounds[0].min(b.x0 as f32),
367 bounds[1].min(b.y0 as f32),
368 bounds[2].max(b.x1 as f32),
369 bounds[3].max(b.y1 as f32),
370 ];
371 }
372 for block in reading::read(page, f32::INFINITY, engine)?.blocks {
373 let b = block.bounds;
374 bounds = [
375 bounds[0].min(b[0]),
376 bounds[1].min(b[1]),
377 bounds[2].max(b[2]),
378 bounds[3].max(b[3]),
379 ];
380 }
381 Ok((PageScene::new(page.clone(), engine)?, bounds))
382}