1//! Pictures as OneNote 2010 stores them in the wild.
2
3use canvas::{editor::CanvasEditor, layout::TextEngine};
4use onestore::{
5 Arena, Section,
6 op::{Edit, Op},
7 page::{Page, PageObject, ParagraphContent},
8};
9
10fn page(bytes: &[u8], title: &str) -> Page {
11 let arena = Arena::default();
12 let mut section = Section::open(&arena, bytes.to_vec()).unwrap();
13 let (space, ..) = section
14 .pages()
15 .unwrap()
16 .into_iter()
17 .find(|page| page.1 == title)
18 .unwrap();
19 section.page(space).unwrap()
20}
21
22/// A picture COM put on the page stores no layout size, only its own (PictureWidth and
23/// PictureHeight), and shows at that size (`corpus/print`).
24#[test]
25fn a_page_picture_without_a_layout_size_shows_at_its_own() {
26 let page = page(
27 include_bytes!("../../../corpus/print/native/Print.one"),
28 "Printing test",
29 );
30 let [picture] = &page
31 .objects
32 .iter()
33 .filter_map(|object| match object {
34 PageObject::Image(image) => Some(image),
35 _ => None,
36 })
37 .collect::<Vec<_>>()[..]
38 else {
39 panic!("one picture");
40 };
41 assert_eq!(picture.layout.max_width, None);
42 let mut engine = TextEngine::default();
43 let editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
44 let origin = [picture.layout.x.unwrap(), picture.layout.y.unwrap()];
45 assert_eq!(
46 editor.image_placement(picture.id),
47 Some((origin, [150.0, 90.0]))
48 );
49 #[cfg(feature = "gpu")]
50 {
51 let (scene, editor) = canvas::gpu::page::PageScene::from_page(page, &mut engine).unwrap();
52 assert_eq!(scene.read_only(Some(&editor)).count(), 0);
53 }
54}
55
56const OBJECTS: &[u8] = include_bytes!("../../../corpus/object-tags/native/notebook/files.one");
57
58/// The picture or file `id` holds, wherever it is on the page.
59fn tags_of(page: &Page, id: onestore::ExGuid) -> Vec<onestore::document::Tag> {
60 let contents = page.objects.iter().flat_map(|object| match object {
61 PageObject::Outline(outline) => outline
62 .paragraphs
63 .iter()
64 .map(|paragraph| match &paragraph.content {
65 ParagraphContent::Image(image) => (image.id, image.tags.clone()),
66 ParagraphContent::Attachment(file) => (file.id, file.tags.clone()),
67 _ => (paragraph.id, Vec::new()),
68 })
69 .collect(),
70 PageObject::Image(image) => vec![(image.id, image.tags.clone())],
71 PageObject::Attachment(file) => vec![(file.id, file.tags.clone())],
72 _ => Vec::new(),
73 });
74 contents
75 .filter(|(object, _)| *object == id)
76 .map(|(_, tags)| tags)
77 .next()
78 .unwrap()
79}
80
81/// OneNote 2010 stores a tag given to a picture or file on the object itself, in an
82/// outline or on the page, and draws it in a column to its left centred on it; a click on a
83/// check box checks it, one revision each, and undo clears it again
84/// (`corpus/object-tags`). `CANVAS_OBJECT_TAGS_EXPORT` names a new directory receiving the
85/// candidate for a cold reopen.
86#[test]
87fn tags_on_pictures_and_files_draw_and_check() {
88 let arena = Arena::default();
89 let mut section = Section::open(&arena, OBJECTS.to_vec()).unwrap();
90 let (space, ..) = section
91 .pages()
92 .unwrap()
93 .into_iter()
94 .find(|page| page.1 == "Pictures and files")
95 .unwrap();
96 let page = section.page(space).unwrap();
97 let outline = page
98 .objects
99 .iter()
100 .find_map(|object| match object {
101 PageObject::Outline(outline) if outline.paragraphs.len() == 4 => Some(outline),
102 _ => None,
103 })
104 .unwrap();
105 let [(picture_paragraph, picture), (_, file)] = [1, 2].map(|index| {
106 let paragraph = &outline.paragraphs[index];
107 let id = match &paragraph.content {
108 ParagraphContent::Image(image) => image.id,
109 ParagraphContent::Attachment(file) => file.id,
110 _ => panic!("a picture and a file"),
111 };
112 (paragraph.id, id)
113 });
114 let outline_id = outline.id;
115 let (page_picture, page_file) = (
116 page.objects
117 .iter()
118 .find_map(|object| match object {
119 PageObject::Image(image) if !image.tags.is_empty() => Some(image.id),
120 _ => None,
121 })
122 .unwrap(),
123 page.objects
124 .iter()
125 .find_map(|object| match object {
126 PageObject::Attachment(file) => Some(file.id),
127 _ => None,
128 })
129 .unwrap(),
130 );
131 for id in [picture, file, page_picture, page_file] {
132 let tags = tags_of(&page, id);
133 assert_eq!(tags.len(), 1);
134 assert_eq!(tags[0].status & 1, 0);
135 assert!(page.definitions.contains_key(&tags[0].definition.unwrap()));
136 }
137
138 let mut engine = TextEngine::default();
139 let mut editor = CanvasEditor::from_page(page.clone(), &mut engine).unwrap();
140 // The outline's tags stand beside the picture and the file they mark.
141 let shaped = editor
142 .outlines()
143 .iter()
144 .find(|item| item.id == outline_id)
145 .unwrap()
146 .shaped();
147 let marked: Vec<_> = shaped.tags().map(|(paragraph, ..)| paragraph).collect();
148 assert_eq!(marked, [picture_paragraph, outline.paragraphs[2].id]);
149 #[cfg(feature = "gpu")]
150 {
151 // The page picture at (400, 86), 120 by 90, has its tag 24.75 points to its left,
152 // centred on it.
153 let (scene, shown) =
154 canvas::gpu::page::PageScene::from_page(page.clone(), &mut engine).unwrap();
155 let hit = |point| scene.hit_test(point, Some(&shown), |_| None::<()>);
156 assert_eq!(
157 hit([376.0, 126.0]),
158 Some(canvas::gpu::page::SceneHit::Check(page_picture))
159 );
160 assert_eq!(
161 hit([374.0, 126.0]),
162 None,
163 "the tag's column starts 24.75 points left"
164 );
165 }
166
167 let save = |editor: &mut CanvasEditor, section: &mut Section| {
168 let ops = editor
169 .take_ops()
170 .unwrap()
171 .into_iter()
172 .map(|op| Op::Page { space, op })
173 .collect();
174 section
175 .apply(
176 "Author",
177 &Edit {
178 at: 134_000_000_000_000_000,
179 ops,
180 },
181 )
182 .unwrap();
183 section.seal().unwrap().unwrap();
184 };
185 editor.click_object_check(page_picture).unwrap();
186 save(&mut editor, &mut section);
187 editor.click_object_check(page_file).unwrap();
188 save(&mut editor, &mut section);
189 editor
190 .click_check(&mut engine, outline_id, picture_paragraph)
191 .unwrap();
192 save(&mut editor, &mut section);
193 let stored = section.page(space).unwrap();
194 for (id, checked) in [
195 (page_picture, true),
196 (page_file, true),
197 (picture, true),
198 (file, false),
199 ] {
200 assert_eq!(tags_of(&stored, id)[0].status & 1 != 0, checked, "{id:?}");
201 }
202 if let Some(directory) = std::env::var_os("CANVAS_OBJECT_TAGS_EXPORT") {
203 let directory = std::path::PathBuf::from(directory);
204 std::fs::create_dir(&directory).unwrap();
205 let written = section.image();
206 std::fs::write(directory.join("files.one"), &written).unwrap();
207 let file_id = onestore::Store::parse(&written).unwrap().header.file_id;
208 std::fs::write(
209 directory.join("Open Notebook.onetoc2"),
210 onestore::create_table_of_contents("Open Notebook.onetoc2", &[("files.one", file_id)])
211 .unwrap(),
212 )
213 .unwrap();
214 }
215 assert!(editor.undo(&mut engine).unwrap());
216 assert!(editor.undo(&mut engine).unwrap());
217 save(&mut editor, &mut section);
218 let stored = section.page(space).unwrap();
219 assert_eq!(tags_of(&stored, page_file)[0].status & 1, 0);
220 assert_eq!(tags_of(&stored, picture)[0].status & 1, 0);
221 assert_eq!(tags_of(&stored, page_picture)[0].status & 1, 1);
222}
223
224/// Text OneNote recognised in a picture is found by search, as OneNote finds it
225/// (`corpus/object-tags`).
226#[test]
227fn search_finds_text_recognised_in_pictures() {
228 let page = page(OBJECTS, "Pictures and files");
229 let recognised: Vec<_> = page
230 .objects
231 .iter()
232 .filter_map(|object| match object {
233 PageObject::Image(image) => image.text.as_ref().map(|text| text.text.as_str()),
234 _ => None,
235 })
236 .collect();
237 assert_eq!(recognised, ["GLACIER HARBOR\nInvoice 4471"]);
238 let mut index = canvas::search::Index::default();
239 index.set(canvas::search::Entry::new(
240 "files",
241 onestore::ExGuid::default(),
242 &page,
243 0,
244 ));
245 let found = index.search(&canvas::search::Query::new("invoice 4471"), |_| true);
246 assert_eq!(found.len(), 1);
247 assert_eq!(found[0].snippet, "Invoice 4471");
248 assert!(
249 index
250 .search(&canvas::search::Query::new("harb"), |_| true)
251 .len()
252 == 1
253 );
254}
255
256/// A picture's link reads back from where OneNote stores it (WzHyperlinkUrl), and a linked
257/// picture deleted and brought back by undo keeps it (`corpus/picture-link`).
258/// `CANVAS_PICTURE_LINK_EXPORT` names a new directory receiving the candidate for a cold
259/// reopen.
260#[test]
261fn a_linked_picture_keeps_its_link_through_undo() {
262 let source = include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one");
263 let arena = Arena::default();
264 let mut section = Section::open(&arena, source.to_vec()).unwrap();
265 let (space, ..) = section
266 .pages()
267 .unwrap()
268 .into_iter()
269 .find(|page| page.1 == "Image png")
270 .unwrap();
271 let page = section.page(space).unwrap();
272 let linked = |page: &Page| -> Vec<(onestore::ExGuid, Option<String>)> {
273 page.objects
274 .iter()
275 .filter_map(|object| match object {
276 PageObject::Image(image) => Some((image.id, image.link.clone())),
277 _ => None,
278 })
279 .collect()
280 };
281 let before = linked(&page);
282 let [(id, Some(link))] = &before[..] else {
283 panic!("one linked picture");
284 };
285 assert_eq!(link, "https://example.invalid/image/png");
286 let mut engine = TextEngine::default();
287 let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap();
288 editor.remove_image(&mut engine, *id).unwrap();
289 assert!(editor.undo(&mut engine).unwrap());
290 let ops = editor
291 .take_ops()
292 .unwrap()
293 .into_iter()
294 .map(|op| Op::Page { space, op })
295 .collect();
296 section
297 .apply(
298 "Author",
299 &Edit {
300 at: 134_000_000_000_000_000,
301 ops,
302 },
303 )
304 .unwrap();
305 section.seal().unwrap();
306 assert_eq!(linked(&section.page(space).unwrap()), before);
307 if let Some(directory) = std::env::var_os("CANVAS_PICTURE_LINK_EXPORT") {
308 let directory = std::path::PathBuf::from(directory);
309 std::fs::create_dir(&directory).unwrap();
310 let written = section.image();
311 std::fs::write(directory.join("Features.one"), &written).unwrap();
312 let file_id = onestore::Store::parse(&written).unwrap().header.file_id;
313 std::fs::write(
314 directory.join("Open Notebook.onetoc2"),
315 onestore::create_table_of_contents(
316 "Open Notebook.onetoc2",
317 &[("Features.one", file_id)],
318 )
319 .unwrap(),
320 )
321 .unwrap();
322 }
323}
324
325/// A tag command on a selected picture or file tags the object itself, as OneNote 2010
326/// does (`corpus/object-tags`): a tag it has comes off, one it lacks goes on unchecked.
327#[test]
328fn tag_commands_tag_a_selected_picture_or_file() {
329 use canvas::editor::{Formatting, NoteTag};
330 let arena = Arena::default();
331 let mut section = Section::open(&arena, OBJECTS.to_vec()).unwrap();
332 let (space, ..) = section
333 .pages()
334 .unwrap()
335 .into_iter()
336 .find(|page| page.1 == "Pictures and files")
337 .unwrap();
338 let page = section.page(space).unwrap();
339 let page_picture = page
340 .objects
341 .iter()
342 .find_map(|object| match object {
343 PageObject::Image(image) if !image.tags.is_empty() => Some(image.id),
344 _ => None,
345 })
346 .unwrap();
347 let file = page
348 .objects
349 .iter()
350 .find_map(|object| match object {
351 PageObject::Outline(outline) => {
352 outline.paragraphs.iter().find_map(|p| match &p.content {
353 ParagraphContent::Attachment(file) => Some(file.id),
354 _ => None,
355 })
356 }
357 _ => None,
358 })
359 .unwrap();
360 let mut engine = TextEngine::default();
361 let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap();
362 let to_do = Formatting::Tag(NoteTag::defaults()[0].clone(), 0);
363 let important = Formatting::Tag(NoteTag::defaults()[1].clone(), 1);
364 assert!(
365 editor
366 .format_object(&mut engine, page_picture, &to_do)
367 .unwrap()
368 );
369 assert!(editor.format_object(&mut engine, file, &important).unwrap());
370 assert!(
371 !editor
372 .format_object(&mut engine, file, &Formatting::Bullets)
373 .unwrap()
374 );
375 let ops = editor
376 .take_ops()
377 .unwrap()
378 .into_iter()
379 .map(|op| Op::Page { space, op })
380 .collect();
381 section
382 .apply(
383 "Author",
384 &Edit {
385 at: 134_000_000_000_000_000,
386 ops,
387 },
388 )
389 .unwrap();
390 section.seal().unwrap();
391 let stored = section.page(space).unwrap();
392 assert!(tags_of(&stored, page_picture).is_empty());
393 let tags = tags_of(&stored, file);
394 assert_eq!(tags.len(), 2);
395 assert!(matches!(
396 &stored.definitions[&tags[0].definition.unwrap()].kind,
397 onestore::document::Kind::TagDefinition { label: Some(label), .. } if label == "Important"
398 ));
399}
400
401/// A picture deleted and brought back by undo keeps the text OneNote recognised in it,
402/// written back as OneNote stores it (`corpus/object-tags/restored`).
403/// `CANVAS_RECOGNIZED_EXPORT` names a new directory receiving the candidate for a cold
404/// reopen.
405#[test]
406fn a_restored_picture_keeps_its_recognised_text() {
407 let arena = Arena::default();
408 let mut section = Section::open(&arena, OBJECTS.to_vec()).unwrap();
409 let (space, ..) = section
410 .pages()
411 .unwrap()
412 .into_iter()
413 .find(|page| page.1 == "Pictures and files")
414 .unwrap();
415 let page = section.page(space).unwrap();
416 let recognized = |page: &Page| {
417 page.objects
418 .iter()
419 .find_map(|object| match object {
420 PageObject::Image(image) if image.text.is_some() => {
421 Some((image.id, image.text.clone().unwrap()))
422 }
423 _ => None,
424 })
425 .unwrap()
426 };
427 let (id, text) = recognized(&page);
428 assert_eq!(text.stored.len(), 2, "its language and word layout");
429 let mut engine = TextEngine::default();
430 let mut editor = CanvasEditor::from_page(page, &mut engine).unwrap();
431 editor.remove_image(&mut engine, id).unwrap();
432 assert!(editor.undo(&mut engine).unwrap());
433 let ops = editor
434 .take_ops()
435 .unwrap()
436 .into_iter()
437 .map(|op| Op::Page { space, op })
438 .collect();
439 section
440 .apply(
441 "Author",
442 &Edit {
443 at: 134_000_000_000_000_000,
444 ops,
445 },
446 )
447 .unwrap();
448 section.seal().unwrap();
449 assert_eq!(recognized(&section.page(space).unwrap()), (id, text));
450 if let Some(directory) = std::env::var_os("CANVAS_RECOGNIZED_EXPORT") {
451 let directory = std::path::PathBuf::from(directory);
452 std::fs::create_dir(&directory).unwrap();
453 let written = section.image();
454 std::fs::write(directory.join("files.one"), &written).unwrap();
455 let file_id = onestore::Store::parse(&written).unwrap().header.file_id;
456 std::fs::write(
457 directory.join("Open Notebook.onetoc2"),
458 onestore::create_table_of_contents("Open Notebook.onetoc2", &[("files.one", file_id)])
459 .unwrap(),
460 )
461 .unwrap();
462 }
463}