1//! An editable page model: outlines, paragraphs, tables and images with their stored
2//! identities, plus everything else on the page retained as `Unsupported`.
3
4use crate::{
5 Error, ExGuid,
6 document::{Document, Element, FieldValue, Format, Kind, Layout, Revision, Tag},
7};
8use std::{
9 collections::{BTreeMap, BTreeSet},
10 sync::Arc,
11};
12
13pub mod ink;
14mod linear;
15pub mod link;
16pub mod math;
17pub mod text;
18
19pub use ink::{Ink, InkStroke};
20pub use math::Math;
21
22/// Picture and attachment payloads travel with the model (queued intents replay them),
23/// as base64 text.
24mod payload {
25 use base64::{Engine, display::Base64Display, engine::general_purpose::STANDARD};
26 use std::sync::Arc;
27
28 struct Encoded<'a>(&'a [u8]);
29
30 impl serde::Serialize for Encoded<'_> {
31 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
32 serializer.collect_str(&Base64Display::new(self.0, &STANDARD))
33 }
34 }
35
36 pub fn serialize<S: serde::Serializer>(
37 bytes: &Option<Arc<[u8]>>,
38 serializer: S,
39 ) -> Result<S::Ok, S::Error> {
40 match bytes {
41 Some(bytes) => serializer.serialize_some(&Encoded(bytes)),
42 None => serializer.serialize_none(),
43 }
44 }
45
46 pub fn deserialize<'de, D: serde::Deserializer<'de>>(
47 deserializer: D,
48 ) -> Result<Option<Arc<[u8]>>, D::Error> {
49 let text: Option<String> = serde::Deserialize::deserialize(deserializer)?;
50 text.map(|text| {
51 STANDARD
52 .decode(text)
53 .map(Arc::from)
54 .map_err(serde::de::Error::custom)
55 })
56 .transpose()
57 }
58}
59pub use text::Paragraph;
60
61/// The page node's colour (View, Page Color) as COLORREF, absent for "No color"; not in
62/// MS-ONE, observed in OneNote 2010's pages (`corpus/notebook-management/native/page-color`).
63pub(crate) const PAGE_COLOR: u32 = 0x14001d2a;
64
65/// The page node's rule lines (View, Rule Lines), as OneNote 2010 orders them; not in MS-ONE,
66/// observed in its pages (`corpus/rule-lines/native`). Each direction stores a kind (1 grid,
67/// 2 ruled, 3 margin line), a spacing in half inches and a colour as 0xAARRGGBB.
68pub(crate) const RULE_LINES: [u32; 6] = [
69 HORIZONTAL_COLOR,
70 HORIZONTAL_KIND,
71 HORIZONTAL_SPACING,
72 VERTICAL_KIND,
73 VERTICAL_SPACING,
74 VERTICAL_COLOR,
75];
76const HORIZONTAL_KIND: u32 = 0x14001cd3;
77const HORIZONTAL_SPACING: u32 = 0x14001cd4;
78const HORIZONTAL_COLOR: u32 = 0x14001cd5;
79const VERTICAL_KIND: u32 = 0x14001cd6;
80const VERTICAL_SPACING: u32 = 0x14001cd7;
81const VERTICAL_COLOR: u32 = 0x14001cd8;
82
83/// A page's rule lines (View, Rule Lines): horizontal lines every `spacing` from the margin
84/// origin down, crossed by a margin line or a grid's vertical lines. Spacings are in half
85/// inches, as stored, so OneNote's presets survive exactly.
86#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
87pub struct RuleLines {
88 pub spacing: f32,
89 /// COLORREF.
90 pub color: u32,
91 pub vertical: VerticalRule,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
95pub enum VerticalRule {
96 /// One line just left of the margin origin, COLORREF.
97 Margin(u32),
98 /// Lines every `spacing` either side of the margin origin.
99 Grid { spacing: f32, color: u32 },
100}
101
102impl RuleLines {
103 /// The page node's properties for these lines, in `RULE_LINES` order.
104 pub(crate) fn properties(&self) -> [(u32, [u8; 4]); 6] {
105 let argb = |color: u32| {
106 let [red, green, blue, _] = color.to_le_bytes();
107 [blue, green, red, 0xff]
108 };
109 let (kinds, spacing, color) = match self.vertical {
110 VerticalRule::Margin(color) => ([2, 3], 0.0, color),
111 VerticalRule::Grid { spacing, color } => ([1, 1], spacing, color),
112 };
113 [
114 (HORIZONTAL_COLOR, argb(self.color)),
115 (HORIZONTAL_KIND, u32::to_le_bytes(kinds[0])),
116 (HORIZONTAL_SPACING, self.spacing.to_le_bytes()),
117 (VERTICAL_KIND, u32::to_le_bytes(kinds[1])),
118 (VERTICAL_SPACING, spacing.to_le_bytes()),
119 (VERTICAL_COLOR, argb(color)),
120 ]
121 }
122
123 /// The lines a page node's `fields` store, if any are whole enough to draw.
124 fn read(fields: &[crate::document::Field<'_>]) -> Option<Self> {
125 let value = |id: u32| {
126 fields
127 .iter()
128 .find(|field| field.id == id)
129 .and_then(|field| match field.value {
130 FieldValue::Bytes(&[a, b, c, d]) => Some([a, b, c, d]),
131 _ => None,
132 })
133 };
134 let spacing = |id| value(id).map(f32::from_le_bytes);
135 let color =
136 |id| value(id).map(|[blue, green, red, _]| u32::from_le_bytes([red, green, blue, 0]));
137 value(HORIZONTAL_KIND)?;
138 let vertical = match value(VERTICAL_KIND).map(u32::from_le_bytes) {
139 Some(1) => VerticalRule::Grid {
140 spacing: spacing(VERTICAL_SPACING)?,
141 color: color(VERTICAL_COLOR)?,
142 },
143 _ => VerticalRule::Margin(color(VERTICAL_COLOR)?),
144 };
145 Some(Self {
146 spacing: spacing(HORIZONTAL_SPACING).filter(|spacing| *spacing > 0.0)?,
147 color: color(HORIZONTAL_COLOR)?,
148 vertical,
149 })
150 }
151}
152
153/// The role of a title-outline paragraph that displays the page's creation date or time.
154#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
155pub enum DateField {
156 Date = 0,
157 Time = 1,
158}
159
160#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
161pub struct Page {
162 pub title: String,
163 /// The page's notebook-management identity, which internal links name as `page-id`.
164 pub identity: Option<[u8; 16]>,
165 /// FILETIME ticks from the page's TopologyCreationTimeStamp.
166 pub created: Option<u64>,
167 pub margin_origin: [f32; 2],
168 /// Right-to-left page direction, whose positions keep the left-to-right frame.
169 #[serde(default)]
170 pub rtl: bool,
171 /// The page's colour (View, Page Color), COLORREF; `None` is OneNote's "No color".
172 #[serde(default)]
173 pub color: Option<u32>,
174 #[serde(default)]
175 pub rule_lines: Option<RuleLines>,
176 pub objects: Vec<PageObject>,
177 pub definitions: BTreeMap<ExGuid, Definition>,
178}
179
180#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
181pub struct Definition {
182 pub kind: Kind<'static>,
183 pub format: Format,
184}
185
186#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
187pub enum PageObject {
188 Outline(Outline),
189 Title(Title),
190 Image(Image),
191 /// A file on a blank part of the page, as OneNote places one attached or dropped there.
192 Attachment(Attachment),
193 Ink(Ink),
194 Unsupported(Unsupported),
195}
196
197impl PageObject {
198 pub fn id(&self) -> ExGuid {
199 match self {
200 Self::Outline(value) => value.id,
201 Self::Title(value) => value.id,
202 Self::Image(value) => value.id,
203 Self::Attachment(value) => value.id,
204 Self::Ink(value) => value.id,
205 Self::Unsupported(value) => value.id,
206 }
207 }
208 pub fn layout(&self) -> &Layout {
209 match self {
210 Self::Outline(value) => &value.layout,
211 Self::Title(value) => &value.layout,
212 Self::Image(value) => &value.layout,
213 Self::Attachment(value) => &value.layout,
214 Self::Ink(value) => &value.layout,
215 Self::Unsupported(value) => &value.layout,
216 }
217 }
218
219 pub fn layout_mut(&mut self) -> &mut Layout {
220 match self {
221 Self::Outline(value) => &mut value.layout,
222 Self::Title(value) => &mut value.layout,
223 Self::Image(value) => &mut value.layout,
224 Self::Attachment(value) => &mut value.layout,
225 Self::Ink(value) => &mut value.layout,
226 Self::Unsupported(value) => &mut value.layout,
227 }
228 }
229}
230
231#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
232pub struct Title {
233 pub id: ExGuid,
234 pub date: Option<ExGuid>,
235 pub layout: Layout,
236 pub outlines: Vec<Outline>,
237}
238
239#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
240pub struct Outline {
241 pub id: ExGuid,
242 pub title: bool,
243 pub min_width: Option<f32>,
244 pub layout: Layout,
245 pub indents: Vec<f32>,
246 pub paragraphs: Vec<PageParagraph>,
247 pub unsupported: Vec<Unsupported>,
248}
249
250#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
251pub struct PageParagraph {
252 pub id: ExGuid,
253 pub parent: Option<ExGuid>,
254 pub level: u32,
255 pub style: Option<ExGuid>,
256 pub format: Format,
257 pub content: ParagraphContent,
258 pub lists: Vec<ExGuid>,
259 pub tags: Vec<Tag>,
260 pub media: MediaIndex,
261 pub collapsed: bool,
262}
263
264#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
265pub enum ParagraphContent {
266 Text(TextObject),
267 Table(Table),
268 /// A picture inside the paragraph, as OneNote inserts pictures into outlines.
269 Image(Image),
270 /// An embedded file, as OneNote inserts attachments into outlines.
271 Attachment(Attachment),
272 /// Handwriting held by the paragraph.
273 Ink(Ink),
274 Unsupported(Unsupported),
275}
276
277/// Payload bytes are identified by `id`, so equality and serialization leave them out.
278#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
279pub struct Attachment {
280 pub id: ExGuid,
281 /// The name OneNote shows and saves the file under.
282 pub filename: String,
283 /// Where the file was inserted from, when recorded.
284 pub source_path: Option<String>,
285 /// The icon's size in points.
286 pub size: Option<[f32; 2]>,
287 /// On the page, where the file's column sits and its extent; a paragraph's file has none.
288 #[serde(default)]
289 pub layout: Layout,
290 #[serde(with = "payload")]
291 pub bytes: Option<Arc<[u8]>>,
292 #[serde(with = "payload")]
293 pub preview: Option<Arc<[u8]>>,
294 /// Set when the file is audio or video OneNote plays; notes on the page refer to it by
295 /// identity (`PageParagraph::media`).
296 pub recording: Option<Recording>,
297 /// Note tags on the file itself, where OneNote stores a tagged file's, in a paragraph or
298 /// on the page.
299 #[serde(default)]
300 pub tags: Vec<Tag>,
301}
302
303/// A recording's identity, whether it is audio (1) or video (2), and its length.
304#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
305pub struct Recording {
306 pub id: [u8; 16],
307 pub kind: u32,
308 pub duration_ms: Option<u32>,
309}
310
311impl Recording {
312 /// The kind of recording MS-ONE requires a file named `filename` to be, if any: audio
313 /// for .wma, .mp3 and .wav, video for .wmv, .avi and .mpg.
314 pub fn kind_of(filename: &str) -> Option<u32> {
315 let extension = filename.rsplit_once('.')?.1.to_ascii_lowercase();
316 match extension.as_str() {
317 "wma" | "mp3" | "wav" => Some(1),
318 "wmv" | "avi" | "mpg" => Some(2),
319 _ => None,
320 }
321 }
322}
323
324/// A paragraph's link to a moment in recordings on the page: OneNote plays from
325/// `time_ms` when the paragraph is chosen, and links what is written while it records.
326#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
327pub struct MediaIndex {
328 pub recordings: Vec<[u8; 16]>,
329 pub time_ms: Option<u32>,
330}
331
332impl PartialEq for Attachment {
333 fn eq(&self, other: &Self) -> bool {
334 self.id == other.id
335 && self.filename == other.filename
336 && self.source_path == other.source_path
337 && self.size == other.size
338 && self.layout == other.layout
339 && self.recording == other.recording
340 && self.tags == other.tags
341 }
342}
343
344impl Attachment {
345 pub(crate) fn read(
346 revision: &Revision<'_>,
347 id: ExGuid,
348 node: &crate::document::Element<'_>,
349 ) -> Result<Self, Error> {
350 let invalid = |message| Error { offset: 0, message };
351 let Kind::Attachment {
352 container,
353 preview,
354 filename,
355 source_path,
356 recording_id,
357 recording_type,
358 recording_duration,
359 icon_width,
360 icon_height,
361 } = &node.kind
362 else {
363 unreachable!()
364 };
365 let payload = |target: &Option<ExGuid>| -> Result<Option<Arc<[u8]>>, Error> {
366 let Some(target) = target else {
367 return Ok(None);
368 };
369 let data = revision
370 .nodes
371 .get(target)
372 .ok_or_else(|| invalid("Missing canvas attachment data"))?;
373 match &data.kind {
374 Kind::File { payload, .. } => Ok(payload.map(Arc::from)),
375 _ => Err(invalid("Canvas attachment data has the wrong type")),
376 }
377 };
378 Ok(Self {
379 id,
380 filename: filename.clone().unwrap_or_default(),
381 source_path: source_path.clone(),
382 size: icon_width.zip(*icon_height).map(|(w, h)| [w, h]),
383 layout: node.layout.clone(),
384 bytes: payload(container)?,
385 preview: payload(preview)?,
386 recording: recording_id.map(|id| Recording {
387 id,
388 kind: recording_type
389 .or_else(|| Recording::kind_of(filename.as_deref().unwrap_or_default()))
390 .unwrap_or(1),
391 duration_ms: *recording_duration,
392 }),
393 tags: node.tags.clone(),
394 })
395 }
396}
397
398#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
399pub struct Table {
400 pub id: ExGuid,
401 pub columns: Vec<TableColumn>,
402 pub rows: Vec<TableRow>,
403 pub borders: Option<bool>,
404 pub layout: Layout,
405 pub tags: Vec<Tag>,
406}
407
408#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
409pub struct TableColumn {
410 pub width: f32,
411 pub locked: bool,
412}
413
414#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
415pub struct TableRow {
416 pub id: ExGuid,
417 pub cells: Vec<TableCell>,
418}
419
420#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
421pub struct TableCell {
422 pub id: ExGuid,
423 pub layout: Layout,
424 pub indents: Vec<f32>,
425 pub shading: Option<u32>,
426 pub paragraphs: Vec<PageParagraph>,
427 pub unsupported: Vec<Unsupported>,
428}
429
430#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
431pub struct TextObject {
432 pub id: ExGuid,
433 pub date_field: Option<DateField>,
434 pub text: Paragraph,
435 pub tags: Vec<Tag>,
436}
437
438/// Payload bytes are identified by `id`, so equality and serialization leave them out.
439#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
440pub struct Image {
441 pub id: ExGuid,
442 pub layout: Layout,
443 /// Displayed picture width and height in points.
444 pub size: Option<[f32; 2]>,
445 #[serde(with = "payload")]
446 pub bytes: Option<Arc<[u8]>>,
447 /// The raster OneNote shows for a picture stored in a format it draws itself, a
448 /// printout's XPS page or a metafile (WebPictureContainer14).
449 #[serde(default, with = "payload")]
450 pub display: Option<Arc<[u8]>>,
451 pub alt: Option<String>,
452 pub background: bool,
453 /// Set when the picture is a page of a file printout, which OneNote frames.
454 #[serde(default)]
455 pub printout: Option<Printout>,
456 /// Note tags on the picture itself, where OneNote stores a tagged picture's, in a
457 /// paragraph or on the page.
458 #[serde(default)]
459 pub tags: Vec<Tag>,
460 /// The address the picture links to (WzHyperlinkUrl).
461 #[serde(default)]
462 pub link: Option<String>,
463 /// Text OneNote recognised in the picture, or a printout page's text; OneNote searches
464 /// it.
465 #[serde(default)]
466 pub text: Option<Recognized>,
467}
468
469/// Text a picture holds (RichEditTextUnicode).
470#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
471pub struct Recognized {
472 pub text: String,
473 /// Its language (RichEditTextLangID) and the undocumented layout of its words,
474 /// `RECOGNIZED_PROPERTIES`, written back as read.
475 pub stored: Vec<(u32, Vec<u8>)>,
476}
477
478/// RichEditTextLangID, and where OneNote 2010 found each word of a picture's text
479/// (`corpus/object-tags`), not in MS-ONE.
480pub(crate) const RECOGNIZED_PROPERTIES: [u32; 2] = [0x10001cfe, 0x1c001d61];
481
482/// A page of a file printout.
483#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
484pub struct Printout {
485 /// The page shown, zero-based (DisplayedPageNumber).
486 pub page: u32,
487 /// The undocumented properties tying the page to its XPS package, `PRINTOUT_PROPERTIES`,
488 /// written back as read; without them OneNote does not draw a page it reads back.
489 pub stored: Vec<(u32, Vec<u8>)>,
490}
491
492/// The printout's identity, its file's index and the page's place in it, as OneNote 2010
493/// stores them on a printout page (`corpus/printout`), not in MS-ONE; with the picture's
494/// file name (ImageFilename) and the package's description and hash, also undocumented.
495pub(crate) const PRINTOUT_PROPERTIES: [u32; 6] = [
496 0x1c001d84, 0x14003480, 0x14003481, 0x1c001dd7, 0x1c001dfb, 0x1c001dfc,
497];
498
499impl PartialEq for Image {
500 fn eq(&self, other: &Self) -> bool {
501 self.id == other.id
502 && self.layout == other.layout
503 && self.size == other.size
504 && self.alt == other.alt
505 && self.background == other.background
506 && self.printout == other.printout
507 && self.tags == other.tags
508 && self.link == other.link
509 && self.text == other.text
510 }
511}
512
513impl Image {
514 pub(crate) fn read(
515 revision: &Revision<'_>,
516 id: ExGuid,
517 node: &crate::document::Element<'_>,
518 ) -> Result<Self, Error> {
519 let invalid = |message| Error { offset: 0, message };
520 let Kind::Image {
521 container,
522 alt,
523 background,
524 printout,
525 picture_width,
526 picture_height,
527 link,
528 filename,
529 ..
530 } = &node.kind
531 else {
532 unreachable!()
533 };
534 let payload = |container: Option<&ExGuid>| -> Result<Option<Arc<[u8]>>, Error> {
535 let Some(container) = container else {
536 return Ok(None);
537 };
538 let data = revision
539 .nodes
540 .get(container)
541 .ok_or_else(|| invalid("Missing canvas image data"))?;
542 match &data.kind {
543 Kind::File { payload, .. } => Ok(payload.map(Arc::from)),
544 _ => Err(invalid("Canvas image data has the wrong type")),
545 }
546 };
547 let bytes = payload(container.as_ref())?;
548 let fields = node.extra.first().map(Vec::as_slice).unwrap_or_default();
549 let web = fields.iter().find_map(|field| match &field.value {
550 FieldValue::Objects(ids) if field.id == 0x200034c8 => ids.first(),
551 _ => None,
552 });
553 Ok(Self {
554 id,
555 layout: node.layout.clone(),
556 size: picture_width.zip(*picture_height).map(|(w, h)| [w, h]),
557 bytes,
558 display: payload(web)?,
559 alt: alt.clone(),
560 background: background.unwrap_or(false),
561 printout: printout.unwrap_or(false).then(|| Printout {
562 page: fields
563 .iter()
564 .find_map(|field| match field.value {
565 FieldValue::Bytes(&[a, b, c, d]) if field.id == 0x14001df9 => {
566 Some(u32::from_le_bytes([a, b, c, d]))
567 }
568 _ => None,
569 })
570 .unwrap_or(0),
571 stored: filename
572 .iter()
573 .map(|name| (0x1c001dd7, crate::create::string(name)))
574 .chain(fields.iter().filter_map(|field| match field.value {
575 FieldValue::Bytes(bytes) if PRINTOUT_PROPERTIES.contains(&field.id) => {
576 Some((field.id, bytes.to_vec()))
577 }
578 _ => None,
579 }))
580 .collect(),
581 }),
582 tags: node.tags.clone(),
583 link: link.clone(),
584 text: fields
585 .iter()
586 .find_map(|field| match field.value {
587 FieldValue::Bytes(bytes) if field.id == 0x1c001c22 => Some(bytes),
588 _ => None,
589 })
590 .map(|bytes| {
591 let units: Vec<u16> = bytes
592 .chunks_exact(2)
593 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
594 .collect();
595 String::from_utf16_lossy(&units)
596 .trim_end_matches('\0')
597 .to_owned()
598 })
599 .filter(|text| !text.trim().is_empty())
600 .map(|text| Recognized {
601 text,
602 stored: fields
603 .iter()
604 .filter_map(|field| match field.value {
605 FieldValue::Bytes(bytes)
606 if RECOGNIZED_PROPERTIES.contains(&field.id) =>
607 {
608 Some((field.id, bytes.to_vec()))
609 }
610 _ => None,
611 })
612 .collect(),
613 }),
614 })
615 }
616}
617
618#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
619pub struct Unsupported {
620 pub id: ExGuid,
621 pub jcid: u32,
622 pub layout: Layout,
623}
624
625impl Page {
626 pub fn from_document(document: &Document<'_>, title: &str) -> Result<Self, Error> {
627 let mut selected = None;
628 for (space, id) in document.pages()? {
629 let revision = document.active(space)?;
630 if page_title(revision, id)
631 .map(crate::edit::without_fields)
632 .as_deref()
633 == Some(title)
634 {
635 if selected.is_some() {
636 return Err(Error {
637 offset: 0,
638 message: "More than one active page has the requested title",
639 });
640 }
641 selected = Some((revision, id));
642 }
643 }
644 let (revision, id) = selected.ok_or(Error {
645 offset: 0,
646 message: "No active page has the requested title",
647 })?;
648 Self::from_revision(revision, id)
649 }
650
651 /// The page's content under fresh identities, for a copy into this or another section:
652 /// OneNote gives a copied page and every object on it new identities, and stored
653 /// links keep naming the original. Content outside the model has no copy, so an
654 /// `Unsupported` object is an error.
655 pub fn copy(&self) -> Result<Self, Error> {
656 self.copy_with(&mut [])
657 }
658
659 /// `copy`, renaming `ids`, identities on this page, to theirs on the copy.
660 pub fn copy_with(&self, ids: &mut [ExGuid]) -> Result<Self, Error> {
661 let invalid = |message| Error { offset: 0, message };
662 let mut value =
663 serde_json::to_value((self, &*ids)).map_err(|_| invalid("Page is not serializable"))?;
664 let mut fresh: BTreeMap<String, String> = BTreeMap::new();
665 fn walk(
666 value: &mut serde_json::Value,
667 fresh: &mut BTreeMap<String, String>,
668 ) -> Result<(), Error> {
669 match value {
670 serde_json::Value::String(text) if text.starts_with('{') => {
671 if text.parse::<ExGuid>().is_ok() {
672 if !fresh.contains_key(text) {
673 fresh.insert(text.clone(), text::new_id()?.to_string());
674 }
675 *text = fresh[text].clone();
676 }
677 }
678 serde_json::Value::Array(items) => {
679 for item in items {
680 walk(item, fresh)?;
681 }
682 }
683 serde_json::Value::Object(fields) => {
684 if fields.contains_key("Unsupported") {
685 return Err(Error {
686 offset: 0,
687 message: "Content outside the page model cannot be copied",
688 });
689 }
690 let keys: Vec<String> = fields.keys().cloned().collect();
691 for key in keys {
692 let mut item = fields.remove(&key).unwrap();
693 walk(&mut item, fresh)?;
694 let key = match key.parse::<ExGuid>() {
695 Ok(_) => {
696 if !fresh.contains_key(&key) {
697 fresh.insert(key.clone(), text::new_id()?.to_string());
698 }
699 fresh[&key].clone()
700 }
701 Err(_) => key,
702 };
703 fields.insert(key, item);
704 }
705 }
706 _ => {}
707 }
708 Ok(())
709 }
710 walk(&mut value, &mut fresh)?;
711 let (mut copy, renamed): (Self, Vec<ExGuid>) =
712 serde_json::from_value(value).map_err(|_| invalid("Page copy does not deserialize"))?;
713 ids.copy_from_slice(&renamed);
714 copy.identity = Some(crate::write::fresh_guid()?);
715 // A paragraph's indent level follows its parent chain; a level beyond that comes
716 // from an outline group, which the model does not hold.
717 fn levels(paragraphs: &[PageParagraph]) -> bool {
718 let by_id: BTreeMap<ExGuid, u32> = paragraphs.iter().map(|p| (p.id, p.level)).collect();
719 paragraphs.iter().all(|paragraph| {
720 let expected = match paragraph.parent {
721 Some(parent) => by_id.get(&parent).map_or(0, |level| level + 1),
722 None => 1,
723 };
724 paragraph.level == expected
725 && match &paragraph.content {
726 ParagraphContent::Table(table) => table
727 .rows
728 .iter()
729 .flat_map(|row| &row.cells)
730 .all(|cell| levels(&cell.paragraphs)),
731 _ => true,
732 }
733 })
734 }
735 let expressible = copy.objects.iter().all(|object| match object {
736 PageObject::Outline(outline) => levels(&outline.paragraphs),
737 _ => true,
738 });
739 if !expressible {
740 return Err(invalid("Outline groups cannot be copied"));
741 }
742 Ok(copy)
743 }
744
745 /// The date and time the title's date fields show, when it has both.
746 pub fn date_text(&self) -> Option<[String; 2]> {
747 let title = self.objects.iter().find_map(|object| match object {
748 PageObject::Title(title) => Some(title),
749 _ => None,
750 })?;
751 let outline = title
752 .outlines
753 .iter()
754 .find(|outline| Some(outline.id) == title.date)?;
755 let [first, second] = &outline.paragraphs[..] else {
756 return None;
757 };
758 let [first, second] = [first.text()?, second.text()?];
759 let text = |object: &TextObject| object.text.text().to_owned();
760 // OneNote writes the date first; a field marked as the other role says otherwise.
761 Some(
762 if first.date_field == Some(DateField::Time)
763 || second.date_field == Some(DateField::Date)
764 {
765 [text(second), text(first)]
766 } else {
767 [text(first), text(second)]
768 },
769 )
770 }
771
772 /// The notebook-management identity of the page in an active page revision, which
773 /// internal links name.
774 pub fn identity_of(revision: &Revision<'_>) -> Option<[u8; 16]> {
775 revision
776 .roots
777 .get(&2)
778 .and_then(|id| revision.nodes.get(id))
779 .filter(|node| matches!(node.kind, Kind::Metadata { .. }))
780 .and_then(|node| node.extra.first())
781 .and_then(|fields| fields.iter().find(|field| field.id == 0x1c001c30))
782 .and_then(|field| match field.value {
783 FieldValue::Bytes(bytes) => bytes.try_into().ok(),
784 _ => None,
785 })
786 }
787
788 /// The title and outline level (1 at the top) a section's page list shows, read from
789 /// the page's metadata without building its content.
790 pub fn heading(revision: &Revision<'_>, id: ExGuid) -> (String, u32) {
791 let level = revision
792 .roots
793 .get(&2)
794 .and_then(|id| revision.nodes.get(id))
795 .and_then(|node| match node.kind {
796 Kind::Metadata { level, .. } => level,
797 _ => None,
798 });
799 (
800 crate::edit::without_fields(page_title(revision, id).unwrap_or_default()),
801 level.unwrap_or(1),
802 )
803 }
804
805 /// The single active page declared in one page object space.
806 pub fn from_space(document: &Document<'_>, space: ExGuid) -> Result<Self, Error> {
807 let pages = document.pages_in(space)?;
808 let [id] = pages.as_slice() else {
809 return Err(Error {
810 offset: 0,
811 message: "Choose an object space containing one active page",
812 });
813 };
814 Self::from_revision(document.active(space)?, *id)
815 }
816
817 pub fn from_revision(revision: &Revision<'_>, id: ExGuid) -> Result<Self, Error> {
818 let invalid = |message| Error { offset: 0, message };
819 let root = revision
820 .nodes
821 .get(&id)
822 .ok_or_else(|| invalid("Missing canvas page"))?;
823 let Kind::Page {
824 margin_origin_x,
825 margin_origin_y,
826 rtl,
827 ..
828 } = &root.kind
829 else {
830 return Err(invalid("Canvas root is not a page"));
831 };
832 let metadata = revision
833 .roots
834 .get(&2)
835 .and_then(|id| revision.nodes.get(id))
836 .filter(|node| matches!(node.kind, Kind::Metadata { .. }))
837 .and_then(|node| node.extra.first());
838 let mut page = Self {
839 title: crate::edit::without_fields(page_title(revision, id).unwrap_or_default()),
840 identity: Self::identity_of(revision),
841 created: metadata
842 .and_then(|fields| fields.iter().find(|field| field.id == 0x18001c65))
843 .map(|field| {
844 let FieldValue::Bytes(bytes) = field.value else {
845 return Err(invalid("The page creation date is damaged"));
846 };
847 Ok(u64::from_le_bytes(bytes.try_into().map_err(|_| {
848 invalid("The page creation date is damaged")
849 })?))
850 })
851 .transpose()?,
852 // OneNote 2010 snaps to, and on the first move writes, (36, 14.4) when unset: 1.0
853 // and 0.4 half inches, read as stored ones are.
854 margin_origin: [
855 margin_origin_x.unwrap_or(1.0_f32 * 36.0),
856 margin_origin_y.unwrap_or(0.4_f32 * 36.0),
857 ],
858 rtl: rtl.unwrap_or(false),
859 color: root
860 .extra
861 .first()
862 .and_then(|fields| fields.iter().find(|field| field.id == PAGE_COLOR))
863 .map(|field| match field.value {
864 FieldValue::Bytes(&[red, green, blue, alpha]) => {
865 Ok(u32::from_le_bytes([red, green, blue, alpha]))
866 }
867 _ => Err(invalid("The page color is damaged")),
868 })
869 .transpose()?,
870 rule_lines: root
871 .extra
872 .first()
873 .and_then(|fields| RuleLines::read(fields)),
874 objects: Vec::new(),
875 definitions: BTreeMap::new(),
876 };
877 let mut roots: Vec<_> = root
878 .children
879 .iter()
880 .chain(&root.structure)
881 .rev()
882 .map(|id| (*id, None))
883 .collect();
884 let mut seen = BTreeSet::new();
885 while let Some((id, title_index)) = roots.pop() {
886 if !seen.insert(id) {
887 return Err(invalid("Repeated canvas page object"));
888 }
889 let node = revision
890 .nodes
891 .get(&id)
892 .ok_or_else(|| invalid("Missing canvas page object"))?;
893 if title_index.is_some() && !matches!(node.kind, Kind::Outline { .. }) {
894 return Err(invalid("Canvas title child is not an outline"));
895 }
896 match &node.kind {
897 Kind::Title => {
898 let index = page.objects.len();
899 page.objects.push(PageObject::Title(Title {
900 id,
901 date: None,
902 layout: node.layout.clone(),
903 outlines: Vec::new(),
904 }));
905 roots.extend(node.children.iter().rev().map(|id| (*id, Some(index))));
906 }
907 Kind::Outline { indents } => {
908 let fields = node.extra.first().map(Vec::as_slice).unwrap_or_default();
909 if let Some(index) = title_index
910 && fields.iter().any(|field| {
911 field.id == 0x88001cb5 && matches!(field.value, FieldValue::NoData)
912 })
913 {
914 let PageObject::Title(title) = &mut page.objects[index] else {
915 unreachable!()
916 };
917 if title.date.replace(id).is_some() {
918 return Err(invalid("The page has more than one date field"));
919 }
920 }
921 let min_width = fields
922 .iter()
923 .find(|field| field.id == 0x14001cec)
924 .map(|field| {
925 let FieldValue::Bytes(bytes) = field.value else {
926 return Err(invalid("Invalid canvas minimum outline width"));
927 };
928 let width =
929 f32::from_le_bytes(bytes.try_into().map_err(|_| {
930 invalid("Invalid canvas minimum outline width")
931 })?) * 36.0;
932 if !width.is_finite() || width <= 0.0 {
933 return Err(invalid("Invalid canvas minimum outline width"));
934 }
935 Ok(width)
936 })
937 .transpose()?;
938 let title_text = |node: &Element<'_>| {
939 node.extra.first().is_some_and(|fields| {
940 fields.iter().any(|field| {
941 field.id == 0x88001cb4 && matches!(field.value, FieldValue::NoData)
942 })
943 })
944 };
945 let mut outline = Outline {
946 id,
947 // OneNote flags the outline and its paragraph, and still shows an outline
948 // whose paragraph alone is flagged as the title.
949 title: title_text(node)
950 || node
951 .children
952 .iter()
953 .any(|id| revision.nodes.get(id).is_some_and(title_text)),
954 min_width,
955 layout: node.layout.clone(),
956 indents: indents.clone(),
957 paragraphs: Vec::new(),
958 unsupported: Vec::new(),
959 };
960 (outline.paragraphs, outline.unsupported) = read_paragraphs(
961 revision,
962 node,
963 &node.format,
964 &mut page.definitions,
965 &mut seen,
966 0,
967 )?;
968 if let Some(index) = title_index {
969 let PageObject::Title(title) = &mut page.objects[index] else {
970 unreachable!()
971 };
972 title.outlines.push(outline);
973 } else {
974 page.objects.push(PageObject::Outline(outline));
975 }
976 }
977 Kind::Image { .. } | Kind::Attachment { .. } => {
978 for tag in node.tags.iter().filter_map(|tag| tag.definition) {
979 define(revision, tag, false, &mut page.definitions)?;
980 }
981 page.objects.push(match node.kind {
982 Kind::Image { .. } => PageObject::Image(Image::read(revision, id, node)?),
983 _ => PageObject::Attachment(Attachment::read(revision, id, node)?),
984 });
985 }
986 Kind::Ink { .. } => {
987 page.objects
988 .push(PageObject::Ink(Ink::read(revision, id, node)?));
989 }
990 _ => page.objects.push(PageObject::Unsupported(Unsupported {
991 id,
992 jcid: node.jcid,
993 layout: node.layout.clone(),
994 })),
995 }
996 }
997 Ok(page)
998 }
999}
1000
1001/// The format `container` passes to its children, as reading its page inherits it.
1002fn inherited_by(
1003 revision: &Revision<'_>,
1004 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
1005 container: ExGuid,
1006) -> Result<Format, Error> {
1007 let invalid = |message| Error { offset: 0, message };
1008 let node = revision
1009 .nodes
1010 .get(&container)
1011 .ok_or_else(|| invalid("Missing canvas outline object"))?;
1012 let parent = || -> Result<ExGuid, Error> {
1013 match parents.get(&container).map(Vec::as_slice) {
1014 Some([parent]) => Ok(*parent),
1015 _ => Err(invalid("Page content must have one parent")),
1016 }
1017 };
1018 Ok(match &node.kind {
1019 Kind::Outline { .. } => node.format.clone(),
1020 Kind::OutlineGroup => node
1021 .format
1022 .inherit(&inherited_by(revision, parents, parent()?)?),
1023 Kind::Paragraph {
1024 paragraph_style, ..
1025 } => {
1026 let style = match paragraph_style {
1027 Some(id) => revision
1028 .nodes
1029 .get(id)
1030 .map(|n| n.format.clone())
1031 .unwrap_or_default(),
1032 None => Format::default(),
1033 };
1034 node.format
1035 .inherit(&style)
1036 .inherit(&inherited_by(revision, parents, parent()?)?)
1037 }
1038 Kind::Cell { .. } => {
1039 let row = parent()?;
1040 let [table] = parents.get(&row).map(Vec::as_slice).unwrap_or_default() else {
1041 return Err(invalid("A table row must have one table"));
1042 };
1043 let [holder] = parents.get(table).map(Vec::as_slice).unwrap_or_default() else {
1044 return Err(invalid("A table must have one paragraph"));
1045 };
1046 node.format.inherit(
1047 &revision.nodes[table]
1048 .format
1049 .inherit(&inherited_by(revision, parents, *holder)?),
1050 )
1051 }
1052 _ => Format::default(),
1053 })
1054}
1055
1056/// The text of rich-text object `text` as its page model shows it, read from its ancestors
1057/// alone.
1058pub(crate) fn text_of(
1059 revision: &Revision<'_>,
1060 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
1061 text: ExGuid,
1062) -> Result<Paragraph, Error> {
1063 let invalid = |message| Error { offset: 0, message };
1064 let [paragraph] = parents.get(&text).map(Vec::as_slice).unwrap_or_default() else {
1065 return Err(invalid("Select text belonging to one paragraph"));
1066 };
1067 let format = inherited_by(revision, parents, *paragraph)?;
1068 let node = revision
1069 .nodes
1070 .get(&text)
1071 .ok_or_else(|| invalid("Missing text object"))?;
1072 let Kind::RichText {
1073 paragraph_style, ..
1074 } = &node.kind
1075 else {
1076 return Err(invalid("Select a rich-text object"));
1077 };
1078 let runs = revision.text_runs(text)?;
1079 Ok(if runs.is_empty() {
1080 let style = match paragraph_style {
1081 Some(id) => revision
1082 .nodes
1083 .get(id)
1084 .map(|n| n.format.clone())
1085 .unwrap_or_default(),
1086 None => Format::default(),
1087 };
1088 Paragraph::new(String::new(), node.format.inherit(&style).inherit(&format))
1089 } else {
1090 Paragraph::from_runs(
1091 runs.into_iter()
1092 .map(|run| (run.text.to_owned(), run.format.inherit(&format))),
1093 )
1094 })
1095}
1096
1097/// Adds list or tag definition `id` to `definitions`.
1098fn define(
1099 revision: &Revision<'_>,
1100 id: ExGuid,
1101 is_list: bool,
1102 definitions: &mut BTreeMap<ExGuid, Definition>,
1103) -> Result<(), Error> {
1104 let invalid = |message| Error { offset: 0, message };
1105 let definition = revision
1106 .nodes
1107 .get(&id)
1108 .ok_or_else(|| invalid("Missing canvas list or tag definition"))?;
1109 let kind = match &definition.kind {
1110 Kind::List {
1111 font,
1112 format,
1113 restart,
1114 bullet,
1115 } if is_list => Kind::List {
1116 font: font.clone(),
1117 format: format.clone(),
1118 restart: *restart,
1119 bullet: *bullet,
1120 },
1121 Kind::TagDefinition {
1122 label,
1123 action_type,
1124 shape,
1125 color,
1126 highlight,
1127 } if !is_list => Kind::TagDefinition {
1128 label: label.clone(),
1129 action_type: *action_type,
1130 shape: *shape,
1131 color: *color,
1132 highlight: *highlight,
1133 },
1134 _ => return Err(invalid("Canvas list or tag definition has the wrong type")),
1135 };
1136 definitions.insert(
1137 id,
1138 Definition {
1139 kind,
1140 format: definition.format.clone(),
1141 },
1142 );
1143 Ok(())
1144}
1145
1146fn read_paragraphs(
1147 revision: &Revision<'_>,
1148 container: &Element<'_>,
1149 inherited: &Format,
1150 definitions: &mut BTreeMap<ExGuid, Definition>,
1151 seen: &mut BTreeSet<ExGuid>,
1152 depth: usize,
1153) -> Result<(Vec<PageParagraph>, Vec<Unsupported>), Error> {
1154 let invalid = |message| Error { offset: 0, message };
1155 let default_format = Format::default();
1156 let style = |id: Option<ExGuid>| -> Result<&Format, Error> {
1157 let Some(id) = id else {
1158 return Ok(&default_format);
1159 };
1160 let node = revision
1161 .nodes
1162 .get(&id)
1163 .ok_or_else(|| invalid("Missing canvas paragraph style"))?;
1164 if !matches!(node.kind, Kind::Style { .. }) {
1165 return Err(invalid("Canvas paragraph style has the wrong type"));
1166 }
1167 Ok(&node.format)
1168 };
1169
1170 let mut paragraphs = Vec::new();
1171 let mut unsupported = Vec::new();
1172 let mut pending: Vec<_> = container
1173 .children
1174 .iter()
1175 .rev()
1176 .map(|id| {
1177 (
1178 *id,
1179 None,
1180 u32::from(container.child_level.unwrap_or(0)),
1181 inherited.clone(),
1182 )
1183 })
1184 .collect();
1185 while let Some((id, parent, level, inherited)) = pending.pop() {
1186 if !seen.insert(id) {
1187 return Err(invalid("Repeated canvas outline object"));
1188 }
1189 let node = revision
1190 .nodes
1191 .get(&id)
1192 .ok_or_else(|| invalid("Missing canvas outline object"))?;
1193 let next_level = level
1194 .checked_add(u32::from(node.child_level.unwrap_or(0)))
1195 .ok_or_else(|| invalid("Canvas outline level overflow"))?;
1196 match &node.kind {
1197 Kind::Paragraph {
1198 lists,
1199 paragraph_style,
1200 collapse_state,
1201 } => {
1202 let format = node
1203 .format
1204 .inherit(style(*paragraph_style)?)
1205 .inherit(&inherited);
1206 let mut base_style = *paragraph_style;
1207 let [content_id] = node.content.as_slice() else {
1208 return Err(invalid("A paragraph must contain one content object"));
1209 };
1210 if !seen.insert(*content_id) {
1211 return Err(invalid("Repeated canvas paragraph content"));
1212 }
1213 let content = revision
1214 .nodes
1215 .get(content_id)
1216 .ok_or_else(|| invalid("Missing canvas paragraph content"))?;
1217 let media = MediaIndex {
1218 recordings: content.media_ids.clone(),
1219 time_ms: content.media_time_ms,
1220 };
1221 let content = if let Kind::RichText {
1222 paragraph_style, ..
1223 } = &content.kind
1224 {
1225 base_style = paragraph_style.or(base_style);
1226 let runs = revision.text_runs(*content_id)?;
1227 let text_content = if runs.is_empty() {
1228 Paragraph::new(
1229 String::new(),
1230 content
1231 .format
1232 .inherit(style(*paragraph_style)?)
1233 .inherit(&format),
1234 )
1235 } else {
1236 Paragraph::from_runs(
1237 runs.into_iter()
1238 .map(|run| (run.text.to_owned(), run.format.inherit(&format))),
1239 )
1240 };
1241 ParagraphContent::Text(TextObject {
1242 id: *content_id,
1243 date_field: content
1244 .extra
1245 .first()
1246 .map(Vec::as_slice)
1247 .unwrap_or_default()
1248 .iter()
1249 .filter(|field| matches!(field.value, FieldValue::NoData))
1250 .filter_map(|field| match field.id {
1251 0x88001cb5 => Some(DateField::Date),
1252 0x88001c87 => Some(DateField::Time),
1253 _ => None,
1254 })
1255 .try_fold(None, |previous, field| {
1256 if previous.is_some() {
1257 Err(invalid("The page date field has conflicting roles"))
1258 } else {
1259 Ok(Some(field))
1260 }
1261 })?,
1262 text: text_content,
1263 tags: content.tags.clone(),
1264 })
1265 } else if matches!(content.kind, Kind::Image { .. }) {
1266 ParagraphContent::Image(Image::read(revision, *content_id, content)?)
1267 } else if matches!(content.kind, Kind::Attachment { .. }) {
1268 ParagraphContent::Attachment(Attachment::read(revision, *content_id, content)?)
1269 } else if matches!(content.kind, Kind::Ink { .. }) {
1270 ParagraphContent::Ink(Ink::read(revision, *content_id, content)?)
1271 } else if matches!(content.kind, Kind::Table { .. }) {
1272 ParagraphContent::Table(read_table(
1273 revision,
1274 *content_id,
1275 &content.format.inherit(&format),
1276 definitions,
1277 seen,
1278 depth + 1,
1279 )?)
1280 } else {
1281 ParagraphContent::Unsupported(Unsupported {
1282 id: *content_id,
1283 jcid: content.jcid,
1284 layout: content.layout.clone(),
1285 })
1286 };
1287 for (id, is_list) in lists.iter().map(|id| (id, true)).chain(
1288 node.tags
1289 .iter()
1290 .chain(match &content {
1291 ParagraphContent::Text(text) => text.tags.as_slice(),
1292 ParagraphContent::Table(table) => table.tags.as_slice(),
1293 ParagraphContent::Image(image) => image.tags.as_slice(),
1294 ParagraphContent::Attachment(file) => file.tags.as_slice(),
1295 ParagraphContent::Ink(_) | ParagraphContent::Unsupported(_) => &[],
1296 })
1297 .filter_map(|tag| tag.definition.as_ref())
1298 .map(|id| (id, false)),
1299 ) {
1300 define(revision, *id, is_list, definitions)?;
1301 }
1302 if let Some(id) = base_style {
1303 let definition = revision
1304 .nodes
1305 .get(&id)
1306 .ok_or_else(|| invalid("Missing canvas paragraph style"))?;
1307 let Kind::Style { name, next } = &definition.kind else {
1308 return Err(invalid("Canvas paragraph style has the wrong type"));
1309 };
1310 definitions.entry(id).or_insert_with(|| Definition {
1311 kind: Kind::Style {
1312 name: name.clone(),
1313 next: next.clone(),
1314 },
1315 format: definition.format.clone(),
1316 });
1317 }
1318 paragraphs.push(PageParagraph {
1319 id,
1320 parent,
1321 level,
1322 style: base_style,
1323 format: format.clone(),
1324 content,
1325 lists: lists.clone(),
1326 tags: node.tags.clone(),
1327 media,
1328 collapsed: *collapse_state == Some(1),
1329 });
1330 pending.extend(
1331 node.children
1332 .iter()
1333 .rev()
1334 .map(|child| (*child, Some(id), next_level, format.clone())),
1335 );
1336 }
1337 Kind::OutlineGroup => pending.extend(
1338 node.children
1339 .iter()
1340 .rev()
1341 .map(|child| (*child, parent, next_level, node.format.inherit(&inherited))),
1342 ),
1343 _ => unsupported.push(Unsupported {
1344 id,
1345 jcid: node.jcid,
1346 layout: node.layout.clone(),
1347 }),
1348 }
1349 }
1350 Ok((paragraphs, unsupported))
1351}
1352
1353fn read_table(
1354 revision: &Revision<'_>,
1355 id: ExGuid,
1356 inherited: &Format,
1357 definitions: &mut BTreeMap<ExGuid, Definition>,
1358 seen: &mut BTreeSet<ExGuid>,
1359 depth: usize,
1360) -> Result<Table, Error> {
1361 let invalid = |message| Error { offset: 0, message };
1362 if depth > 64 {
1363 return Err(invalid("Tables are nested too deeply"));
1364 }
1365 let node = &revision.nodes[&id];
1366 let Kind::Table {
1367 rows,
1368 columns,
1369 widths,
1370 locked,
1371 borders,
1372 } = &node.kind
1373 else {
1374 unreachable!()
1375 };
1376 if rows.map(|n| n as usize) != Some(node.children.len())
1377 || node.children.is_empty()
1378 || columns.map(|n| n as usize) != Some(widths.len())
1379 || widths.is_empty()
1380 || (!locked.is_empty() && locked.len() != widths.len())
1381 || widths.iter().any(|w| !w.is_finite() || *w < 36.0)
1382 {
1383 return Err(invalid("Table dimensions are inconsistent"));
1384 }
1385 let mut table_rows = Vec::new();
1386 for row_id in &node.children {
1387 if !seen.insert(*row_id) {
1388 return Err(invalid("Repeated canvas table row"));
1389 }
1390 let row = revision
1391 .nodes
1392 .get(row_id)
1393 .ok_or_else(|| invalid("Missing canvas table row"))?;
1394 if !matches!(row.kind, Kind::Row) || row.children.len() != widths.len() {
1395 return Err(invalid("Table row dimensions are inconsistent"));
1396 }
1397 let mut cells = Vec::new();
1398 for cell_id in &row.children {
1399 if !seen.insert(*cell_id) {
1400 return Err(invalid("Repeated canvas table cell"));
1401 }
1402 let cell = revision
1403 .nodes
1404 .get(cell_id)
1405 .ok_or_else(|| invalid("Missing canvas table cell"))?;
1406 if cell.children.is_empty() {
1407 return Err(invalid("A table cell has no paragraphs"));
1408 }
1409 let Kind::Cell { shading, indents } = &cell.kind else {
1410 return Err(invalid("A table row contains an invalid cell"));
1411 };
1412 let (paragraphs, unsupported) = read_paragraphs(
1413 revision,
1414 cell,
1415 &cell.format.inherit(inherited),
1416 definitions,
1417 seen,
1418 depth,
1419 )?;
1420 cells.push(TableCell {
1421 id: *cell_id,
1422 layout: cell.layout.clone(),
1423 indents: indents.clone(),
1424 shading: *shading,
1425 paragraphs,
1426 unsupported,
1427 });
1428 }
1429 table_rows.push(TableRow { id: *row_id, cells });
1430 }
1431 Ok(Table {
1432 id,
1433 columns: widths
1434 .iter()
1435 .enumerate()
1436 .map(|(index, &width)| TableColumn {
1437 width,
1438 locked: locked.get(index).copied().unwrap_or(false),
1439 })
1440 .collect(),
1441 rows: table_rows,
1442 borders: *borders,
1443 layout: node.layout.clone(),
1444 tags: node.tags.clone(),
1445 })
1446}
1447
1448fn page_title<'a>(revision: &'a Revision<'_>, id: ExGuid) -> Option<&'a str> {
1449 revision
1450 .roots
1451 .get(&2)
1452 .and_then(|id| revision.nodes.get(id))
1453 .and_then(|node| {
1454 if let Kind::Metadata { title, .. } = &node.kind {
1455 title.as_deref()
1456 } else {
1457 None
1458 }
1459 })
1460 .or_else(|| {
1461 if let Kind::Page {
1462 alternate_title, ..
1463 } = &revision.nodes.get(&id)?.kind
1464 {
1465 alternate_title.as_deref()
1466 } else {
1467 None
1468 }
1469 })
1470}
1471
1472impl PageParagraph {
1473 pub fn text(&self) -> Option<&TextObject> {
1474 match &self.content {
1475 ParagraphContent::Text(text) => Some(text),
1476 _ => None,
1477 }
1478 }
1479 pub fn text_mut(&mut self) -> Option<&mut TextObject> {
1480 match &mut self.content {
1481 ParagraphContent::Text(text) => Some(text),
1482 _ => None,
1483 }
1484 }
1485}
1486
1487#[cfg(test)]
1488mod tests {
1489 use super::*;
1490 use crate::document::{Element, Field, FieldValue, TextRun};
1491
1492 fn id(n: u32) -> ExGuid {
1493 ExGuid {
1494 n,
1495 ..ExGuid::default()
1496 }
1497 }
1498
1499 const IDENTITY: [u8; 16] = [7; 16];
1500
1501 fn element(kind: Kind<'_>) -> Element<'_> {
1502 Element {
1503 jcid: 0,
1504 children: Vec::new(),
1505 content: Vec::new(),
1506 structure: Vec::new(),
1507 spaces: Vec::new(),
1508 child_level: None,
1509 layout: Layout::default(),
1510 format: Format::default(),
1511 created: None,
1512 modified: None,
1513 original_author: None,
1514 latest_author: None,
1515 media_ids: Vec::new(),
1516 media_time_ms: None,
1517 tags: Vec::new(),
1518 kind,
1519 extra: Vec::new(),
1520 }
1521 }
1522
1523 fn revision() -> Revision<'static> {
1524 let mut page = element(Kind::Page {
1525 alternate_title: Some("Fallback".into()),
1526 level: None,
1527 width: None,
1528 height: None,
1529 margin_origin_x: Some(36.0),
1530 margin_origin_y: Some(12.0),
1531 rtl: None,
1532 recordings: Vec::new(),
1533 });
1534 page.children.push(id(2));
1535 let mut outline = element(Kind::Outline {
1536 indents: vec![18.0, 0.0, 36.0],
1537 });
1538 outline.children.push(id(3));
1539 outline.child_level = Some(1);
1540 outline.format.font_size = Some(9.0);
1541 outline.format.bold = Some(true);
1542 let mut paragraph = element(Kind::Paragraph {
1543 lists: Vec::new(),
1544 paragraph_style: Some(id(5)),
1545 collapse_state: Some(1),
1546 });
1547 paragraph.content.push(id(4));
1548 paragraph.format.bold = Some(false);
1549 let text = element(Kind::RichText {
1550 text: "ab".into(),
1551 runs: vec![TextRun {
1552 start: 0,
1553 end: 2,
1554 format: None,
1555 extra_set: None,
1556 }],
1557 paragraph_style: None,
1558 boilerplate: false,
1559 });
1560 let mut style = element(Kind::Style {
1561 name: Some("Body".into()),
1562 next: None,
1563 });
1564 style.format.font_size = Some(12.0);
1565 let mut metadata = element(Kind::Metadata {
1566 title: Some("Page title".into()),
1567 level: None,
1568 });
1569 metadata.extra = vec![vec![Field {
1570 id: 0x1c001c30,
1571 value: FieldValue::Bytes(&IDENTITY),
1572 }]];
1573 Revision {
1574 roots: BTreeMap::from([(2, id(6))]),
1575 nodes: BTreeMap::from([
1576 (id(1), page),
1577 (id(2), outline),
1578 (id(3), paragraph),
1579 (id(4), text),
1580 (id(5), style),
1581 (id(6), metadata),
1582 ]),
1583 }
1584 }
1585
1586 fn table_revision() -> Revision<'static> {
1587 let mut source = revision();
1588 source.nodes.get_mut(&id(3)).unwrap().content = vec![id(7)];
1589 let mut table = element(Kind::Table {
1590 rows: Some(1),
1591 columns: Some(2),
1592 widths: vec![37.11, 99.0],
1593 locked: vec![false, true],
1594 borders: Some(false),
1595 });
1596 table.children = vec![id(8)];
1597 let mut row = element(Kind::Row);
1598 row.children = vec![id(9), id(10)];
1599 source.nodes.insert(id(7), table);
1600 source.nodes.insert(id(8), row);
1601 for (cell, paragraph, text) in [(9, 11, 4), (10, 12, 13)] {
1602 let mut node = element(Kind::Cell {
1603 shading: Some(0x00ffff),
1604 indents: vec![18.0, 0.0, 27.0, 27.0],
1605 });
1606 node.child_level = Some(1);
1607 node.layout.max_width = Some(268.8);
1608 node.children = vec![id(paragraph)];
1609 if cell == 10 {
1610 node.format.font_size = Some(13.0);
1611 }
1612 source.nodes.insert(id(cell), node);
1613 let mut node = element(Kind::Paragraph {
1614 lists: Vec::new(),
1615 paragraph_style: None,
1616 collapse_state: None,
1617 });
1618 node.content = vec![id(text)];
1619 source.nodes.insert(id(paragraph), node);
1620 }
1621 source.nodes.insert(
1622 id(13),
1623 element(Kind::RichText {
1624 text: "cd".into(),
1625 runs: vec![TextRun {
1626 start: 0,
1627 end: 2,
1628 format: None,
1629 extra_set: None,
1630 }],
1631 paragraph_style: None,
1632 boilerplate: false,
1633 }),
1634 );
1635 source
1636 }
1637
1638 #[test]
1639 fn table_import_owns_cells_and_inherits_format_across_containers() {
1640 let page = {
1641 let source = table_revision();
1642 let before = serde_json::to_vec(&source).unwrap();
1643 let page = Page::from_revision(&source, id(1)).unwrap();
1644 assert_eq!(serde_json::to_vec(&source).unwrap(), before);
1645 page
1646 };
1647 let PageObject::Outline(outline) = &page.objects[0] else {
1648 panic!()
1649 };
1650 let ParagraphContent::Table(table) = &outline.paragraphs[0].content else {
1651 panic!()
1652 };
1653 assert_eq!(table.id, id(7));
1654 assert_eq!(table.borders, Some(false));
1655 assert_eq!(
1656 table.columns,
1657 [
1658 TableColumn {
1659 width: 37.11,
1660 locked: false
1661 },
1662 TableColumn {
1663 width: 99.0,
1664 locked: true
1665 }
1666 ]
1667 );
1668 assert_eq!(table.rows[0].id, id(8));
1669 for (index, cell) in table.rows[0].cells.iter().enumerate() {
1670 assert_eq!(cell.id, id(9 + index as u32));
1671 assert_eq!(cell.layout.max_width, Some(268.8));
1672 assert_eq!(cell.indents, [18.0, 0.0, 27.0, 27.0]);
1673 assert_eq!(cell.shading, Some(0x00ffff));
1674 let paragraph = &cell.paragraphs[0];
1675 assert_eq!(paragraph.parent, None);
1676 assert_eq!(paragraph.level, 1);
1677 assert_eq!(paragraph.id, id(11 + index as u32));
1678 let text = paragraph.text().unwrap();
1679 assert_eq!(text.id, if index == 0 { id(4) } else { id(13) });
1680 assert_eq!(text.text.text(), if index == 0 { "ab" } else { "cd" });
1681 assert_eq!(
1682 text.text.spans()[0].format.font_size,
1683 Some(if index == 0 { 12.0 } else { 13.0 })
1684 );
1685 assert_eq!(text.text.spans()[0].format.bold, Some(false));
1686 }
1687 }
1688
1689 #[test]
1690 fn absent_column_locks_use_the_specified_unlocked_default() {
1691 let mut source = table_revision();
1692 let Kind::Table { locked, .. } = &mut source.nodes.get_mut(&id(7)).unwrap().kind else {
1693 panic!()
1694 };
1695 locked.clear();
1696 let page = Page::from_revision(&source, id(1)).unwrap();
1697 let PageObject::Outline(outline) = &page.objects[0] else {
1698 panic!()
1699 };
1700 let ParagraphContent::Table(table) = &outline.paragraphs[0].content else {
1701 panic!()
1702 };
1703 assert!(table.columns.iter().all(|column| !column.locked));
1704 }
1705
1706 #[test]
1707 fn table_import_rejects_aliases_cycles_and_inconsistent_dimensions() {
1708 for case in 0..7 {
1709 let mut source = table_revision();
1710 match case {
1711 0 => source.nodes.get_mut(&id(8)).unwrap().children[1] = id(9),
1712 1 => source.nodes.get_mut(&id(11)).unwrap().content = vec![id(7)],
1713 2 => source.nodes.get_mut(&id(12)).unwrap().content = vec![id(4)],
1714 3 => source.nodes.get_mut(&id(3)).unwrap().content.push(id(4)),
1715 4 => source.nodes.get_mut(&id(9)).unwrap().children.clear(),
1716 5 => {
1717 let Kind::Table { columns, .. } =
1718 &mut source.nodes.get_mut(&id(7)).unwrap().kind
1719 else {
1720 panic!()
1721 };
1722 *columns = Some(3);
1723 }
1724 _ => {
1725 let Kind::Table { widths, .. } =
1726 &mut source.nodes.get_mut(&id(7)).unwrap().kind
1727 else {
1728 panic!()
1729 };
1730 widths[0] = f32::INFINITY;
1731 }
1732 }
1733 assert!(Page::from_revision(&source, id(1)).is_err(), "case {case}");
1734 }
1735 }
1736
1737 #[test]
1738 fn nested_tables_import_with_a_bounded_depth() {
1739 for levels in [1, 62, 65] {
1740 let mut source = table_revision();
1741 let mut paragraph = id(11);
1742 for depth in 0..levels {
1743 let base = 20 + depth * 4;
1744 source.nodes.get_mut(&paragraph).unwrap().content = vec![id(base)];
1745 let mut table = element(Kind::Table {
1746 rows: Some(1),
1747 columns: Some(1),
1748 widths: vec![72.0],
1749 locked: vec![true],
1750 borders: Some(true),
1751 });
1752 table.children = vec![id(base + 1)];
1753 let mut row = element(Kind::Row);
1754 row.children = vec![id(base + 2)];
1755 let mut cell = element(Kind::Cell {
1756 shading: None,
1757 indents: vec![18.0, 0.0, 27.0, 27.0],
1758 });
1759 cell.child_level = Some(1);
1760 cell.children = vec![id(base + 3)];
1761 let mut child = element(Kind::Paragraph {
1762 lists: Vec::new(),
1763 paragraph_style: None,
1764 collapse_state: None,
1765 });
1766 child.content = vec![id(4)];
1767 source.nodes.extend([
1768 (id(base), table),
1769 (id(base + 1), row),
1770 (id(base + 2), cell),
1771 (id(base + 3), child),
1772 ]);
1773 paragraph = id(base + 3);
1774 }
1775 assert_eq!(Page::from_revision(&source, id(1)).is_ok(), levels < 64);
1776 }
1777 }
1778
1779 #[test]
1780 fn owns_page_content_and_resolves_style_before_ancestor_defaults() {
1781 let page = {
1782 let mut source = revision();
1783 source.nodes.get_mut(&id(4)).unwrap().tags.push(Tag {
1784 definition: Some(id(7)),
1785 action_type: None,
1786 shape: None,
1787 property_status: None,
1788 status: 0,
1789 created: Some(123),
1790 completed: None,
1791 start: None,
1792 due: None,
1793 task_id: None,
1794 extra_set: 0,
1795 });
1796 source.nodes.insert(
1797 id(7),
1798 element(Kind::TagDefinition {
1799 label: Some("To Do".into()),
1800 action_type: Some(0),
1801 shape: Some(3),
1802 color: None,
1803 highlight: None,
1804 }),
1805 );
1806 let before = serde_json::to_vec(&source).unwrap();
1807 let page = Page::from_revision(&source, id(1)).unwrap();
1808 assert_eq!(before, serde_json::to_vec(&source).unwrap());
1809 page
1810 };
1811 assert_eq!(page.title, "Page title");
1812 assert_eq!(page.margin_origin, [36.0, 12.0]);
1813 let PageObject::Outline(outline) = &page.objects[0] else {
1814 panic!()
1815 };
1816 assert_eq!(outline.paragraphs.len(), 1);
1817 let paragraph = &outline.paragraphs[0];
1818 assert!(paragraph.collapsed);
1819 assert_eq!(paragraph.level, 1);
1820 assert_eq!(paragraph.parent, None);
1821 assert_eq!(paragraph.style, Some(id(5)));
1822 assert!(matches!(&page.definitions[&id(5)].kind,
1823 Kind::Style { name: Some(name), .. } if name == "Body"));
1824 assert_eq!(page.definitions[&id(5)].format.font_size, Some(12.0));
1825 let text = &paragraph.text().unwrap().text;
1826 assert_eq!(text.text(), "ab");
1827 assert_eq!(text.spans()[0].format.font_size, Some(12.0));
1828 assert_eq!(text.spans()[0].format.bold, Some(false));
1829 assert_eq!(paragraph.text().unwrap().tags[0].created, Some(123));
1830 assert!(matches!(&page.definitions[&id(7)].kind,
1831 Kind::TagDefinition { label: Some(label), shape: Some(3), .. } if label == "To Do"));
1832 }
1833
1834 #[test]
1835 fn creation_time_comes_from_page_metadata_and_date_role_from_the_title_child() {
1836 let mut source = revision();
1837 source.nodes.get_mut(&id(1)).unwrap().created = Some(7);
1838 let bytes = 134_333_468_649_123_456_u64.to_le_bytes();
1839 source.nodes.get_mut(&id(6)).unwrap().extra = vec![vec![
1840 Field {
1841 id: 0x18001c65,
1842 value: FieldValue::Bytes(&bytes),
1843 },
1844 Field {
1845 id: 0x1c001c30,
1846 value: FieldValue::Bytes(&IDENTITY),
1847 },
1848 ]];
1849 source.nodes.get_mut(&id(1)).unwrap().children.clear();
1850 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7));
1851 let mut title = element(Kind::Title);
1852 title.children.push(id(2));
1853 source.nodes.insert(id(7), title);
1854 source.nodes.get_mut(&id(2)).unwrap().extra = vec![vec![Field {
1855 id: 0x88001cb5,
1856 value: FieldValue::NoData,
1857 }]];
1858 source.nodes.get_mut(&id(4)).unwrap().extra = vec![vec![Field {
1859 id: 0x88001c87,
1860 value: FieldValue::NoData,
1861 }]];
1862 let page = Page::from_revision(&source, id(1)).unwrap();
1863 assert_eq!(page.created, Some(134_333_468_649_123_456));
1864 let PageObject::Title(title) = &page.objects[0] else {
1865 panic!()
1866 };
1867 assert_eq!(title.date, Some(id(2)));
1868 assert_eq!(
1869 title.outlines[0].paragraphs[0].text().unwrap().date_field,
1870 Some(DateField::Time)
1871 );
1872 source.nodes.get_mut(&id(6)).unwrap().extra[0][0].value = FieldValue::Bytes(&bytes[..7]);
1873 assert!(Page::from_revision(&source, id(1)).is_err());
1874 source.nodes.get_mut(&id(6)).unwrap().extra.clear();
1875 let bare = Page::from_revision(&source, id(1)).unwrap();
1876 assert_eq!((bare.created, bare.identity), (None, None));
1877 }
1878
1879 #[test]
1880 fn title_role_and_minimum_width_come_from_root_properties() {
1881 let mut source = revision();
1882 source.nodes.get_mut(&id(1)).unwrap().children.clear();
1883 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7));
1884 let mut title = element(Kind::Title);
1885 title.children.push(id(2));
1886 source.nodes.insert(id(7), title);
1887 source.nodes.get_mut(&id(2)).unwrap().extra = vec![vec![
1888 Field {
1889 id: 0x88001cb4,
1890 value: FieldValue::NoData,
1891 },
1892 Field {
1893 id: 0x14001cec,
1894 value: FieldValue::Bytes(&[0, 0, 0x90, 0x40]),
1895 },
1896 ]];
1897 let page = Page::from_revision(&source, id(1)).unwrap();
1898 let PageObject::Title(title) = &page.objects[0] else {
1899 panic!()
1900 };
1901 assert!(title.outlines[0].title);
1902 assert_eq!(title.outlines[0].min_width, Some(162.0));
1903 source.nodes.get_mut(&id(2)).unwrap().extra[0][1].value =
1904 FieldValue::Bytes(&[0, 0, 0x80, 0x7f]);
1905 assert!(Page::from_revision(&source, id(1)).is_err());
1906 source.nodes.get_mut(&id(2)).unwrap().extra = vec![
1907 vec![Field {
1908 id: 0x08001cb4,
1909 value: FieldValue::NoData,
1910 }],
1911 vec![Field {
1912 id: 0x88001cb4,
1913 value: FieldValue::NoData,
1914 }],
1915 ];
1916 let page = Page::from_revision(&source, id(1)).unwrap();
1917 let PageObject::Title(title) = &page.objects[0] else {
1918 panic!()
1919 };
1920 assert!(!title.outlines[0].title);
1921 assert_eq!(title.outlines[0].min_width, None);
1922 }
1923
1924 #[test]
1925 fn preserves_paint_order_nested_parents_and_owned_image_payloads() {
1926 let bytes = vec![1, 2, 3, 4];
1927 let mut source = revision();
1928 let image = element(Kind::Image {
1929 container: Some(id(8)),
1930 filename: None,
1931 alt: Some("Image".into()),
1932 picture_width: None,
1933 picture_height: None,
1934 background: Some(true),
1935 printout: None,
1936 link: None,
1937 });
1938 let file = element(Kind::File {
1939 reference: crate::FileDataReference::Internal([0; 16]),
1940 extension: "png".into(),
1941 payload: Some(bytes.as_slice()),
1942 });
1943 let mut title = element(Kind::Title);
1944 title.layout.x = Some(12.0);
1945 title.layout.y = Some(24.0);
1946 title.children.push(id(10));
1947 let title_outline = element(Kind::Outline {
1948 indents: Vec::new(),
1949 });
1950 let mut group = element(Kind::OutlineGroup);
1951 group.children.push(id(12));
1952 group.child_level = Some(1);
1953 let mut child = element(Kind::Paragraph {
1954 lists: Vec::new(),
1955 paragraph_style: None,
1956 collapse_state: None,
1957 });
1958 child.content.push(id(13));
1959 source.nodes.insert(
1960 id(13),
1961 element(Kind::RichText {
1962 text: "".into(),
1963 runs: Vec::new(),
1964 paragraph_style: None,
1965 boilerplate: false,
1966 }),
1967 );
1968 source.nodes.extend([
1969 (id(7), image),
1970 (id(8), file),
1971 (id(9), title),
1972 (id(10), title_outline),
1973 (id(11), group),
1974 (id(12), child),
1975 ]);
1976 source
1977 .nodes
1978 .get_mut(&id(1))
1979 .unwrap()
1980 .children
1981 .insert(0, id(7));
1982 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(9));
1983 source.nodes.get_mut(&id(3)).unwrap().children.push(id(11));
1984 source.nodes.get_mut(&id(3)).unwrap().child_level = Some(1);
1985 let page = Page::from_revision(&source, id(1)).unwrap();
1986 drop(source);
1987 drop(bytes);
1988 assert_eq!(page.objects.len(), 3);
1989 let PageObject::Image(image) = &page.objects[0] else {
1990 panic!()
1991 };
1992 assert_eq!(image.bytes.as_deref(), Some([1, 2, 3, 4].as_slice()));
1993 let PageObject::Outline(outline) = &page.objects[1] else {
1994 panic!()
1995 };
1996 assert_eq!(outline.paragraphs[1].id, id(12));
1997 assert_eq!(outline.paragraphs[1].parent, Some(id(3)));
1998 assert_eq!(outline.paragraphs[1].level, 3);
1999 let PageObject::Title(title) = &page.objects[2] else {
2000 panic!()
2001 };
2002 assert_eq!(title.id, id(9));
2003 assert_eq!([title.layout.x, title.layout.y], [Some(12.0), Some(24.0)]);
2004 assert_eq!(title.outlines[0].id, id(10));
2005 assert_eq!(title.outlines[0].layout.x, None);
2006 }
2007
2008 #[test]
2009 fn rejects_title_children_that_are_not_outlines() {
2010 let mut source = revision();
2011 let mut title = element(Kind::Title);
2012 title.children.push(id(8));
2013 source.nodes.insert(id(7), title);
2014 source.nodes.insert(id(8), element(Kind::Title));
2015 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7));
2016 assert_eq!(
2017 Page::from_revision(&source, id(1)).err().unwrap().message,
2018 "Canvas title child is not an outline"
2019 );
2020 }
2021
2022 #[test]
2023 fn validates_empty_text_styles_missing_references_and_cycles() {
2024 let mut source = revision();
2025 source.nodes.get_mut(&id(4)).unwrap().kind = Kind::RichText {
2026 text: String::new(),
2027 runs: Vec::new(),
2028 paragraph_style: Some(id(5)),
2029 boilerplate: false,
2030 };
2031 source.nodes.get_mut(&id(3)).unwrap().format.font_size = Some(10.0);
2032 let page = Page::from_revision(&source, id(1)).unwrap();
2033 let PageObject::Outline(outline) = &page.objects[0] else {
2034 panic!()
2035 };
2036 assert_eq!(
2037 outline.paragraphs[0].text().unwrap().text.spans()[0]
2038 .format
2039 .font_size,
2040 Some(12.0)
2041 );
2042 source.nodes.get_mut(&id(3)).unwrap().children.push(id(2));
2043 assert!(Page::from_revision(&source, id(1)).is_err());
2044 source.nodes.get_mut(&id(3)).unwrap().children.clear();
2045 source.nodes.remove(&id(5));
2046 assert!(Page::from_revision(&source, id(1)).is_err());
2047 }
2048}