1#[path = "support/ops.rs"]
2mod ops;
3use onestore::{
4 ExGuid, RevisionIndex, Store,
5 document::{Document, Format, Layout},
6 page::{Image, Page, PageObject, PageParagraph, ParagraphContent, TextObject, text::new_id},
7};
8use std::sync::Arc;
9
10const NATIVE: &[u8] =
11 include_bytes!("../../../corpus/native/cold-05-05-image/notebook/synthetic.one");
12const NATIVE_RESIZE: &[u8] =
13 include_bytes!("../../../corpus/picture-edit/native-resize/notebook/pictures.one");
14const NATIVE_PAGE_LEVEL: &[u8] =
15 include_bytes!("../../../corpus/picture-edit/native-page-level/notebook/pictures.one");
16/// A one-pixel PNG.
17const PNG: &[u8] = &[
18 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
19 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
20 0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
21 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
22 0x44, 0xae, 0x42, 0x60, 0x82,
23];
24
25fn first_page(bytes: &[u8]) -> (ExGuid, Page) {
26 let store = Store::parse(bytes).unwrap();
27 assert!(store.checksum_mismatches.is_empty());
28 let index = RevisionIndex::parse(&store).unwrap();
29 let document = Document::parse(&index).unwrap();
30 let (space, _) = document.pages().unwrap()[0];
31 (space, Page::from_space(&document, space).unwrap())
32}
33
34fn page_in(bytes: &[u8], space: ExGuid) -> Page {
35 let store = Store::parse(bytes).unwrap();
36 assert!(store.checksum_mismatches.is_empty());
37 let index = RevisionIndex::parse(&store).unwrap();
38 Page::from_space(&Document::parse(&index).unwrap(), space).unwrap()
39}
40
41fn body_paragraphs(page: &mut Page) -> &mut Vec<PageParagraph> {
42 page.objects
43 .iter_mut()
44 .find_map(|object| match object {
45 PageObject::Outline(outline) if !outline.title => Some(&mut outline.paragraphs),
46 _ => None,
47 })
48 .unwrap()
49}
50
51fn pictures(page: &Page) -> Vec<&Image> {
52 page.objects
53 .iter()
54 .filter_map(|object| match object {
55 PageObject::Outline(outline) => Some(outline),
56 _ => None,
57 })
58 .flat_map(|outline| outline.paragraphs.iter())
59 .filter_map(|p| match &p.content {
60 ParagraphContent::Image(image) => Some(image),
61 _ => None,
62 })
63 .collect()
64}
65
66fn plain_paragraph(template: &PageParagraph, text: &str) -> PageParagraph {
67 let mut paragraph = template.clone();
68 paragraph.id = new_id().unwrap();
69 paragraph.parent = None;
70 paragraph.level = 1;
71 paragraph.lists.clear();
72 paragraph.tags.clear();
73 paragraph.style = None;
74 paragraph.collapsed = false;
75 paragraph.content = ParagraphContent::Text(TextObject {
76 id: new_id().unwrap(),
77 date_field: None,
78 text: onestore::page::Paragraph::new(
79 text.into(),
80 Format {
81 font: Some("Calibri".into()),
82 font_size: Some(11.0),
83 language: Some(1033),
84 ..Default::default()
85 },
86 ),
87 tags: Vec::new(),
88 });
89 paragraph
90}
91
92#[test]
93fn a_native_picture_reads_as_paragraph_content_with_its_payload() {
94 let (_, page) = first_page(NATIVE);
95 let found = pictures(&page);
96 let [picture] = found.as_slice() else {
97 panic!("{page:?}");
98 };
99 assert_eq!(picture.bytes.as_ref().map(|b| b.len()), Some(68));
100 assert!(picture.bytes.as_ref().unwrap().starts_with(&PNG[..8]));
101 assert_eq!(picture.size, Some([0.75, 0.75]));
102}
103
104/// `ONESTORE_IMAGE_EXPORT` names a new directory receiving the candidate for a cold reopen.
105#[test]
106fn a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed() {
107 let source = onestore::create_section("pictures.one", "Before the picture", "Author").unwrap();
108 let (space, before) = first_page(&source);
109 let mut after = before.clone();
110 let template = body_paragraphs(&mut after)[0].clone();
111 let mut holder = plain_paragraph(&template, "");
112 holder.format = Format::default();
113 let image = Image {
114 id: new_id().unwrap(),
115 layout: Default::default(),
116 size: Some([0.75, 0.75]),
117 bytes: Some(Arc::from(PNG)),
118 display: None,
119 alt: None,
120 background: false,
121 printout: None,
122 tags: Vec::new(),
123 link: None,
124 text: None,
125 };
126 holder.content = ParagraphContent::Image(image.clone());
127 body_paragraphs(&mut after).push(holder.clone());
128 body_paragraphs(&mut after).push(plain_paragraph(&template, "After the picture"));
129 let written = ops::saved(&source, space, &after).unwrap();
130 let stored = page_in(written.as_slice(), space);
131 let mut expected = after.clone();
132 expected.title = stored.title.clone();
133 assert_eq!(stored, expected);
134 let found = pictures(&stored);
135 let [picture] = found.as_slice() else {
136 panic!()
137 };
138 assert_eq!(picture.bytes.as_deref(), Some(PNG));
139 assert_eq!(
140 ops::saved(written.as_slice(), space, &stored)
141 .unwrap()
142 .as_slice(),
143 written.as_slice()
144 );
145 if let Some(directory) = std::env::var_os("ONESTORE_IMAGE_EXPORT") {
146 let directory = std::path::PathBuf::from(directory);
147 std::fs::create_dir(&directory).unwrap();
148 std::fs::write(directory.join("pictures.one"), written.as_slice()).unwrap();
149 let written_store = Store::parse(written.as_slice()).unwrap();
150 std::fs::write(
151 directory.join("Open Notebook.onetoc2"),
152 onestore::create_table_of_contents(
153 "Open Notebook.onetoc2",
154 &[("pictures.one", written_store.header.file_id)],
155 )
156 .unwrap(),
157 )
158 .unwrap();
159 }
160 let mut removed = stored.clone();
161 body_paragraphs(&mut removed).retain(|p| p.id != holder.id);
162 let again = ops::saved(written.as_slice(), space, &removed).unwrap();
163 let stored = page_in(again.as_slice(), space);
164 assert!(pictures(&stored).is_empty());
165 assert_eq!(body_paragraphs(&mut stored.clone()).len(), 2);
166}
167
168fn resize(page: &mut Page, layout: Layout, alt: Option<&str>) -> ExGuid {
169 let mut found = None;
170 for object in &mut page.objects {
171 if let PageObject::Outline(outline) = object {
172 for paragraph in &mut outline.paragraphs {
173 if let ParagraphContent::Image(image) = &mut paragraph.content {
174 image.layout = layout.clone();
175 image.alt = alt.map(str::to_owned);
176 assert!(found.replace(image.id).is_none());
177 }
178 }
179 }
180 }
181 found.unwrap()
182}
183
184/// `ONESTORE_IMAGE_RESIZE_EXPORT` names a new directory receiving the candidate for a cold reopen.
185#[test]
186fn a_native_picture_is_resized_and_described_then_reset() {
187 let (space, native) = first_page(NATIVE);
188 let mut resized = native.clone();
189 let layout = Layout {
190 max_width: Some(144.0),
191 width_set_by_user: Some(true),
192 max_height: Some(108.0),
193 ..Default::default()
194 };
195 resize(&mut resized, layout, Some("Resized in Rust"));
196 let written = ops::saved(NATIVE, space, &resized).unwrap();
197 let stored = page_in(written.as_slice(), space);
198 assert_eq!(stored, resized);
199 assert_eq!(
200 ops::saved(written.as_slice(), space, &stored)
201 .unwrap()
202 .as_slice(),
203 written.as_slice()
204 );
205 if let Some(directory) = std::env::var_os("ONESTORE_IMAGE_RESIZE_EXPORT") {
206 let directory = std::path::PathBuf::from(directory);
207 std::fs::create_dir(&directory).unwrap();
208 std::fs::write(directory.join("synthetic.one"), written.as_slice()).unwrap();
209 let written_store = Store::parse(written.as_slice()).unwrap();
210 std::fs::write(
211 directory.join("Open Notebook.onetoc2"),
212 onestore::create_table_of_contents(
213 "Open Notebook.onetoc2",
214 &[("synthetic.one", written_store.header.file_id)],
215 )
216 .unwrap(),
217 )
218 .unwrap();
219 }
220 let mut reset = stored.clone();
221 resize(&mut reset, Layout::default(), None);
222 let again = ops::saved(written.as_slice(), space, &reset).unwrap();
223 assert_eq!(page_in(again.as_slice(), space), native);
224}
225
226#[test]
227fn a_rust_picture_resized_natively_reads_back_with_its_size_and_description() {
228 let (space, page) = first_page(NATIVE_RESIZE);
229 let found = pictures(&page);
230 let [picture] = found.as_slice() else {
231 panic!("{page:?}");
232 };
233 assert_eq!(picture.bytes.as_deref(), Some(PNG));
234 assert_eq!(picture.size, Some([0.75, 0.75]));
235 assert_eq!(picture.layout.max_width, Some(144.0));
236 assert_eq!(picture.layout.max_height, Some(108.0));
237 assert_eq!(picture.layout.width_set_by_user, Some(true));
238 assert_eq!(picture.alt.as_deref(), Some("Resized by OneNote"));
239 let mut wider = page.clone();
240 let mut layout = picture.layout.clone();
241 layout.max_width = Some(200.0);
242 resize(&mut wider, layout, Some("Wider in Rust"));
243 let written = ops::saved(NATIVE_RESIZE, space, &wider).unwrap();
244 assert_eq!(page_in(written.as_slice(), space), wider);
245}
246
247fn page_pictures(page: &Page) -> Vec<&Image> {
248 page.objects
249 .iter()
250 .filter_map(|object| match object {
251 PageObject::Image(image) => Some(image),
252 _ => None,
253 })
254 .collect()
255}
256
257#[test]
258fn a_page_level_picture_placed_natively_reads_as_a_page_object() {
259 let (space, page) = first_page(NATIVE_PAGE_LEVEL);
260 assert_eq!(pictures(&page).len(), 1);
261 let found = page_pictures(&page);
262 let [picture] = found.as_slice() else {
263 panic!("{page:?}");
264 };
265 assert_eq!(picture.bytes.as_deref(), Some(PNG));
266 assert_eq!(
267 (picture.layout.x, picture.layout.y),
268 (Some(360.0), Some(240.0))
269 );
270 assert_eq!(picture.layout.width_set_by_user, Some(true));
271 assert_eq!(picture.alt.as_deref(), Some("Page-level picture"));
272 let mut moved = page.clone();
273 for object in &mut moved.objects {
274 if let PageObject::Image(image) = object {
275 image.layout.x = Some(72.0);
276 image.layout.y = Some(400.0);
277 }
278 }
279 let written = ops::saved(NATIVE_PAGE_LEVEL, space, &moved).unwrap();
280 assert_eq!(page_in(written.as_slice(), space), moved);
281}
282
283/// `ONESTORE_PAGE_IMAGE_EXPORT` names a new directory receiving the candidate for a cold reopen.
284#[test]
285fn a_page_level_picture_is_inserted_moved_and_removed() {
286 let source = onestore::create_section("pictures.one", "Beside the picture", "Author").unwrap();
287 let (space, before) = first_page(&source);
288 let mut after = before.clone();
289 let image = Image {
290 id: new_id().unwrap(),
291 layout: Layout {
292 x: Some(360.0),
293 y: Some(240.0),
294 max_width: Some(96.0),
295 width_set_by_user: Some(true),
296 max_height: Some(72.0),
297 reserved_width: None,
298 },
299 size: Some([0.75, 0.75]),
300 bytes: Some(Arc::from(PNG)),
301 display: None,
302 alt: Some("Placed in Rust".into()),
303 background: false,
304 printout: None,
305 tags: Vec::new(),
306 link: None,
307 text: None,
308 };
309 after.objects.push(PageObject::Image(image.clone()));
310 let written = ops::saved(&source, space, &after).unwrap();
311 let stored = page_in(written.as_slice(), space);
312 let mut expected = after.clone();
313 expected.title = stored.title.clone();
314 assert_eq!(stored, expected);
315 assert_eq!(page_pictures(&stored)[0].bytes.as_deref(), Some(PNG));
316 assert_eq!(
317 ops::saved(written.as_slice(), space, &stored)
318 .unwrap()
319 .as_slice(),
320 written.as_slice()
321 );
322 if let Some(directory) = std::env::var_os("ONESTORE_PAGE_IMAGE_EXPORT") {
323 let directory = std::path::PathBuf::from(directory);
324 std::fs::create_dir(&directory).unwrap();
325 std::fs::write(directory.join("pictures.one"), written.as_slice()).unwrap();
326 let written_store = Store::parse(written.as_slice()).unwrap();
327 std::fs::write(
328 directory.join("Open Notebook.onetoc2"),
329 onestore::create_table_of_contents(
330 "Open Notebook.onetoc2",
331 &[("pictures.one", written_store.header.file_id)],
332 )
333 .unwrap(),
334 )
335 .unwrap();
336 }
337 let mut moved = stored.clone();
338 for object in &mut moved.objects {
339 if let PageObject::Image(image) = object {
340 image.layout.x = Some(36.0);
341 image.alt = None;
342 }
343 }
344 let again = ops::saved(written.as_slice(), space, &moved).unwrap();
345 assert_eq!(page_in(again.as_slice(), space), moved);
346 let mut removed = moved.clone();
347 removed.objects.retain(|object| object.id() != image.id);
348 let last = ops::saved(again.as_slice(), space, &removed).unwrap();
349 let stored = page_in(last.as_slice(), space);
350 assert!(page_pictures(&stored).is_empty());
351 assert_eq!(stored, removed);
352 let mut unplaced = stored.clone();
353 let mut nowhere = image.clone();
354 nowhere.layout = Layout::default();
355 unplaced.objects.push(PageObject::Image(nowhere));
356 assert!(ops::saved(last.as_slice(), space, &unplaced).is_err());
357}
358
359#[test]
360fn pictures_need_a_recognised_payload_and_stored_ones_stay_fixed() {
361 let source = onestore::create_section("pictures.one", "Text", "Author").unwrap();
362 let (space, before) = first_page(&source);
363 let mut after = before.clone();
364 let template = body_paragraphs(&mut after)[0].clone();
365 let mut holder = plain_paragraph(&template, "");
366 holder.content = ParagraphContent::Image(Image {
367 id: new_id().unwrap(),
368 layout: Default::default(),
369 size: None,
370 bytes: Some(Arc::from(b"not a picture".as_slice())),
371 display: None,
372 alt: None,
373 background: false,
374 printout: None,
375 tags: Vec::new(),
376 link: None,
377 text: None,
378 });
379 body_paragraphs(&mut after).push(holder);
380 assert!(ops::saved(&source, space, &after).is_err());
381 let (space, native) = first_page(NATIVE);
382 let mut resized = native.clone();
383 let mut found = 0;
384 for object in &mut resized.objects {
385 if let PageObject::Outline(outline) = object {
386 for paragraph in &mut outline.paragraphs {
387 if let ParagraphContent::Image(image) = &mut paragraph.content {
388 image.size = Some([10.0, 10.0]);
389 found += 1;
390 }
391 }
392 }
393 }
394 assert_eq!(found, 1);
395 assert!(ops::saved(NATIVE, space, &resized).is_err());
396}