1//! Paragraph and page content other than text: pictures, attachments, ink and equations,
2//! written as OneNote 2010 stores them.
3
4use super::Values;
5use crate::{
6 Error, ExGuid,
7 active::{ActivePage, Changes},
8 document::{Format, Layout},
9 page::{Attachment, Image, Ink, InkStroke, Paragraph, Recording},
10 write::PropertyObject,
11};
12use std::collections::BTreeMap;
13
14fn invalid(message: &'static str) -> Error {
15 Error { offset: 0, message }
16}
17
18/// The `<ifndf>{GUID}` form a file-data declaration uses to name a payload in the file-data store.
19pub(crate) fn payload_reference(guid: [u8; 16]) -> String {
20 let id = ExGuid { guid, n: 0 }.to_string();
21 format!("<ifndf>{}", id.split(',').next().unwrap())
22}
23
24/// The compact child references a page object holds.
25fn page_children(object: &PropertyObject) -> Result<Vec<u8>, Error> {
26 let properties = crate::PropertySets::parse(&object.bytes)?;
27 match properties.sets[0]
28 .iter()
29 .find(|p| p.id == 0x24001c20)
30 .map(|p| &p.value)
31 {
32 Some(crate::Value::References { compact_ids, .. }) => Ok(compact_ids.to_vec()),
33 None => Ok(Vec::new()),
34 _ => Err(invalid("The page has an invalid child list")),
35 }
36}
37
38/// Makes `object` the content of paragraph `holder`, or a child of the page after its others.
39fn hold(
40 active: &ActivePage<'_>,
41 changed: &mut Changes,
42 object: ExGuid,
43 holder: Option<ExGuid>,
44 modified: &[u8; 4],
45) -> Result<(), Error> {
46 let raw = &active.live.revision;
47 match holder {
48 Some(holder) => {
49 let mut paragraph = PropertyObject::from_object(&raw.objects[&holder])?;
50 let content = paragraph.reference(object)?;
51 paragraph.set(&[(0x24001c1f, &content), (0x14001d7a, modified)])?;
52 changed.insert(holder, paragraph);
53 }
54 None => {
55 let [page] = active.pages[..] else {
56 return Err(invalid("Choose an object space containing one active page"));
57 };
58 let mut node = PropertyObject::from_object(&raw.objects[&page])?;
59 let mut children = page_children(&node)?;
60 children.extend(node.reference(object)?);
61 node.set(&[(0x24001c20, &children), (0x14001d7a, modified)])?;
62 changed.insert(page, node);
63 }
64 }
65 Ok(())
66}
67
68/// The character-style properties a span format sets; paragraph-level fields stay on the
69/// paragraph style.
70pub(crate) fn style_values(format: &Format) -> Values {
71 let mut values = Values::new();
72 for (id, flag) in [
73 (0x08001c04, format.bold),
74 (0x08001c05, format.italic),
75 (0x08001c06, format.underline),
76 (0x08001c07, format.strike),
77 (0x08001c08, format.superscript),
78 (0x08001c09, format.subscript),
79 (0x08001e16, format.hidden),
80 (0x08001e14, format.hyperlink),
81 (0x08001e19, format.hyperlink_label),
82 (0x08003401, format.math),
83 (0x08001e22, format.embedded_object),
84 ] {
85 if let Some(flag) = flag {
86 values.push((id | (u32::from(flag) << 31), Vec::new()));
87 }
88 }
89 if let Some(font) = &format.font {
90 values.push((0x1c001c0a, crate::create::string(font)));
91 }
92 if let Some(size) = format.font_size {
93 values.push((
94 0x10001c0b,
95 ((size * 2.0).round() as u16).to_le_bytes().to_vec(),
96 ));
97 }
98 if let Some(color) = format.color {
99 values.push((0x14001c0c, color.to_le_bytes().to_vec()));
100 }
101 if let Some(highlight) = format.highlight {
102 values.push((0x14001c0d, highlight.to_le_bytes().to_vec()));
103 }
104 if let Some(language) = format.language {
105 values.push((0x14001c3b, language.to_le_bytes().to_vec()));
106 }
107 values
108}
109
110/// The indentation table, in points, OneNote gives each outline and table cell it creates.
111pub(crate) const NATIVE_INDENTS: [f32; 4] = [18.0, 0.0, 27.0, 27.0];
112
113/// A native measurement array: a count byte, `header - 1` reserved bytes, then each value
114/// in half-inch units.
115pub(crate) fn measurement_bytes(values: &[f32], header: usize) -> Result<Vec<u8>, Error> {
116 let count = u8::try_from(values.len())
117 .map_err(|_| invalid("A measurement array exceeds the document range"))?;
118 let mut bytes = vec![0u8; header];
119 bytes[0] = count;
120 for value in values {
121 if !value.is_finite() {
122 return Err(invalid("A measurement must be finite"));
123 }
124 bytes.extend_from_slice(&(value / 36.0).to_le_bytes());
125 }
126 Ok(bytes)
127}
128
129/// A stored picture keeps what `Picture` cannot change.
130pub(crate) fn picture_fixed_fields(stored: &Image, image: &Image) -> Result<(), Error> {
131 if stored.id != image.id
132 || stored.bytes != image.bytes
133 || stored.size != image.size
134 || stored.background != image.background
135 || stored.link != image.link
136 || stored.text != image.text
137 {
138 return Err(invalid(
139 "A stored picture keeps its payload, intrinsic size, background state, link and text",
140 ));
141 }
142 Ok(())
143}
144
145/// Position and displayed-size properties that take `layout` from `stored`, and the ones
146/// to remove.
147fn layout_values(stored: &Layout, layout: &Layout) -> Result<(Values, Vec<u32>), Error> {
148 let mut values = Values::new();
149 let mut removed = Vec::new();
150 if (layout.x, layout.y) != (stored.x, stored.y) {
151 let (Some(x), Some(y)) = (layout.x, layout.y) else {
152 return Err(invalid("A picture position needs both coordinates"));
153 };
154 if !(x.is_finite() && y.is_finite()) {
155 return Err(invalid("A picture position must be finite"));
156 }
157 values.push((0x14001c14, (x / 36.0).to_le_bytes().to_vec()));
158 values.push((0x14001c15, (y / 36.0).to_le_bytes().to_vec()));
159 }
160 if (
161 layout.max_width,
162 layout.max_height,
163 layout.width_set_by_user,
164 ) != (
165 stored.max_width,
166 stored.max_height,
167 stored.width_set_by_user,
168 ) {
169 match (layout.max_width, layout.max_height) {
170 (Some(width), Some(height)) => {
171 if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
172 return Err(invalid("Picture size must be positive"));
173 }
174 values.push((0x14001c1b, (width / 36.0).to_le_bytes().to_vec()));
175 values.push((0x14001c1c, (height / 36.0).to_le_bytes().to_vec()));
176 let user_set = u32::from(layout.width_set_by_user == Some(true));
177 values.push((0x08001cbd | (user_set << 31), Vec::new()));
178 }
179 (None, None) => removed.extend([0x14001c1b, 0x14001c1c, 0x08001cbd]),
180 _ => return Err(invalid("A picture size needs both dimensions")),
181 }
182 }
183 if layout.reserved_width != stored.reserved_width {
184 return Err(invalid("A picture has no reserved width"));
185 }
186 Ok((values, removed))
187}
188
189fn icon_size(
190 values: &mut Values,
191 size: Option<[f32; 2]>,
192 message: &'static str,
193) -> Result<(), Error> {
194 if let Some([width, height]) = size {
195 if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
196 return Err(invalid(message));
197 }
198 values.push((0x140034cd, (width / 36.0).to_le_bytes().to_vec()));
199 values.push((0x140034ce, (height / 36.0).to_le_bytes().to_vec()));
200 }
201 Ok(())
202}
203
204/// What OneNote stores for an inserted picture: a file-data object declaring payload
205/// `payload` by identity and extension, and picture object `id` that paragraph `holder`
206/// holds as content or the page lists as a child; with `shown`, the file-data object and
207/// payload of the raster OneNote shows of it (WebPictureContainer14), as a printout page
208/// stores beside its XPS package. The caller embeds the payloads.
209pub(crate) fn picture_changes(
210 active: &ActivePage<'_>,
211 image: &Image,
212 (id, file, payload): (ExGuid, ExGuid, [u8; 16]),
213 shown: Option<(ExGuid, [u8; 16])>,
214 holder: Option<ExGuid>,
215) -> Result<Changes, Error> {
216 let Some(bytes) = &image.bytes else {
217 return Err(invalid("A new picture needs its payload"));
218 };
219 let extension = match bytes.as_ref() {
220 [0x89, b'P', b'N', b'G', ..] => ".png",
221 [0xff, 0xd8, 0xff, ..] => ".jpg",
222 [b'G', b'I', b'F', b'8', ..] => ".gif",
223 [b'B', b'M', ..] => ".bmp",
224 // OneNote keeps an imported TIFF as it is (`corpus/m6/native-features-01`).
225 [b'I', b'I', 0x2a, 0x00, ..] | [b'M', b'M', 0x00, 0x2a, ..] => ".tif",
226 // A file printout's page: its XPS package (`corpus/printout`).
227 [b'P', b'K', 3, 4, ..] if image.printout.is_some() && shown.is_some() => ".xps",
228 _ => return Err(invalid("Choose a PNG, JPEG, GIF, BMP or TIFF picture")),
229 };
230 if image
231 .display
232 .as_ref()
233 .is_some_and(|raster| !raster.starts_with(b"\x89PNG"))
234 {
235 return Err(invalid(
236 "A picture's shown raster is a PNG stored beside it",
237 ));
238 }
239 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
240 let mut values: Values = vec![(0x14001d7a, modified.to_vec())];
241 if let Some([width, height]) = image.size {
242 if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
243 return Err(invalid("Picture size must be positive"));
244 }
245 values.push((0x140034cd, (width / 36.0).to_le_bytes().to_vec()));
246 values.push((0x140034ce, (height / 36.0).to_le_bytes().to_vec()));
247 }
248 values.extend(layout_values(&Default::default(), &image.layout)?.0);
249 if let Some(alt) = &image.alt {
250 values.push((0x1c001e58, crate::create::string(alt)));
251 }
252 if let Some(link) = &image.link {
253 values.push((0x1c001e20, crate::create::string(link)));
254 }
255 if let Some(recognized) = &image.text {
256 values.push((0x1c001c22, crate::create::string(&recognized.text)));
257 for (id, value) in &recognized.stored {
258 if !crate::page::RECOGNIZED_PROPERTIES.contains(id) {
259 return Err(invalid("A picture's text stores only its known properties"));
260 }
261 values.push((*id, value.clone()));
262 }
263 }
264 if image.background {
265 values.push((0x08001d13 | (1 << 31), Vec::new()));
266 }
267 values.push((0x14001c3b, 0x409_u32.to_le_bytes().to_vec()));
268 // IsPrintout, and the printout page shown (DisplayedPageNumber).
269 values.push((
270 0x08001d85 | (u32::from(image.printout.is_some()) << 31),
271 Vec::new(),
272 ));
273 if let Some(printout) = &image.printout {
274 values.push((0x14001df9, printout.page.to_le_bytes().to_vec()));
275 for (id, value) in &printout.stored {
276 if !crate::page::PRINTOUT_PROPERTIES.contains(id) {
277 return Err(invalid("A printout page stores only its known properties"));
278 }
279 values.push((*id, value.clone()));
280 }
281 }
282 let mut changed = BTreeMap::new();
283 let mut container = PropertyObject::file(file, &payload_reference(payload), extension)?;
284 if extension == ".xps" {
285 container.jcid = crate::write::PRINTOUT_FILE_JCID;
286 }
287 changed.insert(file, container);
288 let mut picture = PropertyObject {
289 jcid: 0x60011,
290 bytes: crate::create::properties(&values)?,
291 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
292 };
293 picture.reference(id)?;
294 let container = picture.reference(file)?;
295 picture.set(&[(0x20001c3f, &container)])?;
296 if let Some((raster, payload)) = shown {
297 changed.insert(
298 raster,
299 PropertyObject::file(raster, &payload_reference(payload), ".png")?,
300 );
301 let web = picture.reference(raster)?;
302 picture.set(&[(0x200034c8, &web)])?;
303 }
304 changed.insert(id, picture);
305 hold(active, &mut changed, id, holder, &modified)?;
306 Ok(changed)
307}
308
309/// A moved, resized or described picture as OneNote stores one: the position, the layout
310/// width and height with the user flag and the description on the picture object, leaving
311/// the intrinsic size alone.
312pub(crate) fn picture_edit_changes(
313 active: &ActivePage<'_>,
314 object: ExGuid,
315 stored: (&Layout, &Option<String>),
316 layout: &Layout,
317 alt: &Option<String>,
318) -> Result<Changes, Error> {
319 let mut values: Values = vec![(
320 0x14001d7a,
321 crate::create::current_timestamps()?
322 .0
323 .to_le_bytes()
324 .to_vec(),
325 )];
326 let (layout, mut removed) = layout_values(stored.0, layout)?;
327 values.extend(layout);
328 if alt != stored.1 {
329 match alt {
330 Some(alt) => values.push((0x1c001e58, crate::create::string(alt))),
331 None => removed.push(0x1c001e58),
332 }
333 }
334 let raw = &active.live.revision;
335 let mut picture = PropertyObject::from_object(&raw.objects[&object])?;
336 picture.remove(&removed)?;
337 let values: Vec<(u32, &[u8])> = values
338 .iter()
339 .map(|(id, bytes)| (*id, bytes.as_slice()))
340 .collect();
341 picture.set(&values)?;
342 Ok(BTreeMap::from([(object, picture)]))
343}
344
345/// Identities of a new attachment's objects and payloads.
346pub(crate) struct AttachmentIds {
347 pub object: ExGuid,
348 pub file: ExGuid,
349 pub payload: [u8; 16],
350 /// The preview icon's payload and file-data object, when it has one.
351 pub preview: Option<([u8; 16], ExGuid)>,
352}
353
354/// What OneNote stores for an inserted file: an embedded-file container declaring the
355/// payload and an attachment object naming the file, which paragraph `holder` holds as
356/// content or the page lists as a child at the attachment's position
357/// (`corpus/attachment-floating`). The caller embeds the payloads.
358pub(crate) fn attachment_changes(
359 active: &ActivePage<'_>,
360 attachment: &Attachment,
361 ids: &AttachmentIds,
362 holder: Option<ExGuid>,
363) -> Result<Changes, Error> {
364 let name = attachment.filename.as_str();
365 if name.is_empty() || name.contains(['\0', '/', '\\']) {
366 return Err(invalid(
367 "An attachment needs a file name without path separators",
368 ));
369 }
370 if Recording::kind_of(name).is_some() != attachment.recording.is_some() {
371 return Err(invalid(
372 "Audio and video files are recordings, and only they are",
373 ));
374 }
375 if attachment.bytes.is_none() {
376 return Err(invalid("A new attachment needs its payload"));
377 }
378 if attachment
379 .preview
380 .as_ref()
381 .is_some_and(|icon| !icon.starts_with(&[0x89, b'P', b'N', b'G']))
382 {
383 return Err(invalid("An attachment preview is a PNG icon"));
384 }
385 let extension = name
386 .rfind('.')
387 .filter(|dot| *dot > 0)
388 .map(|dot| &name[dot..])
389 .unwrap_or("");
390 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
391 let mut values: Values = vec![(0x14001d7a, modified.to_vec())];
392 icon_size(
393 &mut values,
394 attachment.size,
395 "Attachment icon size must be positive",
396 )?;
397 values.push((0x14001c3b, 0x409_u32.to_le_bytes().to_vec()));
398 values.push((0x10001cfe, 0x409_u16.to_le_bytes().to_vec()));
399 values.push((0x1c001dcf, vec![0; 32]));
400 values.push((
401 0x1c001d61,
402 [16u32, 1, 0, 0, 0]
403 .iter()
404 .flat_map(|v| v.to_le_bytes())
405 .collect(),
406 ));
407 let positioned = attachment.layout.x.is_some() || attachment.layout.y.is_some();
408 if holder.is_some() == positioned {
409 return Err(invalid(
410 "A file on the page has a position and a paragraph's file has none",
411 ));
412 }
413 // LayoutAlignmentInParent and LayoutAlignmentSelf: a paragraph's file is left-aligned in
414 // its paragraph; one on the page is aligned by its left margin (9), as OneNote stores.
415 match holder {
416 Some(_) => {
417 values.push((0x14001c3e, 1u32.to_le_bytes().to_vec()));
418 values.push((0x14001c84, 1u32.to_le_bytes().to_vec()));
419 }
420 None => values.push((0x14001c84, 9u32.to_le_bytes().to_vec())),
421 }
422 values.push((0x1c001c22, crate::create::string(name)));
423 values.push((0x1c001d9c, crate::create::string(name)));
424 if let Some(path) = &attachment.source_path {
425 values.push((0x1c001d9d, crate::create::string(path)));
426 }
427 if let Some(recording) = attachment.recording {
428 if recording.id == [0; 16] || !matches!(recording.kind, 1 | 2) {
429 return Err(invalid(
430 "A recording needs an identity and is audio or video",
431 ));
432 }
433 // AudioRecordingGuid, IRecordMedia and AudioRecordingDuration.
434 values.push((0x1c001c97, recording.id.to_vec()));
435 values.push((0x14001d24, recording.kind.to_le_bytes().to_vec()));
436 if let Some(duration) = recording.duration_ms {
437 values.push((0x14001cfd, duration.to_le_bytes().to_vec()));
438 }
439 }
440 values.extend(layout_values(&Default::default(), &attachment.layout)?.0);
441 let mut changed = BTreeMap::new();
442 let mut file = PropertyObject::file(ids.file, &payload_reference(ids.payload), extension)?;
443 file.jcid = crate::write::EMBEDDED_FILE_JCID;
444 changed.insert(ids.file, file);
445 let mut node = PropertyObject {
446 jcid: 0x60035,
447 bytes: crate::create::properties(&values)?,
448 global_ids: std::sync::Arc::new(BTreeMap::from([(0, ids.object.guid)])),
449 };
450 node.reference(ids.object)?;
451 let container = node.reference(ids.file)?;
452 node.set(&[(0x20001d9b, &container)])?;
453 if let Some((payload, icon)) = ids.preview {
454 changed.insert(
455 icon,
456 PropertyObject::file(icon, &payload_reference(payload), ".png")?,
457 );
458 let reference = node.reference(icon)?;
459 node.set(&[(0x20001c3f, &reference)])?;
460 }
461 changed.insert(ids.object, node);
462 hold(active, &mut changed, ids.object, holder, &modified)?;
463 Ok(changed)
464}
465
466/// Page `page` listing `recordings` in its AudioRecordingGuids, or none.
467pub(crate) fn playlist_changes(
468 active: &ActivePage<'_>,
469 page: ExGuid,
470 recordings: &[[u8; 16]],
471) -> Result<Changes, Error> {
472 let mut node = PropertyObject::from_object(&active.live.revision.objects[&page])?;
473 if recordings.is_empty() {
474 node.remove(&[0x1c001ca3])?;
475 } else {
476 node.set(&[(0x1c001ca3, recordings.concat().as_slice())])?;
477 }
478 Ok(BTreeMap::from([(page, node)]))
479}
480
481/// A stored attachment keeps its payload and preview; its shown name, recorded source
482/// path and icon size change in place, as OneNote's rename does.
483pub(crate) fn attachment_edit_changes(
484 active: &ActivePage<'_>,
485 stored: &Attachment,
486 attachment: &Attachment,
487) -> Result<Changes, Error> {
488 let mut values: Values = vec![(
489 0x14001d7a,
490 crate::create::current_timestamps()?
491 .0
492 .to_le_bytes()
493 .to_vec(),
494 )];
495 if attachment.recording != stored.recording {
496 return Err(invalid("A file keeps its recording"));
497 }
498 let name = attachment.filename.as_str();
499 if name.is_empty() || name.contains(['\0', '/', '\\']) {
500 return Err(invalid(
501 "An attachment needs a file name without path separators",
502 ));
503 }
504 let mut removed = Vec::new();
505 if attachment.filename != stored.filename {
506 if Recording::kind_of(name).is_some() != attachment.recording.is_some() {
507 return Err(invalid(
508 "Audio and video files are recordings, and only they are",
509 ));
510 }
511 let name = crate::create::string(&attachment.filename);
512 values.push((0x1c001c22, name.clone()));
513 values.push((0x1c001d9c, name));
514 }
515 if attachment.source_path != stored.source_path {
516 match &attachment.source_path {
517 Some(path) => values.push((0x1c001d9d, crate::create::string(path))),
518 None => removed.push(0x1c001d9d),
519 }
520 }
521 if attachment.size != stored.size {
522 match attachment.size {
523 Some(_) => icon_size(
524 &mut values,
525 attachment.size,
526 "Attachment icon size must be positive",
527 )?,
528 None => removed.extend([0x140034cd, 0x140034ce]),
529 }
530 }
531 let object = stored.id;
532 let raw = &active.live.revision;
533 let mut node = PropertyObject::from_object(&raw.objects[&object])?;
534 node.remove(&removed)?;
535 let values: Vec<(u32, &[u8])> = values
536 .iter()
537 .map(|(id, bytes)| (*id, bytes.as_slice()))
538 .collect();
539 node.set(&values)?;
540 Ok(BTreeMap::from([(object, node)]))
541}
542
543/// Creates stroke objects (numbered on from the page's last stroke) and one
544/// drawing-attribute object per distinct pen; returns the compact references for the data
545/// node's stroke list. A shape's strokes carry only their path and pen, as OneNote's do.
546fn write_strokes(
547 active: &ActivePage<'_>,
548 changed: &mut Changes,
549 data_object: &mut PropertyObject,
550 strokes: &[(ExGuid, &InkStroke)],
551 shape: bool,
552 filetime: u64,
553) -> Result<Vec<u8>, Error> {
554 let first = active
555 .view
556 .nodes
557 .values()
558 .filter_map(|node| match node.kind {
559 crate::document::Kind::InkStroke { index, .. } => index,
560 _ => None,
561 })
562 .max()
563 .unwrap_or(0);
564 let mut styles: Vec<(InkPen, ExGuid)> = Vec::new();
565 let mut references = Vec::new();
566 for (offset, (stroke_id, stroke)) in strokes.iter().enumerate() {
567 let pen = InkPen::of(stroke, shape);
568 let style = match styles.iter().find(|(known, _)| *known == pen) {
569 Some((_, id)) => *id,
570 None => {
571 let id = ExGuid {
572 guid: crate::write::fresh_guid()?,
573 n: 1,
574 };
575 let mut style = PropertyObject {
576 jcid: 0x120048,
577 bytes: crate::create::properties(&pen.values())?,
578 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
579 };
580 style.reference(id)?;
581 changed.insert(id, style);
582 styles.push((pen, id));
583 id
584 }
585 };
586 let values = if shape {
587 stroke_path(stroke)?
588 } else {
589 stroke_values(stroke, first + offset as u32 + 1, filetime)?
590 };
591 let mut object = PropertyObject {
592 jcid: 0x20047,
593 bytes: crate::create::properties(&values)?,
594 global_ids: std::sync::Arc::new(BTreeMap::from([(0, stroke_id.guid)])),
595 };
596 object.reference(*stroke_id)?;
597 let style_reference = object.reference(style)?;
598 object.set(&[(0x20003409, &style_reference)])?;
599 changed.insert(*stroke_id, object);
600 references.extend(data_object.reference(*stroke_id)?);
601 }
602 Ok(references)
603}
604
605/// The drawing attributes OneNote shares between strokes drawn with the same pen. A pen
606/// drawn with the mouse ignores pressure, and one that follows it records it as a third
607/// dimension (`corpus/ink-pressure`); a shape's pen spans every coordinate instead.
608#[derive(PartialEq)]
609struct InkPen {
610 width: u32,
611 height: u32,
612 color: Option<u32>,
613 transparency: Option<u8>,
614 pen_tip: Option<u8>,
615 raster_operation: Option<u8>,
616 shape: bool,
617 pressure: bool,
618}
619
620impl InkPen {
621 fn of(stroke: &InkStroke, shape: bool) -> Self {
622 Self {
623 width: (stroke.width * 2540.0 / 72.0).to_bits(),
624 height: (stroke.height * 2540.0 / 72.0).to_bits(),
625 color: stroke.color,
626 transparency: stroke.transparency,
627 pen_tip: stroke.pen_tip,
628 raster_operation: stroke.raster_operation,
629 shape,
630 pressure: !stroke.pressure.is_empty(),
631 }
632 }
633
634 fn values(&self) -> Values {
635 let mut dimensions = if self.shape {
636 crate::page::ink::SHAPE_DIMENSIONS
637 } else {
638 crate::page::ink::DIMENSIONS
639 }
640 .to_vec();
641 if self.pressure {
642 dimensions.extend(crate::page::ink::PRESSURE_DIMENSION);
643 }
644 let mut values: Values = vec![
645 (0x1c00340a, dimensions),
646 (0x1400340c, self.height.to_le_bytes().to_vec()),
647 (0x1400340d, self.width.to_le_bytes().to_vec()),
648 ];
649 if let Some(color) = self.color {
650 values.push((0x1400340f, color.to_le_bytes().to_vec()));
651 }
652 if let Some(transparency) = self.transparency {
653 values.push((0x0c003414, vec![transparency]));
654 }
655 if let Some(tip) = self.pen_tip {
656 values.push((0x0c003412, vec![tip]));
657 }
658 if let Some(operation) = self.raster_operation {
659 values.push((0x0c003413, vec![operation]));
660 }
661 if !self.shape && !self.pressure {
662 values.push((0x88003411, Vec::new()));
663 }
664 values
665 }
666}
667
668fn stroke_path(stroke: &InkStroke) -> Result<Values, Error> {
669 if stroke.points.is_empty() {
670 return Err(invalid("A stroke needs at least one point"));
671 }
672 if stroke
673 .points
674 .iter()
675 .any(|p| !p[0].is_finite() || !p[1].is_finite())
676 || !(stroke.width.is_finite() && stroke.height.is_finite())
677 || stroke.width <= 0.0
678 || stroke.height <= 0.0
679 {
680 return Err(invalid(
681 "Stroke points and pen size must be finite and positive",
682 ));
683 }
684 if !stroke.pressure.is_empty()
685 && (stroke.pressure.len() != stroke.points.len()
686 || stroke.pressure.iter().any(|p| !p.is_finite()))
687 {
688 return Err(invalid(
689 "A stroke's pressure needs one finite level per point",
690 ));
691 }
692 Ok(vec![(0x1c00340b, stroke.packet())])
693}
694
695/// A drawn stroke as OneNote stores one: its path, index on the page, language, identity,
696/// time and half-inch origin, and a bias of 2 for a highlighter.
697fn stroke_values(stroke: &InkStroke, index: u32, filetime: u64) -> Result<Values, Error> {
698 let mut values = stroke_path(stroke)?;
699 let [left, top] = [0, 1].map(|axis| {
700 stroke
701 .points
702 .iter()
703 .map(|p| p[axis])
704 .fold(f32::INFINITY, f32::min)
705 });
706 let mut origin = ((left - stroke.width / 2.0) / 36.0).to_le_bytes().to_vec();
707 origin.extend_from_slice(&((top - stroke.height / 2.0) / 36.0).to_le_bytes());
708 let bias = if stroke.raster_operation == Some(9) {
709 2
710 } else {
711 0
712 };
713 values.extend([
714 (0x14003419, index.to_le_bytes().to_vec()),
715 (0x1000341b, 0x409_u16.to_le_bytes().to_vec()),
716 (0x0c00341c, vec![bias]),
717 (0x1c00341a, crate::write::fresh_guid()?.to_vec()),
718 (0x1c00341d, filetime.to_le_bytes().to_vec()),
719 (0x1c00345b, origin),
720 ]);
721 Ok(values)
722}
723
724/// New ink as OneNote 2010 stores a drawing: container `id` that the page lists as a child
725/// (or paragraph `holder` holds as content), its data node `data` listing the strokes, each
726/// stroke's packet and half-inch origin, and one drawing-attribute object per distinct pen.
727pub(crate) fn ink_changes(
728 active: &ActivePage<'_>,
729 ink: &Ink,
730 id: ExGuid,
731 data: ExGuid,
732 strokes: &[(ExGuid, &InkStroke)],
733 holder: Option<ExGuid>,
734) -> Result<Changes, Error> {
735 if !ink.groups.is_empty() {
736 return Err(invalid("New ink holds strokes, not nested groups"));
737 }
738 if ink.strokes.is_empty() {
739 return Err(invalid("New ink needs at least one stroke"));
740 }
741 let placed = crate::document::Layout {
742 x: None,
743 y: None,
744 ..ink.layout.clone()
745 };
746 if placed != Default::default() {
747 return Err(invalid("Ink sizes come from its strokes"));
748 }
749 let (modified, filetime) = crate::create::current_timestamps()?;
750 let modified = modified.to_le_bytes();
751 let mut changed = BTreeMap::new();
752 let mut data_object = PropertyObject {
753 jcid: 0x2003b,
754 bytes: crate::create::properties(&[])?,
755 global_ids: std::sync::Arc::new(BTreeMap::from([(0, data.guid)])),
756 };
757 data_object.reference(data)?;
758 let references = write_strokes(
759 active,
760 &mut changed,
761 &mut data_object,
762 strokes,
763 ink.shape.is_some(),
764 filetime,
765 )?;
766 data_object.set(&[(0x24003416, &references)])?;
767 changed.insert(data, data_object);
768 // A shape keeps its kind and geometry beside the strokes, and counts as a second kind of
769 // drawing.
770 let mut values: Values = vec![(0x14001d7a, modified.to_vec())];
771 if let Some(shape) = &ink.shape {
772 let (kind, property, bytes) = shape.stored();
773 values.extend([(0x0c001d4f, vec![kind]), (property, bytes)]);
774 }
775 values.push((
776 0x14001d4e,
777 (1 + u32::from(ink.shape.is_some())).to_le_bytes().to_vec(),
778 ));
779 let mut object = PropertyObject {
780 jcid: 0x60014,
781 bytes: crate::create::properties(&values)?,
782 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
783 };
784 object.reference(id)?;
785 let data_reference = object.reference(data)?;
786 object.set(&[(0x20003415, &data_reference)])?;
787 changed.insert(id, object);
788 hold(active, &mut changed, id, holder, &modified)?;
789 Ok(changed)
790}
791
792/// Rewrites a drawing's stroke list: `kept` strokes stay as they are (OneNote erases whole
793/// strokes rather than editing them), the rest leave the list, `added` ones are created.
794pub(crate) fn strokes_changes(
795 active: &ActivePage<'_>,
796 container: ExGuid,
797 kept: &[ExGuid],
798 added: &[(ExGuid, &InkStroke)],
799) -> Result<Changes, Error> {
800 let (data, shape) = match active.view.nodes.get(&container).map(|node| &node.kind) {
801 Some(crate::document::Kind::Ink {
802 data: Some(data),
803 shape_kind,
804 ..
805 }) => (*data, shape_kind.is_some()),
806 _ => return Err(invalid("Stored ink has no stroke data to rewrite")),
807 };
808 let (modified, filetime) = crate::create::current_timestamps()?;
809 let modified = modified.to_le_bytes();
810 let raw = &active.live.revision;
811 let mut changed = BTreeMap::new();
812 let mut data_object = PropertyObject::from_object(&raw.objects[&data])?;
813 let mut references = Vec::new();
814 for id in kept {
815 references.extend(data_object.reference(*id)?);
816 }
817 references.extend(write_strokes(
818 active,
819 &mut changed,
820 &mut data_object,
821 added,
822 shape,
823 filetime,
824 )?);
825 data_object.set(&[(0x24003416, &references)])?;
826 changed.insert(data, data_object);
827 let mut object = PropertyObject::from_object(&raw.objects[&container])?;
828 object.set(&[(0x14001d7a, &modified)])?;
829 changed.insert(container, object);
830 Ok(changed)
831}
832
833/// An equation as OneNote stores one: the linear text, one run per span with a style
834/// carrying the span's format, the run-data array naming each run's inline object, and
835/// the math language marker on the text object.
836pub(crate) fn equation_changes(
837 active: &ActivePage<'_>,
838 object: ExGuid,
839 text: &Paragraph,
840) -> Result<Changes, Error> {
841 if text.text().contains('\u{fffc}') {
842 return Err(invalid("Equations cannot hold embedded objects"));
843 }
844 let encoded: Vec<u8> = text
845 .text()
846 .encode_utf16()
847 .chain([0])
848 .flat_map(u16::to_le_bytes)
849 .collect();
850 let mut ends = Vec::new();
851 let mut styles: Vec<Values> = Vec::new();
852 let mut sets = Vec::new();
853 let all_math = text.spans().iter().all(|s| s.format.math == Some(true));
854 for span in text.spans() {
855 ends.extend(text.utf16_offset(span.end)?.to_le_bytes());
856 // A run style states its language (MS-ONE 2.2.43), so the text object's never shows
857 // through; an unset one is the writer's insertion language.
858 let mut format = span.format.clone();
859 format.language.get_or_insert(0x409);
860 styles.push(style_values(&format));
861 sets.push(match &span.format.math_object {
862 Some(object) => {
863 let mut set = vec![(0x1400344f, object.kind.to_le_bytes().to_vec())];
864 if let Some(count) = object.arguments {
865 set.push((0x14003450, count.to_le_bytes().to_vec()));
866 }
867 if let Some(columns) = object.columns {
868 set.push((0x0c003451, vec![columns]));
869 }
870 for (id, symbol) in [0x10003453, 0x10003454, 0x10003455]
871 .into_iter()
872 .zip(&object.symbols)
873 {
874 let unit = u16::try_from(u32::from(*symbol))
875 .map_err(|_| invalid("Math symbols are single UTF-16 units"))?;
876 set.push((id, unit.to_le_bytes().to_vec()));
877 }
878 set
879 }
880 None => Vec::new(),
881 });
882 }
883 ends.truncate(ends.len() - 4);
884 let has_objects = sets.iter().any(|set| !set.is_empty());
885 let modified = crate::create::current_timestamps()?.0.to_le_bytes();
886 let raw = &active.live.revision;
887 let mut changed = BTreeMap::new();
888 let mut target = PropertyObject::from_object(&raw.objects[&object])?;
889 let mut references = Vec::new();
890 let mut created: Vec<(Values, ExGuid)> = Vec::new();
891 for values in &styles {
892 let id = match created.iter().find(|(known, _)| known == values) {
893 Some((_, id)) => *id,
894 None => {
895 let id = ExGuid {
896 guid: crate::write::fresh_guid()?,
897 n: 1,
898 };
899 let mut style = PropertyObject {
900 jcid: 0x12004d,
901 bytes: crate::create::properties(values)?,
902 global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])),
903 };
904 style.reference(id)?;
905 changed.insert(id, style);
906 created.push((values.clone(), id));
907 id
908 }
909 };
910 references.extend(target.reference(id)?);
911 }
912 target.remove(&[0x1c003498, 0x40003499])?;
913 target.set(&[
914 (0x1c001c22, &encoded),
915 (0x1c001e12, &ends),
916 (0x24001e13, &references),
917 (0x14001d7a, &modified),
918 ])?;
919 if has_objects {
920 target.set_sets(0x40003499, 0x44000811, &sets)?;
921 }
922 if all_math {
923 // The flags and language marker OneNote's equation editor leaves on every equation
924 // text object.
925 target.set(&[
926 (0x10001cfe, &0x7f_u16.to_le_bytes()),
927 (0x14001c3e, &1u32.to_le_bytes()),
928 (0x14001c84, &1u32.to_le_bytes()),
929 ])?;
930 }
931 changed.insert(object, target);
932 Ok(changed)
933}