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 .as_chunks::<2>()
593 .0
594 .iter()
595 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
596 .collect();
597 String::from_utf16_lossy(&units)
598 .trim_end_matches('\0')
599 .to_owned()
600 })
601 .filter(|text| !text.trim().is_empty())
602 .map(|text| Recognized {
603 text,
604 stored: fields
605 .iter()
606 .filter_map(|field| match field.value {
607 FieldValue::Bytes(bytes)
608 if RECOGNIZED_PROPERTIES.contains(&field.id) =>
609 {
610 Some((field.id, bytes.to_vec()))
611 }
612 _ => None,
613 })
614 .collect(),
615 }),
616 })
617 }
618}
619
620#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
621pub struct Unsupported {
622 pub id: ExGuid,
623 pub jcid: u32,
624 pub layout: Layout,
625}
626
627impl Page {
628 pub fn from_document(document: &Document<'_>, title: &str) -> Result<Self, Error> {
629 let mut selected = None;
630 for (space, id) in document.pages()? {
631 let revision = document.active(space)?;
632 if page_title(revision, id)
633 .map(crate::edit::without_fields)
634 .as_deref()
635 == Some(title)
636 {
637 if selected.is_some() {
638 return Err(Error {
639 offset: 0,
640 message: "More than one active page has the requested title",
641 });
642 }
643 selected = Some((revision, id));
644 }
645 }
646 let (revision, id) = selected.ok_or(Error {
647 offset: 0,
648 message: "No active page has the requested title",
649 })?;
650 Self::from_revision(revision, id)
651 }
652
653 /// The page's content under fresh identities, for a copy into this or another section:
654 /// OneNote gives a copied page and every object on it new identities, and stored
655 /// links keep naming the original. Content outside the model has no copy, so an
656 /// `Unsupported` object is an error.
657 pub fn copy(&self) -> Result<Self, Error> {
658 self.copy_with(&mut [])
659 }
660
661 /// `copy`, renaming `ids`, identities on this page, to theirs on the copy.
662 pub fn copy_with(&self, ids: &mut [ExGuid]) -> Result<Self, Error> {
663 let invalid = |message| Error { offset: 0, message };
664 let mut value =
665 serde_json::to_value((self, &*ids)).map_err(|_| invalid("Page is not serializable"))?;
666 let mut fresh: BTreeMap<String, String> = BTreeMap::new();
667 fn walk(
668 value: &mut serde_json::Value,
669 fresh: &mut BTreeMap<String, String>,
670 ) -> Result<(), Error> {
671 match value {
672 serde_json::Value::String(text) if text.starts_with('{') => {
673 if text.parse::<ExGuid>().is_ok() {
674 if !fresh.contains_key(text) {
675 fresh.insert(text.clone(), text::new_id()?.to_string());
676 }
677 *text = fresh[text].clone();
678 }
679 }
680 serde_json::Value::Array(items) => {
681 for item in items {
682 walk(item, fresh)?;
683 }
684 }
685 serde_json::Value::Object(fields) => {
686 if fields.contains_key("Unsupported") {
687 return Err(Error {
688 offset: 0,
689 message: "Content outside the page model cannot be copied",
690 });
691 }
692 let keys: Vec<String> = fields.keys().cloned().collect();
693 for key in keys {
694 let mut item = fields.remove(&key).unwrap();
695 walk(&mut item, fresh)?;
696 let key = match key.parse::<ExGuid>() {
697 Ok(_) => {
698 if !fresh.contains_key(&key) {
699 fresh.insert(key.clone(), text::new_id()?.to_string());
700 }
701 fresh[&key].clone()
702 }
703 Err(_) => key,
704 };
705 fields.insert(key, item);
706 }
707 }
708 _ => {}
709 }
710 Ok(())
711 }
712 walk(&mut value, &mut fresh)?;
713 let (mut copy, renamed): (Self, Vec<ExGuid>) =
714 serde_json::from_value(value).map_err(|_| invalid("Page copy does not deserialize"))?;
715 ids.copy_from_slice(&renamed);
716 copy.identity = Some(crate::write::fresh_guid()?);
717 // A paragraph's indent level follows its parent chain; a level beyond that comes
718 // from an outline group, which the model does not hold.
719 fn levels(paragraphs: &[PageParagraph]) -> bool {
720 let by_id: BTreeMap<ExGuid, u32> = paragraphs.iter().map(|p| (p.id, p.level)).collect();
721 paragraphs.iter().all(|paragraph| {
722 let expected = match paragraph.parent {
723 Some(parent) => by_id.get(&parent).map_or(0, |level| level + 1),
724 None => 1,
725 };
726 paragraph.level == expected
727 && match &paragraph.content {
728 ParagraphContent::Table(table) => table
729 .rows
730 .iter()
731 .flat_map(|row| &row.cells)
732 .all(|cell| levels(&cell.paragraphs)),
733 _ => true,
734 }
735 })
736 }
737 let expressible = copy.objects.iter().all(|object| match object {
738 PageObject::Outline(outline) => levels(&outline.paragraphs),
739 _ => true,
740 });
741 if !expressible {
742 return Err(invalid("Outline groups cannot be copied"));
743 }
744 Ok(copy)
745 }
746
747 /// The date and time the title's date fields show, when it has both.
748 pub fn date_text(&self) -> Option<[String; 2]> {
749 let title = self.objects.iter().find_map(|object| match object {
750 PageObject::Title(title) => Some(title),
751 _ => None,
752 })?;
753 let outline = title
754 .outlines
755 .iter()
756 .find(|outline| Some(outline.id) == title.date)?;
757 let [first, second] = &outline.paragraphs[..] else {
758 return None;
759 };
760 let [first, second] = [first.text()?, second.text()?];
761 let text = |object: &TextObject| object.text.text().to_owned();
762 // OneNote writes the date first; a field marked as the other role says otherwise.
763 Some(
764 if first.date_field == Some(DateField::Time)
765 || second.date_field == Some(DateField::Date)
766 {
767 [text(second), text(first)]
768 } else {
769 [text(first), text(second)]
770 },
771 )
772 }
773
774 /// The notebook-management identity of the page in an active page revision, which
775 /// internal links name.
776 pub fn identity_of(revision: &Revision<'_>) -> Option<[u8; 16]> {
777 revision
778 .roots
779 .get(&2)
780 .and_then(|id| revision.nodes.get(id))
781 .filter(|node| matches!(node.kind, Kind::Metadata { .. }))
782 .and_then(|node| node.extra.first())
783 .and_then(|fields| fields.iter().find(|field| field.id == 0x1c001c30))
784 .and_then(|field| match field.value {
785 FieldValue::Bytes(bytes) => bytes.try_into().ok(),
786 _ => None,
787 })
788 }
789
790 /// The title and outline level (1 at the top) a section's page list shows, read from
791 /// the page's metadata without building its content.
792 pub fn heading(revision: &Revision<'_>, id: ExGuid) -> (String, u32) {
793 let level = revision
794 .roots
795 .get(&2)
796 .and_then(|id| revision.nodes.get(id))
797 .and_then(|node| match node.kind {
798 Kind::Metadata { level, .. } => level,
799 _ => None,
800 });
801 (
802 crate::edit::without_fields(page_title(revision, id).unwrap_or_default()),
803 level.unwrap_or(1),
804 )
805 }
806
807 /// The single active page declared in one page object space.
808 pub fn from_space(document: &Document<'_>, space: ExGuid) -> Result<Self, Error> {
809 let pages = document.pages_in(space)?;
810 let [id] = pages.as_slice() else {
811 return Err(Error {
812 offset: 0,
813 message: "Choose an object space containing one active page",
814 });
815 };
816 Self::from_revision(document.active(space)?, *id)
817 }
818
819 pub fn from_revision(revision: &Revision<'_>, id: ExGuid) -> Result<Self, Error> {
820 let invalid = |message| Error { offset: 0, message };
821 let root = revision
822 .nodes
823 .get(&id)
824 .ok_or_else(|| invalid("Missing canvas page"))?;
825 let Kind::Page {
826 margin_origin_x,
827 margin_origin_y,
828 rtl,
829 ..
830 } = &root.kind
831 else {
832 return Err(invalid("Canvas root is not a page"));
833 };
834 let metadata = revision
835 .roots
836 .get(&2)
837 .and_then(|id| revision.nodes.get(id))
838 .filter(|node| matches!(node.kind, Kind::Metadata { .. }))
839 .and_then(|node| node.extra.first());
840 let mut page = Self {
841 title: crate::edit::without_fields(page_title(revision, id).unwrap_or_default()),
842 identity: Self::identity_of(revision),
843 created: metadata
844 .and_then(|fields| fields.iter().find(|field| field.id == 0x18001c65))
845 .map(|field| {
846 let FieldValue::Bytes(bytes) = field.value else {
847 return Err(invalid("The page creation date is damaged"));
848 };
849 Ok(u64::from_le_bytes(bytes.try_into().map_err(|_| {
850 invalid("The page creation date is damaged")
851 })?))
852 })
853 .transpose()?,
854 // OneNote 2010 snaps to, and on the first move writes, (36, 14.4) when unset: 1.0
855 // and 0.4 half inches, read as stored ones are.
856 margin_origin: [
857 margin_origin_x.unwrap_or(1.0_f32 * 36.0),
858 margin_origin_y.unwrap_or(0.4_f32 * 36.0),
859 ],
860 rtl: rtl.unwrap_or(false),
861 color: root
862 .extra
863 .first()
864 .and_then(|fields| fields.iter().find(|field| field.id == PAGE_COLOR))
865 .map(|field| match field.value {
866 FieldValue::Bytes(&[red, green, blue, alpha]) => {
867 Ok(u32::from_le_bytes([red, green, blue, alpha]))
868 }
869 _ => Err(invalid("The page color is damaged")),
870 })
871 .transpose()?,
872 rule_lines: root
873 .extra
874 .first()
875 .and_then(|fields| RuleLines::read(fields)),
876 objects: Vec::new(),
877 definitions: BTreeMap::new(),
878 };
879 let mut roots: Vec<_> = root
880 .children
881 .iter()
882 .chain(&root.structure)
883 .rev()
884 .map(|id| (*id, None))
885 .collect();
886 let mut seen = BTreeSet::new();
887 while let Some((id, title_index)) = roots.pop() {
888 if !seen.insert(id) {
889 return Err(invalid("Repeated canvas page object"));
890 }
891 let node = revision
892 .nodes
893 .get(&id)
894 .ok_or_else(|| invalid("Missing canvas page object"))?;
895 if title_index.is_some() && !matches!(node.kind, Kind::Outline { .. }) {
896 return Err(invalid("Canvas title child is not an outline"));
897 }
898 match &node.kind {
899 Kind::Title => {
900 let index = page.objects.len();
901 page.objects.push(PageObject::Title(Title {
902 id,
903 date: None,
904 layout: node.layout.clone(),
905 outlines: Vec::new(),
906 }));
907 roots.extend(node.children.iter().rev().map(|id| (*id, Some(index))));
908 }
909 Kind::Outline { indents } => {
910 let fields = node.extra.first().map(Vec::as_slice).unwrap_or_default();
911 if let Some(index) = title_index
912 && fields.iter().any(|field| {
913 field.id == 0x88001cb5 && matches!(field.value, FieldValue::NoData)
914 })
915 {
916 let PageObject::Title(title) = &mut page.objects[index] else {
917 unreachable!()
918 };
919 if title.date.replace(id).is_some() {
920 return Err(invalid("The page has more than one date field"));
921 }
922 }
923 let min_width = fields
924 .iter()
925 .find(|field| field.id == 0x14001cec)
926 .map(|field| {
927 let FieldValue::Bytes(bytes) = field.value else {
928 return Err(invalid("Invalid canvas minimum outline width"));
929 };
930 let width =
931 f32::from_le_bytes(bytes.try_into().map_err(|_| {
932 invalid("Invalid canvas minimum outline width")
933 })?) * 36.0;
934 if !width.is_finite() || width <= 0.0 {
935 return Err(invalid("Invalid canvas minimum outline width"));
936 }
937 Ok(width)
938 })
939 .transpose()?;
940 let title_text = |node: &Element<'_>| {
941 node.extra.first().is_some_and(|fields| {
942 fields.iter().any(|field| {
943 field.id == 0x88001cb4 && matches!(field.value, FieldValue::NoData)
944 })
945 })
946 };
947 let mut outline = Outline {
948 id,
949 // OneNote flags the outline and its paragraph, and still shows an outline
950 // whose paragraph alone is flagged as the title.
951 title: title_text(node)
952 || node
953 .children
954 .iter()
955 .any(|id| revision.nodes.get(id).is_some_and(title_text)),
956 min_width,
957 layout: node.layout.clone(),
958 indents: indents.clone(),
959 paragraphs: Vec::new(),
960 unsupported: Vec::new(),
961 };
962 (outline.paragraphs, outline.unsupported) = read_paragraphs(
963 revision,
964 node,
965 &node.format,
966 &mut page.definitions,
967 &mut seen,
968 0,
969 )?;
970 if let Some(index) = title_index {
971 let PageObject::Title(title) = &mut page.objects[index] else {
972 unreachable!()
973 };
974 title.outlines.push(outline);
975 } else {
976 page.objects.push(PageObject::Outline(outline));
977 }
978 }
979 Kind::Image { .. } | Kind::Attachment { .. } => {
980 for tag in node.tags.iter().filter_map(|tag| tag.definition) {
981 define(revision, tag, false, &mut page.definitions)?;
982 }
983 page.objects.push(match node.kind {
984 Kind::Image { .. } => PageObject::Image(Image::read(revision, id, node)?),
985 _ => PageObject::Attachment(Attachment::read(revision, id, node)?),
986 });
987 }
988 Kind::Ink { .. } => {
989 page.objects
990 .push(PageObject::Ink(Ink::read(revision, id, node)?));
991 }
992 _ => page.objects.push(PageObject::Unsupported(Unsupported {
993 id,
994 jcid: node.jcid,
995 layout: node.layout.clone(),
996 })),
997 }
998 }
999 Ok(page)
1000 }
1001}
1002
1003/// The format `container` passes to its children, as reading its page inherits it.
1004fn inherited_by(
1005 revision: &Revision<'_>,
1006 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
1007 container: ExGuid,
1008) -> Result<Format, Error> {
1009 let invalid = |message| Error { offset: 0, message };
1010 let node = revision
1011 .nodes
1012 .get(&container)
1013 .ok_or_else(|| invalid("Missing canvas outline object"))?;
1014 let parent = || -> Result<ExGuid, Error> {
1015 match parents.get(&container).map(Vec::as_slice) {
1016 Some([parent]) => Ok(*parent),
1017 _ => Err(invalid("Page content must have one parent")),
1018 }
1019 };
1020 Ok(match &node.kind {
1021 Kind::Outline { .. } => node.format.clone(),
1022 Kind::OutlineGroup => node
1023 .format
1024 .inherit(&inherited_by(revision, parents, parent()?)?),
1025 Kind::Paragraph {
1026 paragraph_style, ..
1027 } => {
1028 let style = match paragraph_style {
1029 Some(id) => revision
1030 .nodes
1031 .get(id)
1032 .map(|n| n.format.clone())
1033 .unwrap_or_default(),
1034 None => Format::default(),
1035 };
1036 node.format
1037 .inherit(&style)
1038 .inherit(&inherited_by(revision, parents, parent()?)?)
1039 }
1040 Kind::Cell { .. } => {
1041 let row = parent()?;
1042 let [table] = parents.get(&row).map(Vec::as_slice).unwrap_or_default() else {
1043 return Err(invalid("A table row must have one table"));
1044 };
1045 let [holder] = parents.get(table).map(Vec::as_slice).unwrap_or_default() else {
1046 return Err(invalid("A table must have one paragraph"));
1047 };
1048 node.format.inherit(
1049 &revision.nodes[table]
1050 .format
1051 .inherit(&inherited_by(revision, parents, *holder)?),
1052 )
1053 }
1054 _ => Format::default(),
1055 })
1056}
1057
1058/// The text of rich-text object `text` as its page model shows it, read from its ancestors
1059/// alone.
1060pub(crate) fn text_of(
1061 revision: &Revision<'_>,
1062 parents: &BTreeMap<ExGuid, Vec<ExGuid>>,
1063 text: ExGuid,
1064) -> Result<Paragraph, Error> {
1065 let invalid = |message| Error { offset: 0, message };
1066 let [paragraph] = parents.get(&text).map(Vec::as_slice).unwrap_or_default() else {
1067 return Err(invalid("Select text belonging to one paragraph"));
1068 };
1069 let format = inherited_by(revision, parents, *paragraph)?;
1070 let node = revision
1071 .nodes
1072 .get(&text)
1073 .ok_or_else(|| invalid("Missing text object"))?;
1074 let Kind::RichText {
1075 paragraph_style, ..
1076 } = &node.kind
1077 else {
1078 return Err(invalid("Select a rich-text object"));
1079 };
1080 let runs = revision.text_runs(text)?;
1081 Ok(if runs.is_empty() {
1082 let style = match paragraph_style {
1083 Some(id) => revision
1084 .nodes
1085 .get(id)
1086 .map(|n| n.format.clone())
1087 .unwrap_or_default(),
1088 None => Format::default(),
1089 };
1090 Paragraph::new(String::new(), node.format.inherit(&style).inherit(&format))
1091 } else {
1092 Paragraph::from_runs(
1093 runs.into_iter()
1094 .map(|run| (run.text.to_owned(), run.format.inherit(&format))),
1095 )
1096 })
1097}
1098
1099/// Adds list or tag definition `id` to `definitions`.
1100fn define(
1101 revision: &Revision<'_>,
1102 id: ExGuid,
1103 is_list: bool,
1104 definitions: &mut BTreeMap<ExGuid, Definition>,
1105) -> Result<(), Error> {
1106 let invalid = |message| Error { offset: 0, message };
1107 let definition = revision
1108 .nodes
1109 .get(&id)
1110 .ok_or_else(|| invalid("Missing canvas list or tag definition"))?;
1111 let kind = match &definition.kind {
1112 Kind::List {
1113 font,
1114 format,
1115 restart,
1116 bullet,
1117 } if is_list => Kind::List {
1118 font: font.clone(),
1119 format: format.clone(),
1120 restart: *restart,
1121 bullet: *bullet,
1122 },
1123 Kind::TagDefinition {
1124 label,
1125 action_type,
1126 shape,
1127 color,
1128 highlight,
1129 } if !is_list => Kind::TagDefinition {
1130 label: label.clone(),
1131 action_type: *action_type,
1132 shape: *shape,
1133 color: *color,
1134 highlight: *highlight,
1135 },
1136 _ => return Err(invalid("Canvas list or tag definition has the wrong type")),
1137 };
1138 definitions.insert(
1139 id,
1140 Definition {
1141 kind,
1142 format: definition.format.clone(),
1143 },
1144 );
1145 Ok(())
1146}
1147
1148fn read_paragraphs(
1149 revision: &Revision<'_>,
1150 container: &Element<'_>,
1151 inherited: &Format,
1152 definitions: &mut BTreeMap<ExGuid, Definition>,
1153 seen: &mut BTreeSet<ExGuid>,
1154 depth: usize,
1155) -> Result<(Vec<PageParagraph>, Vec<Unsupported>), Error> {
1156 let invalid = |message| Error { offset: 0, message };
1157 let default_format = Format::default();
1158 let style = |id: Option<ExGuid>| -> Result<&Format, Error> {
1159 let Some(id) = id else {
1160 return Ok(&default_format);
1161 };
1162 let node = revision
1163 .nodes
1164 .get(&id)
1165 .ok_or_else(|| invalid("Missing canvas paragraph style"))?;
1166 if !matches!(node.kind, Kind::Style { .. }) {
1167 return Err(invalid("Canvas paragraph style has the wrong type"));
1168 }
1169 Ok(&node.format)
1170 };
1171
1172 let mut paragraphs = Vec::new();
1173 let mut unsupported = Vec::new();
1174 let mut pending: Vec<_> = container
1175 .children
1176 .iter()
1177 .rev()
1178 .map(|id| {
1179 (
1180 *id,
1181 None,
1182 u32::from(container.child_level.unwrap_or(0)),
1183 inherited.clone(),
1184 )
1185 })
1186 .collect();
1187 while let Some((id, parent, level, inherited)) = pending.pop() {
1188 if !seen.insert(id) {
1189 return Err(invalid("Repeated canvas outline object"));
1190 }
1191 let node = revision
1192 .nodes
1193 .get(&id)
1194 .ok_or_else(|| invalid("Missing canvas outline object"))?;
1195 let next_level = level
1196 .checked_add(u32::from(node.child_level.unwrap_or(0)))
1197 .ok_or_else(|| invalid("Canvas outline level overflow"))?;
1198 match &node.kind {
1199 Kind::Paragraph {
1200 lists,
1201 paragraph_style,
1202 collapse_state,
1203 } => {
1204 let format = node
1205 .format
1206 .inherit(style(*paragraph_style)?)
1207 .inherit(&inherited);
1208 let mut base_style = *paragraph_style;
1209 let [content_id] = node.content.as_slice() else {
1210 return Err(invalid("A paragraph must contain one content object"));
1211 };
1212 if !seen.insert(*content_id) {
1213 return Err(invalid("Repeated canvas paragraph content"));
1214 }
1215 let content = revision
1216 .nodes
1217 .get(content_id)
1218 .ok_or_else(|| invalid("Missing canvas paragraph content"))?;
1219 let media = MediaIndex {
1220 recordings: content.media_ids.clone(),
1221 time_ms: content.media_time_ms,
1222 };
1223 let content = if let Kind::RichText {
1224 paragraph_style, ..
1225 } = &content.kind
1226 {
1227 base_style = paragraph_style.or(base_style);
1228 let runs = revision.text_runs(*content_id)?;
1229 let text_content = if runs.is_empty() {
1230 Paragraph::new(
1231 String::new(),
1232 content
1233 .format
1234 .inherit(style(*paragraph_style)?)
1235 .inherit(&format),
1236 )
1237 } else {
1238 Paragraph::from_runs(
1239 runs.into_iter()
1240 .map(|run| (run.text.to_owned(), run.format.inherit(&format))),
1241 )
1242 };
1243 ParagraphContent::Text(TextObject {
1244 id: *content_id,
1245 date_field: content
1246 .extra
1247 .first()
1248 .map(Vec::as_slice)
1249 .unwrap_or_default()
1250 .iter()
1251 .filter(|field| matches!(field.value, FieldValue::NoData))
1252 .filter_map(|field| match field.id {
1253 0x88001cb5 => Some(DateField::Date),
1254 0x88001c87 => Some(DateField::Time),
1255 _ => None,
1256 })
1257 .try_fold(None, |previous, field| {
1258 if previous.is_some() {
1259 Err(invalid("The page date field has conflicting roles"))
1260 } else {
1261 Ok(Some(field))
1262 }
1263 })?,
1264 text: text_content,
1265 tags: content.tags.clone(),
1266 })
1267 } else if matches!(content.kind, Kind::Image { .. }) {
1268 ParagraphContent::Image(Image::read(revision, *content_id, content)?)
1269 } else if matches!(content.kind, Kind::Attachment { .. }) {
1270 ParagraphContent::Attachment(Attachment::read(revision, *content_id, content)?)
1271 } else if matches!(content.kind, Kind::Ink { .. }) {
1272 ParagraphContent::Ink(Ink::read(revision, *content_id, content)?)
1273 } else if matches!(content.kind, Kind::Table { .. }) {
1274 ParagraphContent::Table(read_table(
1275 revision,
1276 *content_id,
1277 &content.format.inherit(&format),
1278 definitions,
1279 seen,
1280 depth + 1,
1281 )?)
1282 } else {
1283 ParagraphContent::Unsupported(Unsupported {
1284 id: *content_id,
1285 jcid: content.jcid,
1286 layout: content.layout.clone(),
1287 })
1288 };
1289 for (id, is_list) in lists.iter().map(|id| (id, true)).chain(
1290 node.tags
1291 .iter()
1292 .chain(match &content {
1293 ParagraphContent::Text(text) => text.tags.as_slice(),
1294 ParagraphContent::Table(table) => table.tags.as_slice(),
1295 ParagraphContent::Image(image) => image.tags.as_slice(),
1296 ParagraphContent::Attachment(file) => file.tags.as_slice(),
1297 ParagraphContent::Ink(_) | ParagraphContent::Unsupported(_) => &[],
1298 })
1299 .filter_map(|tag| tag.definition.as_ref())
1300 .map(|id| (id, false)),
1301 ) {
1302 define(revision, *id, is_list, definitions)?;
1303 }
1304 if let Some(id) = base_style {
1305 let definition = revision
1306 .nodes
1307 .get(&id)
1308 .ok_or_else(|| invalid("Missing canvas paragraph style"))?;
1309 let Kind::Style { name, next } = &definition.kind else {
1310 return Err(invalid("Canvas paragraph style has the wrong type"));
1311 };
1312 definitions.entry(id).or_insert_with(|| Definition {
1313 kind: Kind::Style {
1314 name: name.clone(),
1315 next: next.clone(),
1316 },
1317 format: definition.format.clone(),
1318 });
1319 }
1320 paragraphs.push(PageParagraph {
1321 id,
1322 parent,
1323 level,
1324 style: base_style,
1325 format: format.clone(),
1326 content,
1327 lists: lists.clone(),
1328 tags: node.tags.clone(),
1329 media,
1330 collapsed: *collapse_state == Some(1),
1331 });
1332 pending.extend(
1333 node.children
1334 .iter()
1335 .rev()
1336 .map(|child| (*child, Some(id), next_level, format.clone())),
1337 );
1338 }
1339 Kind::OutlineGroup => pending.extend(
1340 node.children
1341 .iter()
1342 .rev()
1343 .map(|child| (*child, parent, next_level, node.format.inherit(&inherited))),
1344 ),
1345 _ => unsupported.push(Unsupported {
1346 id,
1347 jcid: node.jcid,
1348 layout: node.layout.clone(),
1349 }),
1350 }
1351 }
1352 Ok((paragraphs, unsupported))
1353}
1354
1355fn read_table(
1356 revision: &Revision<'_>,
1357 id: ExGuid,
1358 inherited: &Format,
1359 definitions: &mut BTreeMap<ExGuid, Definition>,
1360 seen: &mut BTreeSet<ExGuid>,
1361 depth: usize,
1362) -> Result<Table, Error> {
1363 let invalid = |message| Error { offset: 0, message };
1364 if depth > 64 {
1365 return Err(invalid("Tables are nested too deeply"));
1366 }
1367 let node = &revision.nodes[&id];
1368 let Kind::Table {
1369 rows,
1370 columns,
1371 widths,
1372 locked,
1373 borders,
1374 } = &node.kind
1375 else {
1376 unreachable!()
1377 };
1378 if rows.map(|n| n as usize) != Some(node.children.len())
1379 || node.children.is_empty()
1380 || columns.map(|n| n as usize) != Some(widths.len())
1381 || widths.is_empty()
1382 || (!locked.is_empty() && locked.len() != widths.len())
1383 || widths.iter().any(|w| !w.is_finite() || *w < 36.0)
1384 {
1385 return Err(invalid("Table dimensions are inconsistent"));
1386 }
1387 let mut table_rows = Vec::new();
1388 for row_id in &node.children {
1389 if !seen.insert(*row_id) {
1390 return Err(invalid("Repeated canvas table row"));
1391 }
1392 let row = revision
1393 .nodes
1394 .get(row_id)
1395 .ok_or_else(|| invalid("Missing canvas table row"))?;
1396 if !matches!(row.kind, Kind::Row) || row.children.len() != widths.len() {
1397 return Err(invalid("Table row dimensions are inconsistent"));
1398 }
1399 let mut cells = Vec::new();
1400 for cell_id in &row.children {
1401 if !seen.insert(*cell_id) {
1402 return Err(invalid("Repeated canvas table cell"));
1403 }
1404 let cell = revision
1405 .nodes
1406 .get(cell_id)
1407 .ok_or_else(|| invalid("Missing canvas table cell"))?;
1408 if cell.children.is_empty() {
1409 return Err(invalid("A table cell has no paragraphs"));
1410 }
1411 let Kind::Cell { shading, indents } = &cell.kind else {
1412 return Err(invalid("A table row contains an invalid cell"));
1413 };
1414 let (paragraphs, unsupported) = read_paragraphs(
1415 revision,
1416 cell,
1417 &cell.format.inherit(inherited),
1418 definitions,
1419 seen,
1420 depth,
1421 )?;
1422 cells.push(TableCell {
1423 id: *cell_id,
1424 layout: cell.layout.clone(),
1425 indents: indents.clone(),
1426 shading: *shading,
1427 paragraphs,
1428 unsupported,
1429 });
1430 }
1431 table_rows.push(TableRow { id: *row_id, cells });
1432 }
1433 Ok(Table {
1434 id,
1435 columns: widths
1436 .iter()
1437 .enumerate()
1438 .map(|(index, &width)| TableColumn {
1439 width,
1440 locked: locked.get(index).copied().unwrap_or(false),
1441 })
1442 .collect(),
1443 rows: table_rows,
1444 borders: *borders,
1445 layout: node.layout.clone(),
1446 tags: node.tags.clone(),
1447 })
1448}
1449
1450fn page_title<'a>(revision: &'a Revision<'_>, id: ExGuid) -> Option<&'a str> {
1451 revision
1452 .roots
1453 .get(&2)
1454 .and_then(|id| revision.nodes.get(id))
1455 .and_then(|node| {
1456 if let Kind::Metadata { title, .. } = &node.kind {
1457 title.as_deref()
1458 } else {
1459 None
1460 }
1461 })
1462 .or_else(|| {
1463 if let Kind::Page {
1464 alternate_title, ..
1465 } = &revision.nodes.get(&id)?.kind
1466 {
1467 alternate_title.as_deref()
1468 } else {
1469 None
1470 }
1471 })
1472}
1473
1474impl PageParagraph {
1475 pub fn text(&self) -> Option<&TextObject> {
1476 match &self.content {
1477 ParagraphContent::Text(text) => Some(text),
1478 _ => None,
1479 }
1480 }
1481 pub fn text_mut(&mut self) -> Option<&mut TextObject> {
1482 match &mut self.content {
1483 ParagraphContent::Text(text) => Some(text),
1484 _ => None,
1485 }
1486 }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use super::*;
1492 use crate::document::{Element, Field, FieldValue, TextRun};
1493
1494 fn id(n: u32) -> ExGuid {
1495 ExGuid {
1496 n,
1497 ..ExGuid::default()
1498 }
1499 }
1500
1501 const IDENTITY: [u8; 16] = [7; 16];
1502
1503 fn element(kind: Kind<'_>) -> Element<'_> {
1504 Element {
1505 jcid: 0,
1506 children: Vec::new(),
1507 content: Vec::new(),
1508 structure: Vec::new(),
1509 spaces: Vec::new(),
1510 child_level: None,
1511 layout: Layout::default(),
1512 format: Format::default(),
1513 created: None,
1514 modified: None,
1515 original_author: None,
1516 latest_author: None,
1517 media_ids: Vec::new(),
1518 media_time_ms: None,
1519 tags: Vec::new(),
1520 kind,
1521 extra: Vec::new(),
1522 }
1523 }
1524
1525 fn revision() -> Revision<'static> {
1526 let mut page = element(Kind::Page {
1527 alternate_title: Some("Fallback".into()),
1528 level: None,
1529 width: None,
1530 height: None,
1531 margin_origin_x: Some(36.0),
1532 margin_origin_y: Some(12.0),
1533 rtl: None,
1534 recordings: Vec::new(),
1535 });
1536 page.children.push(id(2));
1537 let mut outline = element(Kind::Outline {
1538 indents: vec![18.0, 0.0, 36.0],
1539 });
1540 outline.children.push(id(3));
1541 outline.child_level = Some(1);
1542 outline.format.font_size = Some(9.0);
1543 outline.format.bold = Some(true);
1544 let mut paragraph = element(Kind::Paragraph {
1545 lists: Vec::new(),
1546 paragraph_style: Some(id(5)),
1547 collapse_state: Some(1),
1548 });
1549 paragraph.content.push(id(4));
1550 paragraph.format.bold = Some(false);
1551 let text = element(Kind::RichText {
1552 text: "ab".into(),
1553 runs: vec![TextRun {
1554 start: 0,
1555 end: 2,
1556 format: None,
1557 extra_set: None,
1558 }],
1559 paragraph_style: None,
1560 boilerplate: false,
1561 });
1562 let mut style = element(Kind::Style {
1563 name: Some("Body".into()),
1564 next: None,
1565 });
1566 style.format.font_size = Some(12.0);
1567 let mut metadata = element(Kind::Metadata {
1568 title: Some("Page title".into()),
1569 level: None,
1570 });
1571 metadata.extra = vec![vec![Field {
1572 id: 0x1c001c30,
1573 value: FieldValue::Bytes(&IDENTITY),
1574 }]];
1575 Revision {
1576 roots: BTreeMap::from([(2, id(6))]),
1577 nodes: BTreeMap::from([
1578 (id(1), page),
1579 (id(2), outline),
1580 (id(3), paragraph),
1581 (id(4), text),
1582 (id(5), style),
1583 (id(6), metadata),
1584 ]),
1585 }
1586 }
1587
1588 fn table_revision() -> Revision<'static> {
1589 let mut source = revision();
1590 source.nodes.get_mut(&id(3)).unwrap().content = vec![id(7)];
1591 let mut table = element(Kind::Table {
1592 rows: Some(1),
1593 columns: Some(2),
1594 widths: vec![37.11, 99.0],
1595 locked: vec![false, true],
1596 borders: Some(false),
1597 });
1598 table.children = vec![id(8)];
1599 let mut row = element(Kind::Row);
1600 row.children = vec![id(9), id(10)];
1601 source.nodes.insert(id(7), table);
1602 source.nodes.insert(id(8), row);
1603 for (cell, paragraph, text) in [(9, 11, 4), (10, 12, 13)] {
1604 let mut node = element(Kind::Cell {
1605 shading: Some(0x00ffff),
1606 indents: vec![18.0, 0.0, 27.0, 27.0],
1607 });
1608 node.child_level = Some(1);
1609 node.layout.max_width = Some(268.8);
1610 node.children = vec![id(paragraph)];
1611 if cell == 10 {
1612 node.format.font_size = Some(13.0);
1613 }
1614 source.nodes.insert(id(cell), node);
1615 let mut node = element(Kind::Paragraph {
1616 lists: Vec::new(),
1617 paragraph_style: None,
1618 collapse_state: None,
1619 });
1620 node.content = vec![id(text)];
1621 source.nodes.insert(id(paragraph), node);
1622 }
1623 source.nodes.insert(
1624 id(13),
1625 element(Kind::RichText {
1626 text: "cd".into(),
1627 runs: vec![TextRun {
1628 start: 0,
1629 end: 2,
1630 format: None,
1631 extra_set: None,
1632 }],
1633 paragraph_style: None,
1634 boilerplate: false,
1635 }),
1636 );
1637 source
1638 }
1639
1640 #[test]
1641 fn table_import_owns_cells_and_inherits_format_across_containers() {
1642 let page = {
1643 let source = table_revision();
1644 let before = serde_json::to_vec(&source).unwrap();
1645 let page = Page::from_revision(&source, id(1)).unwrap();
1646 assert_eq!(serde_json::to_vec(&source).unwrap(), before);
1647 page
1648 };
1649 let PageObject::Outline(outline) = &page.objects[0] else {
1650 panic!()
1651 };
1652 let ParagraphContent::Table(table) = &outline.paragraphs[0].content else {
1653 panic!()
1654 };
1655 assert_eq!(table.id, id(7));
1656 assert_eq!(table.borders, Some(false));
1657 assert_eq!(
1658 table.columns,
1659 [
1660 TableColumn {
1661 width: 37.11,
1662 locked: false
1663 },
1664 TableColumn {
1665 width: 99.0,
1666 locked: true
1667 }
1668 ]
1669 );
1670 assert_eq!(table.rows[0].id, id(8));
1671 for (index, cell) in table.rows[0].cells.iter().enumerate() {
1672 assert_eq!(cell.id, id(9 + index as u32));
1673 assert_eq!(cell.layout.max_width, Some(268.8));
1674 assert_eq!(cell.indents, [18.0, 0.0, 27.0, 27.0]);
1675 assert_eq!(cell.shading, Some(0x00ffff));
1676 let paragraph = &cell.paragraphs[0];
1677 assert_eq!(paragraph.parent, None);
1678 assert_eq!(paragraph.level, 1);
1679 assert_eq!(paragraph.id, id(11 + index as u32));
1680 let text = paragraph.text().unwrap();
1681 assert_eq!(text.id, if index == 0 { id(4) } else { id(13) });
1682 assert_eq!(text.text.text(), if index == 0 { "ab" } else { "cd" });
1683 assert_eq!(
1684 text.text.spans()[0].format.font_size,
1685 Some(if index == 0 { 12.0 } else { 13.0 })
1686 );
1687 assert_eq!(text.text.spans()[0].format.bold, Some(false));
1688 }
1689 }
1690
1691 #[test]
1692 fn absent_column_locks_use_the_specified_unlocked_default() {
1693 let mut source = table_revision();
1694 let Kind::Table { locked, .. } = &mut source.nodes.get_mut(&id(7)).unwrap().kind else {
1695 panic!()
1696 };
1697 locked.clear();
1698 let page = Page::from_revision(&source, id(1)).unwrap();
1699 let PageObject::Outline(outline) = &page.objects[0] else {
1700 panic!()
1701 };
1702 let ParagraphContent::Table(table) = &outline.paragraphs[0].content else {
1703 panic!()
1704 };
1705 assert!(table.columns.iter().all(|column| !column.locked));
1706 }
1707
1708 #[test]
1709 fn table_import_rejects_aliases_cycles_and_inconsistent_dimensions() {
1710 for case in 0..7 {
1711 let mut source = table_revision();
1712 match case {
1713 0 => source.nodes.get_mut(&id(8)).unwrap().children[1] = id(9),
1714 1 => source.nodes.get_mut(&id(11)).unwrap().content = vec![id(7)],
1715 2 => source.nodes.get_mut(&id(12)).unwrap().content = vec![id(4)],
1716 3 => source.nodes.get_mut(&id(3)).unwrap().content.push(id(4)),
1717 4 => source.nodes.get_mut(&id(9)).unwrap().children.clear(),
1718 5 => {
1719 let Kind::Table { columns, .. } =
1720 &mut source.nodes.get_mut(&id(7)).unwrap().kind
1721 else {
1722 panic!()
1723 };
1724 *columns = Some(3);
1725 }
1726 _ => {
1727 let Kind::Table { widths, .. } =
1728 &mut source.nodes.get_mut(&id(7)).unwrap().kind
1729 else {
1730 panic!()
1731 };
1732 widths[0] = f32::INFINITY;
1733 }
1734 }
1735 assert!(Page::from_revision(&source, id(1)).is_err(), "case {case}");
1736 }
1737 }
1738
1739 #[test]
1740 fn nested_tables_import_with_a_bounded_depth() {
1741 for levels in [1, 62, 65] {
1742 let mut source = table_revision();
1743 let mut paragraph = id(11);
1744 for depth in 0..levels {
1745 let base = 20 + depth * 4;
1746 source.nodes.get_mut(&paragraph).unwrap().content = vec![id(base)];
1747 let mut table = element(Kind::Table {
1748 rows: Some(1),
1749 columns: Some(1),
1750 widths: vec![72.0],
1751 locked: vec![true],
1752 borders: Some(true),
1753 });
1754 table.children = vec![id(base + 1)];
1755 let mut row = element(Kind::Row);
1756 row.children = vec![id(base + 2)];
1757 let mut cell = element(Kind::Cell {
1758 shading: None,
1759 indents: vec![18.0, 0.0, 27.0, 27.0],
1760 });
1761 cell.child_level = Some(1);
1762 cell.children = vec![id(base + 3)];
1763 let mut child = element(Kind::Paragraph {
1764 lists: Vec::new(),
1765 paragraph_style: None,
1766 collapse_state: None,
1767 });
1768 child.content = vec![id(4)];
1769 source.nodes.extend([
1770 (id(base), table),
1771 (id(base + 1), row),
1772 (id(base + 2), cell),
1773 (id(base + 3), child),
1774 ]);
1775 paragraph = id(base + 3);
1776 }
1777 assert_eq!(Page::from_revision(&source, id(1)).is_ok(), levels < 64);
1778 }
1779 }
1780
1781 #[test]
1782 fn owns_page_content_and_resolves_style_before_ancestor_defaults() {
1783 let page = {
1784 let mut source = revision();
1785 source.nodes.get_mut(&id(4)).unwrap().tags.push(Tag {
1786 definition: Some(id(7)),
1787 action_type: None,
1788 shape: None,
1789 property_status: None,
1790 status: 0,
1791 created: Some(123),
1792 completed: None,
1793 start: None,
1794 due: None,
1795 task_id: None,
1796 extra_set: 0,
1797 });
1798 source.nodes.insert(
1799 id(7),
1800 element(Kind::TagDefinition {
1801 label: Some("To Do".into()),
1802 action_type: Some(0),
1803 shape: Some(3),
1804 color: None,
1805 highlight: None,
1806 }),
1807 );
1808 let before = serde_json::to_vec(&source).unwrap();
1809 let page = Page::from_revision(&source, id(1)).unwrap();
1810 assert_eq!(before, serde_json::to_vec(&source).unwrap());
1811 page
1812 };
1813 assert_eq!(page.title, "Page title");
1814 assert_eq!(page.margin_origin, [36.0, 12.0]);
1815 let PageObject::Outline(outline) = &page.objects[0] else {
1816 panic!()
1817 };
1818 assert_eq!(outline.paragraphs.len(), 1);
1819 let paragraph = &outline.paragraphs[0];
1820 assert!(paragraph.collapsed);
1821 assert_eq!(paragraph.level, 1);
1822 assert_eq!(paragraph.parent, None);
1823 assert_eq!(paragraph.style, Some(id(5)));
1824 assert!(matches!(&page.definitions[&id(5)].kind,
1825 Kind::Style { name: Some(name), .. } if name == "Body"));
1826 assert_eq!(page.definitions[&id(5)].format.font_size, Some(12.0));
1827 let text = &paragraph.text().unwrap().text;
1828 assert_eq!(text.text(), "ab");
1829 assert_eq!(text.spans()[0].format.font_size, Some(12.0));
1830 assert_eq!(text.spans()[0].format.bold, Some(false));
1831 assert_eq!(paragraph.text().unwrap().tags[0].created, Some(123));
1832 assert!(matches!(&page.definitions[&id(7)].kind,
1833 Kind::TagDefinition { label: Some(label), shape: Some(3), .. } if label == "To Do"));
1834 }
1835
1836 #[test]
1837 fn creation_time_comes_from_page_metadata_and_date_role_from_the_title_child() {
1838 let mut source = revision();
1839 source.nodes.get_mut(&id(1)).unwrap().created = Some(7);
1840 let bytes = 134_333_468_649_123_456_u64.to_le_bytes();
1841 source.nodes.get_mut(&id(6)).unwrap().extra = vec![vec![
1842 Field {
1843 id: 0x18001c65,
1844 value: FieldValue::Bytes(&bytes),
1845 },
1846 Field {
1847 id: 0x1c001c30,
1848 value: FieldValue::Bytes(&IDENTITY),
1849 },
1850 ]];
1851 source.nodes.get_mut(&id(1)).unwrap().children.clear();
1852 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7));
1853 let mut title = element(Kind::Title);
1854 title.children.push(id(2));
1855 source.nodes.insert(id(7), title);
1856 source.nodes.get_mut(&id(2)).unwrap().extra = vec![vec![Field {
1857 id: 0x88001cb5,
1858 value: FieldValue::NoData,
1859 }]];
1860 source.nodes.get_mut(&id(4)).unwrap().extra = vec![vec![Field {
1861 id: 0x88001c87,
1862 value: FieldValue::NoData,
1863 }]];
1864 let page = Page::from_revision(&source, id(1)).unwrap();
1865 assert_eq!(page.created, Some(134_333_468_649_123_456));
1866 let PageObject::Title(title) = &page.objects[0] else {
1867 panic!()
1868 };
1869 assert_eq!(title.date, Some(id(2)));
1870 assert_eq!(
1871 title.outlines[0].paragraphs[0].text().unwrap().date_field,
1872 Some(DateField::Time)
1873 );
1874 source.nodes.get_mut(&id(6)).unwrap().extra[0][0].value = FieldValue::Bytes(&bytes[..7]);
1875 assert!(Page::from_revision(&source, id(1)).is_err());
1876 source.nodes.get_mut(&id(6)).unwrap().extra.clear();
1877 let bare = Page::from_revision(&source, id(1)).unwrap();
1878 assert_eq!((bare.created, bare.identity), (None, None));
1879 }
1880
1881 #[test]
1882 fn title_role_and_minimum_width_come_from_root_properties() {
1883 let mut source = revision();
1884 source.nodes.get_mut(&id(1)).unwrap().children.clear();
1885 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7));
1886 let mut title = element(Kind::Title);
1887 title.children.push(id(2));
1888 source.nodes.insert(id(7), title);
1889 source.nodes.get_mut(&id(2)).unwrap().extra = vec![vec![
1890 Field {
1891 id: 0x88001cb4,
1892 value: FieldValue::NoData,
1893 },
1894 Field {
1895 id: 0x14001cec,
1896 value: FieldValue::Bytes(&[0, 0, 0x90, 0x40]),
1897 },
1898 ]];
1899 let page = Page::from_revision(&source, id(1)).unwrap();
1900 let PageObject::Title(title) = &page.objects[0] else {
1901 panic!()
1902 };
1903 assert!(title.outlines[0].title);
1904 assert_eq!(title.outlines[0].min_width, Some(162.0));
1905 source.nodes.get_mut(&id(2)).unwrap().extra[0][1].value =
1906 FieldValue::Bytes(&[0, 0, 0x80, 0x7f]);
1907 assert!(Page::from_revision(&source, id(1)).is_err());
1908 source.nodes.get_mut(&id(2)).unwrap().extra = vec![
1909 vec![Field {
1910 id: 0x08001cb4,
1911 value: FieldValue::NoData,
1912 }],
1913 vec![Field {
1914 id: 0x88001cb4,
1915 value: FieldValue::NoData,
1916 }],
1917 ];
1918 let page = Page::from_revision(&source, id(1)).unwrap();
1919 let PageObject::Title(title) = &page.objects[0] else {
1920 panic!()
1921 };
1922 assert!(!title.outlines[0].title);
1923 assert_eq!(title.outlines[0].min_width, None);
1924 }
1925
1926 #[test]
1927 fn preserves_paint_order_nested_parents_and_owned_image_payloads() {
1928 let bytes = vec![1, 2, 3, 4];
1929 let mut source = revision();
1930 let image = element(Kind::Image {
1931 container: Some(id(8)),
1932 filename: None,
1933 alt: Some("Image".into()),
1934 picture_width: None,
1935 picture_height: None,
1936 background: Some(true),
1937 printout: None,
1938 link: None,
1939 });
1940 let file = element(Kind::File {
1941 reference: crate::FileDataReference::Internal([0; 16]),
1942 extension: "png".into(),
1943 payload: Some(bytes.as_slice()),
1944 });
1945 let mut title = element(Kind::Title);
1946 title.layout.x = Some(12.0);
1947 title.layout.y = Some(24.0);
1948 title.children.push(id(10));
1949 let title_outline = element(Kind::Outline {
1950 indents: Vec::new(),
1951 });
1952 let mut group = element(Kind::OutlineGroup);
1953 group.children.push(id(12));
1954 group.child_level = Some(1);
1955 let mut child = element(Kind::Paragraph {
1956 lists: Vec::new(),
1957 paragraph_style: None,
1958 collapse_state: None,
1959 });
1960 child.content.push(id(13));
1961 source.nodes.insert(
1962 id(13),
1963 element(Kind::RichText {
1964 text: "".into(),
1965 runs: Vec::new(),
1966 paragraph_style: None,
1967 boilerplate: false,
1968 }),
1969 );
1970 source.nodes.extend([
1971 (id(7), image),
1972 (id(8), file),
1973 (id(9), title),
1974 (id(10), title_outline),
1975 (id(11), group),
1976 (id(12), child),
1977 ]);
1978 source
1979 .nodes
1980 .get_mut(&id(1))
1981 .unwrap()
1982 .children
1983 .insert(0, id(7));
1984 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(9));
1985 source.nodes.get_mut(&id(3)).unwrap().children.push(id(11));
1986 source.nodes.get_mut(&id(3)).unwrap().child_level = Some(1);
1987 let page = Page::from_revision(&source, id(1)).unwrap();
1988 drop(source);
1989 drop(bytes);
1990 assert_eq!(page.objects.len(), 3);
1991 let PageObject::Image(image) = &page.objects[0] else {
1992 panic!()
1993 };
1994 assert_eq!(image.bytes.as_deref(), Some([1, 2, 3, 4].as_slice()));
1995 let PageObject::Outline(outline) = &page.objects[1] else {
1996 panic!()
1997 };
1998 assert_eq!(outline.paragraphs[1].id, id(12));
1999 assert_eq!(outline.paragraphs[1].parent, Some(id(3)));
2000 assert_eq!(outline.paragraphs[1].level, 3);
2001 let PageObject::Title(title) = &page.objects[2] else {
2002 panic!()
2003 };
2004 assert_eq!(title.id, id(9));
2005 assert_eq!([title.layout.x, title.layout.y], [Some(12.0), Some(24.0)]);
2006 assert_eq!(title.outlines[0].id, id(10));
2007 assert_eq!(title.outlines[0].layout.x, None);
2008 }
2009
2010 #[test]
2011 fn rejects_title_children_that_are_not_outlines() {
2012 let mut source = revision();
2013 let mut title = element(Kind::Title);
2014 title.children.push(id(8));
2015 source.nodes.insert(id(7), title);
2016 source.nodes.insert(id(8), element(Kind::Title));
2017 source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7));
2018 assert_eq!(
2019 Page::from_revision(&source, id(1)).err().unwrap().message,
2020 "Canvas title child is not an outline"
2021 );
2022 }
2023
2024 #[test]
2025 fn validates_empty_text_styles_missing_references_and_cycles() {
2026 let mut source = revision();
2027 source.nodes.get_mut(&id(4)).unwrap().kind = Kind::RichText {
2028 text: String::new(),
2029 runs: Vec::new(),
2030 paragraph_style: Some(id(5)),
2031 boilerplate: false,
2032 };
2033 source.nodes.get_mut(&id(3)).unwrap().format.font_size = Some(10.0);
2034 let page = Page::from_revision(&source, id(1)).unwrap();
2035 let PageObject::Outline(outline) = &page.objects[0] else {
2036 panic!()
2037 };
2038 assert_eq!(
2039 outline.paragraphs[0].text().unwrap().text.spans()[0]
2040 .format
2041 .font_size,
2042 Some(12.0)
2043 );
2044 source.nodes.get_mut(&id(3)).unwrap().children.push(id(2));
2045 assert!(Page::from_revision(&source, id(1)).is_err());
2046 source.nodes.get_mut(&id(3)).unwrap().children.clear();
2047 source.nodes.remove(&id(5));
2048 assert!(Page::from_revision(&source, id(1)).is_err());
2049 }
2050}