1//! Pictures pasted or inserted through the editor, placed as OneNote 2010 places them (lab,
2//! 2026-09-30): at a caret in text the paragraph splits around the picture, and at a caret
3//! on blank page the picture lies on the page there with the caret on the grid row below.
4//! Each is one edit and one undo step. `CANVAS_PICTURE_EXPORT` names a directory receiving
5//! the section as a notebook, for a cold reopen in OneNote 2010.
6
7use canvas::{
8 document::TextPosition,
9 editor::{CanvasEditor, Piece, Selection},
10 layout::TextEngine,
11};
12use onestore::{
13 Arena, PageCreation, Section, Store,
14 document::Layout,
15 op::{Edit, Op, SectionOp},
16 page::{Image, PageObject, ParagraphContent},
17};
18use parley::Affinity;
19
20fn picture(size: [f32; 2]) -> Image {
21 let mut png = Vec::new();
22 image::RgbaImage::from_pixel(200, 100, image::Rgba([30, 144, 255, 255]))
23 .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
24 .unwrap();
25 Image {
26 id: onestore::page::text::new_id().unwrap(),
27 layout: Layout {
28 max_width: Some(size[0]),
29 max_height: Some(size[1]),
30 ..Default::default()
31 },
32 size: Some(size),
33 bytes: Some(png.into()),
34 display: None,
35 alt: None,
36 background: false,
37 printout: None,
38 tags: Vec::new(),
39 link: None,
40 text: None,
41 }
42}
43
44fn caret(paragraph: usize, offset: u32) -> Selection {
45 let position = TextPosition { paragraph, offset };
46 Selection {
47 positions: [position; 2],
48 affinities: [Affinity::Downstream; 2],
49 }
50}
51
52/// The body outline's paragraphs: text as itself, a picture as its size.
53fn body(editor: &CanvasEditor) -> Vec<String> {
54 let page = editor.page().unwrap();
55 let outline = page
56 .objects
57 .iter()
58 .find_map(|object| match object {
59 PageObject::Outline(outline) if !outline.title => Some(outline),
60 _ => None,
61 })
62 .unwrap();
63 outline
64 .paragraphs
65 .iter()
66 .map(|paragraph| match &paragraph.content {
67 ParagraphContent::Text(text) => text.text.text().to_owned(),
68 ParagraphContent::Image(image) => format!("{:?}", image.size.unwrap()),
69 _ => panic!(),
70 })
71 .collect()
72}
73
74fn page_pictures(editor: &CanvasEditor) -> Vec<Image> {
75 editor
76 .page()
77 .unwrap()
78 .objects
79 .into_iter()
80 .filter_map(|object| match object {
81 PageObject::Image(image) if !image.background => Some(image),
82 _ => None,
83 })
84 .collect()
85}
86
87#[test]
88fn pasted_pictures_split_the_text_or_lie_on_the_page_as_onenote_places_them() {
89 let source = onestore::create_section("pictures.one", "Pasted pictures", "Author").unwrap();
90 let arena = Arena::default();
91 let mut section = Section::open(&arena, source.clone()).unwrap();
92 let (space, ..) = section.pages().unwrap()[0].clone();
93 let mut engine = TextEngine::default();
94 let mut editor = CanvasEditor::from_page(section.page(space).unwrap(), &mut engine).unwrap();
95 let mut at = 133_000_000_000_000_000;
96 let mut store = |editor: &mut CanvasEditor| {
97 let ops = editor.take_ops().unwrap();
98 assert!(!ops.is_empty());
99 at += 10_000_000;
100 let ops = ops.into_iter().map(|op| Op::Page { space, op }).collect();
101 section.apply("Author", &Edit { at, ops }).unwrap();
102 };
103 let text = editor
104 .outlines()
105 .iter()
106 .find(|outline| !outline.title)
107 .unwrap()
108 .id;
109 editor.focus_outline(text).unwrap();
110
111 // Within text, the caret starting the second half.
112 editor.select(caret(0, 6)).unwrap();
113 editor
114 .insert_picture(&mut engine, picture([150.0, 75.0]))
115 .unwrap();
116 store(&mut editor);
117 assert_eq!(body(&editor), ["Pasted", "[150.0, 75.0]", " pictures"]);
118 assert_eq!(
119 editor.selection().positions,
120 [TextPosition {
121 paragraph: 1,
122 offset: 0
123 }; 2]
124 );
125 editor.undo(&mut engine).unwrap();
126 store(&mut editor);
127 assert_eq!(body(&editor), ["Pasted pictures"]);
128
129 // A web page's text and pictures in order, undone in one step.
130 editor.select(caret(0, 7)).unwrap();
131 let png = picture([1.0; 2]).bytes.unwrap().to_vec();
132 editor
133 .paste_pieces(
134 &mut engine,
135 vec![
136 pasted("web&nbsp;"),
137 Piece::Picture(png, [150.0, 75.0]),
138 pasted("page<br>text&nbsp;"),
139 ],
140 )
141 .unwrap();
142 store(&mut editor);
143 assert_eq!(
144 body(&editor),
145 ["Pasted web ", "[150.0, 75.0]", "page", "text ", "pictures"]
146 );
147 editor.undo(&mut engine).unwrap();
148 store(&mut editor);
149 assert_eq!(body(&editor), ["Pasted pictures"]);
150
151 // A web picture still on its way ends the paragraph and goes in when it arrives, an
152 // undo step of its own, while the caret stays.
153 editor.select(caret(0, 7)).unwrap();
154 let awaited = editor
155 .paste_pieces(
156 &mut engine,
157 vec![pasted("web"), Piece::Awaited, pasted("page&nbsp;")],
158 )
159 .unwrap();
160 store(&mut editor);
161 assert_eq!(body(&editor), ["Pasted web", "page pictures"]);
162 let typing = editor.selection();
163 let arrived = picture([150.0, 75.0]);
164 assert!(
165 editor
166 .insert_awaited(&mut engine, awaited[0], arrived)
167 .unwrap()
168 );
169 store(&mut editor);
170 assert_eq!(
171 body(&editor),
172 ["Pasted web", "[150.0, 75.0]", "page pictures"]
173 );
174 assert_eq!(editor.selection(), typing);
175 editor.undo(&mut engine).unwrap();
176 store(&mut editor);
177 assert_eq!(body(&editor), ["Pasted web", "page pictures"]);
178 editor.undo(&mut engine).unwrap();
179 store(&mut editor);
180 assert_eq!(body(&editor), ["Pasted pictures"]);
181
182 // At the end, an empty paragraph follows for the caret; at an empty paragraph's start
183 // the picture takes its place.
184 editor.select(caret(0, 15)).unwrap();
185 editor
186 .insert_picture(&mut engine, picture([150.0, 75.0]))
187 .unwrap();
188 store(&mut editor);
189 editor
190 .insert_picture(&mut engine, picture([200.0, 100.0]))
191 .unwrap();
192 store(&mut editor);
193 assert_eq!(
194 body(&editor),
195 ["Pasted pictures", "[150.0, 75.0]", "[200.0, 100.0]", ""]
196 );
197
198 // On blank page: at the caret, the caret moving to the grid row below (OneNote put a
199 // 75 pt picture at y 266.4 and the caret at 356.4, a 72 pt one at 248.4 and 338.4).
200 for (y, height, below) in [(266.4, 75.0, 356.4), (248.4 + 180.0, 72.0, 338.4 + 180.0)] {
201 editor.place_caret(&mut engine, [270.0, y], 468.0).unwrap();
202 editor
203 .insert_picture(&mut engine, picture([150.0, height]))
204 .unwrap();
205 store(&mut editor);
206 let placed = page_pictures(&editor).pop().unwrap();
207 assert_eq!([placed.layout.x, placed.layout.y], [Some(270.0), Some(y)]);
208 assert_eq!(editor.active_outline().origin(), [270.0, below]);
209 }
210 editor.insert(&mut engine, "below").unwrap();
211 store(&mut editor);
212 editor.undo(&mut engine).unwrap();
213 editor.undo(&mut engine).unwrap();
214 store(&mut editor);
215 assert_eq!(page_pictures(&editor).len(), 1);
216
217 let mut image = source;
218 section.seal().unwrap().unwrap().apply(&mut image).unwrap();
219 let arena = Arena::default();
220 let reopened = Section::open(&arena, image.clone()).unwrap();
221 let page = reopened.page(space).unwrap();
222 let stored: Vec<_> = page
223 .objects
224 .iter()
225 .filter_map(|object| match object {
226 PageObject::Image(image) => Some(image),
227 _ => None,
228 })
229 .collect();
230 assert_eq!(stored.len(), 1);
231 assert_eq!(
232 [stored[0].layout.x, stored[0].layout.y],
233 [Some(270.0), Some(266.4)]
234 );
235 assert_eq!(*stored[0], page_pictures(&editor)[0]);
236
237 if let Some(directory) = std::env::var_os("CANVAS_PICTURE_EXPORT") {
238 let directory = std::path::PathBuf::from(directory);
239 std::fs::create_dir_all(&directory).unwrap();
240 std::fs::write(directory.join("pictures.one"), &image).unwrap();
241 let file_id = Store::parse(&image).unwrap().header.file_id;
242 std::fs::write(
243 directory.join("Open Notebook.onetoc2"),
244 onestore::create_table_of_contents(
245 "Open Notebook.onetoc2",
246 &[("pictures.one", file_id)],
247 )
248 .unwrap(),
249 )
250 .unwrap();
251 }
252}
253
254/// From the title, OneNote 2010 (lab, 2026-09-30) put a pasted picture at the end of the
255/// outline where the body starts; on a page with no body, in a new outline there; and on a
256/// page whose content lay elsewhere, on the page two grid rows below it.
257#[test]
258fn a_picture_pasted_from_the_title_goes_into_the_body() {
259 let source = onestore::create_section("pictures.one", "First page", "Author").unwrap();
260 let arena = Arena::default();
261 let mut section = Section::open(&arena, source).unwrap();
262 let creation = PageCreation::new(None, Some("Title paste"), "Author").unwrap();
263 section
264 .apply(
265 "Author",
266 &Edit {
267 at: 133_000_000_000_000_000,
268 ops: vec![Op::Section(SectionOp::Create(creation))],
269 },
270 )
271 .unwrap();
272 let (space, ..) = section.pages().unwrap()[1].clone();
273 let page = section.page(space).unwrap();
274 let mut engine = TextEngine::default();
275 let opened = |engine: &mut TextEngine| {
276 let mut editor = CanvasEditor::from_page(page.clone(), engine).unwrap();
277 let title = editor.outlines().iter().find(|o| o.title).unwrap().id;
278 editor.focus_outline(title).unwrap();
279 editor
280 };
281
282 // No body: a new outline where it starts.
283 let mut editor = opened(&mut engine);
284 let start = editor.body_start().unwrap();
285 editor
286 .insert_picture(&mut engine, picture([150.0, 75.0]))
287 .unwrap();
288 assert_eq!(body(&editor), ["[150.0, 75.0]", ""]);
289 assert_eq!(editor.active_outline().origin(), start);
290
291 // A body where it starts: at its end.
292 let mut editor = opened(&mut engine);
293 editor.leave_title(&mut engine).unwrap();
294 editor.insert(&mut engine, "body").unwrap();
295 assert_eq!(editor.active_outline().origin(), start);
296 let title = editor.outlines().iter().find(|o| o.title).unwrap().id;
297 editor.focus_outline(title).unwrap();
298 editor
299 .insert_picture(&mut engine, picture([150.0, 75.0]))
300 .unwrap();
301 assert_eq!(body(&editor), ["body", "[150.0, 75.0]", ""]);
302
303 // Content elsewhere: on the page two grid rows below it.
304 let mut editor = opened(&mut engine);
305 let margin = editor.margin_origin();
306 editor
307 .place_caret(&mut engine, [start[0] + 270.0, start[1] + 72.0], 468.0)
308 .unwrap();
309 editor.insert(&mut engine, "far").unwrap();
310 let bottom = editor.active_outline().bounds().y1 as f32;
311 let title = editor.outlines().iter().find(|o| o.title).unwrap().id;
312 editor.focus_outline(title).unwrap();
313 editor
314 .insert_picture(&mut engine, picture([150.0, 75.0]))
315 .unwrap();
316 let placed = page_pictures(&editor).pop().unwrap();
317 let row = ((bottom - margin[1]) / 18.0).floor() + 2.0;
318 assert_eq!(
319 [placed.layout.x, placed.layout.y],
320 [Some(start[0]), Some(margin[1] + row * 18.0)]
321 );
322}
323
324/// Pasted text, as a web page gives it.
325fn pasted(html: &str) -> Piece {
326 canvas::editor::html_pieces(html, 0x0409, |_, _| None)
327 .pop()
328 .unwrap()
329}