1use super::{EditError, EditorError, TextOutline};
2use crate::{
3 date::PageDate,
4 layout::{LayoutError, TextEngine, TextLayout},
5 outline::{Arrange, OutlineLayout},
6};
7use onestore::page::text::Paragraph;
8use onestore::page::{Attachment, Image, Ink, Outline, Page, PageObject, PageParagraph};
9use std::collections::BTreeMap;
10
11/// A title object's own state, plus the child origins `build` replaces with page coordinates.
12pub(crate) struct TitleArea {
13 pub(crate) id: onestore::ExGuid,
14 pub(crate) date: Option<onestore::ExGuid>,
15 pub(crate) layout: onestore::document::Layout,
16 pub(crate) origins: BTreeMap<onestore::ExGuid, [Option<f32>; 2]>,
17}
18
19pub(crate) struct Import {
20 pub(crate) objects: Vec<Content>,
21 pub(crate) outlines: Vec<TextOutline>,
22 pub(crate) date: Option<PageDate>,
23 pub(crate) areas: Vec<TitleArea>,
24 /// The paragraph each outline holding no text shows after its objects, not stored.
25 pub(crate) provisional: BTreeMap<onestore::ExGuid, PageParagraph>,
26}
27
28pub(crate) enum Content {
29 Date {
30 below_title: Option<onestore::ExGuid>,
31 },
32 Outline {
33 source: Outline,
34 layout: OutlineLayout,
35 below_title: Option<onestore::ExGuid>,
36 },
37 Image(Image),
38 /// A file on the page, laid out relative to its position.
39 File {
40 source: Attachment,
41 layout: Box<crate::outline::ObjectLayout>,
42 },
43 Ink(Ink),
44 Editable(onestore::ExGuid),
45 ReadOnly(Box<ReadOnlyObject>),
46}
47
48pub struct ReadOnlyObject {
49 pub source: PageObject,
50 pub message: &'static str,
51 pub(crate) label: TextLayout,
52 /// Title coordinates are stored relative to the page margin; nothing else is offset.
53 offset: [f32; 2],
54}
55
56impl Content {
57 /// A picture drawn as a placeholder because it has no data, size or decodable pixels.
58 pub(crate) fn unavailable(
59 source: PageObject,
60 engine: &mut TextEngine,
61 ) -> Result<Self, EditorError> {
62 Ok(Self::ReadOnly(ReadOnlyObject::new(
63 source,
64 [0.0; 2],
65 "Image unavailable\nRead-only",
66 engine,
67 )?))
68 }
69
70 /// The page-level picture shown, drawn or as a placeholder.
71 pub(super) fn picture(&self) -> Option<&Image> {
72 match self {
73 Self::Image(image) => Some(image),
74 Self::ReadOnly(object) => match &object.source {
75 PageObject::Image(image) => Some(image),
76 _ => None,
77 },
78 _ => None,
79 }
80 }
81
82 pub(super) fn layout(&self) -> Option<(onestore::ExGuid, &onestore::document::Layout)> {
83 match self {
84 Self::Outline { source, .. } => Some((source.id, &source.layout)),
85 Self::Image(source) => Some((source.id, &source.layout)),
86 Self::File { source, .. } => Some((source.id, &source.layout)),
87 Self::Ink(source) => Some((source.id, &source.layout)),
88 Self::ReadOnly(object) => Some((object.source.id(), object.source.layout())),
89 Self::Date { .. } | Self::Editable(_) => None,
90 }
91 }
92
93 /// A file on the page, where its column's `[x0, y0, x1, y1]` lie in page points.
94 pub(crate) fn file(&self) -> Option<(&Attachment, [f32; 4])> {
95 let Self::File { source, layout } = self else {
96 return None;
97 };
98 let origin = crate::origin(&source.layout);
99 Some((source, crate::translated(layout.bounds(), origin)))
100 }
101
102 /// A tagged picture or file: its identity, note tags and bounds in page points.
103 pub(crate) fn tagged(
104 &self,
105 ) -> Option<(onestore::ExGuid, &[onestore::document::Tag], [f32; 4])> {
106 let (id, tags, bounds) = match self {
107 Self::Image(image) => {
108 let [x, y] = crate::origin(&image.layout);
109 let [width, height] = crate::outline::image_size(image)?;
110 (image.id, &image.tags, [x, y, x + width, y + height])
111 }
112 Self::File { source, .. } => (source.id, &source.tags, self.file()?.1),
113 _ => return None,
114 };
115 (!tags.is_empty()).then_some((id, tags.as_slice(), bounds))
116 }
117
118 pub(super) fn layout_mut(
119 &mut self,
120 ) -> Option<(onestore::ExGuid, &mut onestore::document::Layout)> {
121 match self {
122 Self::Outline { source, .. } => Some((source.id, &mut source.layout)),
123 Self::Image(source) => Some((source.id, &mut source.layout)),
124 Self::File { source, .. } => Some((source.id, &mut source.layout)),
125 Self::Ink(source) => Some((source.id, &mut source.layout)),
126 Self::ReadOnly(object) => Some((object.source.id(), object.source.layout_mut())),
127 Self::Date { .. } | Self::Editable(_) => None,
128 }
129 }
130}
131
132impl ReadOnlyObject {
133 fn new(
134 source: PageObject,
135 margin: [f32; 2],
136 message: &'static str,
137 engine: &mut TextEngine,
138 ) -> Result<Box<Self>, EditorError> {
139 let offset = if matches!(source, PageObject::Title(_)) {
140 margin
141 } else {
142 [0.0; 2]
143 };
144 let layout = source.layout();
145 let width = layout.max_width.unwrap_or(160.0);
146 let height = layout.max_height.unwrap_or(42.0);
147 if ![width, height].iter().all(|v| v.is_finite() && *v > 0.0) {
148 return Err(EditorError::InvalidGeometry);
149 }
150 let label = engine.layout(
151 &Paragraph::new(
152 message.into(),
153 onestore::document::Format {
154 font: Some("Arial".into()),
155 font_size: Some(11.0),
156 ..Default::default()
157 },
158 ),
159 width.max(160.0) - 16.0,
160 )?;
161 let object = Box::new(Self {
162 source,
163 message,
164 label,
165 offset,
166 });
167 if object.rect().iter().any(|v| !v.is_finite()) {
168 return Err(EditorError::InvalidGeometry);
169 }
170 Ok(object)
171 }
172
173 pub(crate) fn rect(&self) -> [f32; 4] {
174 let layout = self.source.layout();
175 let x = layout.x.unwrap_or(0.0) + self.offset[0];
176 let y = layout.y.unwrap_or(0.0) + self.offset[1];
177 [
178 x,
179 y,
180 x + layout.max_width.unwrap_or(160.0).max(160.0),
181 y + layout
182 .max_height
183 .unwrap_or(42.0)
184 .max(self.label.height() + 16.0),
185 ]
186 }
187}
188
189pub(crate) fn build(
190 page: &mut Page,
191 engine: &mut TextEngine,
192 editable: bool,
193) -> Result<Import, EditorError> {
194 if page.margin_origin.iter().any(|v| !v.is_finite()) {
195 return Err(EditorError::InvalidGeometry);
196 }
197 let mut objects = Vec::new();
198 let mut outlines = Vec::new();
199 let mut areas = Vec::new();
200 let mut date = None;
201 let mut provisional = BTreeMap::new();
202 for object in std::mem::take(&mut page.objects) {
203 match &object {
204 PageObject::Outline(outline) => {
205 let origin = crate::origin(&outline.layout);
206 if origin.iter().any(|v| !v.is_finite()) {
207 return Err(EditorError::InvalidGeometry);
208 }
209 let unsupported = |error: &EditorError| {
210 matches!(
211 error,
212 EditorError::Layout(LayoutError::UnsupportedContent)
213 | EditorError::Edit(EditError::UnsupportedContent)
214 )
215 };
216 if editable {
217 // Pictures and files alone in an outline take a paragraph after them where
218 // a click beside them lands, as OneNote 2010 adds one when typing there.
219 let extended;
220 let source = if crate::document::leaves(&outline.paragraphs, None)
221 .next()
222 .is_none()
223 {
224 let blank = crate::document::node(
225 Paragraph::new(String::new(), Default::default()),
226 Default::default(),
227 )?;
228 extended = Outline {
229 paragraphs: [&outline.paragraphs[..], std::slice::from_ref(&blank)]
230 .concat(),
231 ..outline.clone()
232 };
233 provisional.insert(outline.id, blank);
234 &extended
235 } else {
236 outline
237 };
238 match TextOutline::from_outline(engine, source, &page.definitions) {
239 Ok(outline) => {
240 objects.push(Content::Editable(outline.id));
241 outlines.push(outline);
242 continue;
243 }
244 Err(error) if !unsupported(&error) => return Err(error),
245 // Drawn as stored when the editor cannot hold it.
246 Err(_) => {
247 provisional.remove(&outline.id);
248 }
249 }
250 }
251 match outline.layout(engine, &page.definitions) {
252 Ok(layout) => objects.push(Content::Outline {
253 source: outline.clone(),
254 layout,
255 below_title: None,
256 }),
257 Err(LayoutError::UnsupportedContent) => {
258 objects.push(Content::ReadOnly(ReadOnlyObject::new(
259 object,
260 page.margin_origin,
261 "Unsupported content\nRead-only",
262 engine,
263 )?))
264 }
265 Err(error) => return Err(error.into()),
266 }
267 }
268 PageObject::Title(title) => {
269 let layouts = match title.layout(engine, &page.definitions) {
270 Ok(layouts) => layouts,
271 Err(LayoutError::UnsupportedContent) => {
272 objects.push(Content::ReadOnly(ReadOnlyObject::new(
273 object,
274 page.margin_origin,
275 "Unsupported content\nRead-only",
276 engine,
277 )?));
278 continue;
279 }
280 Err(error) => return Err(error.into()),
281 };
282 areas.push(TitleArea {
283 id: title.id,
284 date: title.date,
285 layout: title.layout.clone(),
286 origins: title
287 .outlines
288 .iter()
289 .map(|outline| (outline.id, [outline.layout.x, outline.layout.y]))
290 .collect(),
291 });
292 let mut anchor = if editable {
293 title
294 .outlines
295 .iter()
296 .zip(&layouts)
297 .find(|(source, _)| source.title)
298 .map(|(source, (origin, layout))| {
299 (
300 source.id,
301 title.layout.y.unwrap_or(0.0)
302 + page.margin_origin[1]
303 + origin[1]
304 + layout.size[1].max(source.layout.max_height.unwrap_or(0.0)),
305 )
306 })
307 } else {
308 None
309 };
310 for (index, (origin, layout)) in layouts.into_iter().enumerate() {
311 let mut origin = [
312 title.layout.x.unwrap_or(0.0) + origin[0] + page.margin_origin[0],
313 title.layout.y.unwrap_or(0.0) + origin[1] + page.margin_origin[1],
314 ];
315 if origin.iter().any(|v| !v.is_finite()) {
316 return Err(EditorError::InvalidGeometry);
317 }
318 if editable && title.outlines[index].title {
319 let mut source = title.outlines[index].clone();
320 source.layout.x = Some(origin[0]);
321 source.layout.y = Some(origin[1]);
322 let outline =
323 TextOutline::from_outline(engine, &source, &page.definitions)?;
324 anchor = Some((outline.id, outline.bounds().y1 as f32));
325 objects.push(Content::Editable(outline.id));
326 outlines.push(outline);
327 } else {
328 if let Some((_, bottom)) = anchor {
329 origin[1] -= bottom;
330 }
331 if editable
332 && title.date == Some(title.outlines[index].id)
333 && let Some(timestamp) = page.created
334 {
335 let mut source = title.outlines[index].clone();
336 source.layout.x = Some(origin[0]);
337 source.layout.y = Some(origin[1]);
338 match PageDate::new(timestamp, source, engine, &page.definitions) {
339 Ok(value) => {
340 if date.replace(value).is_some() {
341 return Err(EditorError::InvalidGeometry);
342 }
343 objects.push(Content::Date {
344 below_title: anchor.map(|(id, _)| id),
345 });
346 continue;
347 }
348 Err(EditorError::Edit(
349 onestore::page::text::EditError::UnsupportedContent,
350 )) => {}
351 Err(error) => return Err(error),
352 }
353 }
354 let mut source = title.outlines[index].clone();
355 source.layout.x = Some(origin[0]);
356 source.layout.y = Some(origin[1]);
357 objects.push(Content::Outline {
358 source,
359 layout,
360 below_title: anchor.map(|(id, _)| id),
361 });
362 }
363 }
364 }
365 PageObject::Image(source) => {
366 // A picture without a layout size shows at its own, as COM inserts them.
367 let Some(size) =
368 crate::outline::image_size(source).filter(|_| source.bytes.is_some())
369 else {
370 objects.push(Content::unavailable(object, engine)?);
371 continue;
372 };
373 let origin = crate::origin(&source.layout);
374 if origin.iter().any(|v| !v.is_finite())
375 || size.iter().any(|v| !v.is_finite() || *v <= 0.0)
376 || !(origin[0] + size[0]).is_finite()
377 || !(origin[1] + size[1]).is_finite()
378 {
379 return Err(EditorError::InvalidGeometry);
380 }
381 let PageObject::Image(source) = object else {
382 unreachable!()
383 };
384 objects.push(Content::Image(source));
385 }
386 PageObject::Attachment(source) => {
387 if [source.layout.x, source.layout.y]
388 .iter()
389 .any(|v| !v.unwrap_or(0.0).is_finite())
390 {
391 return Err(EditorError::InvalidGeometry);
392 }
393 let layout = Box::new(crate::outline::page_file(engine, source)?);
394 let PageObject::Attachment(source) = object else {
395 unreachable!()
396 };
397 objects.push(Content::File { source, layout });
398 }
399 PageObject::Ink(ink) => {
400 if ink_bounds(ink).is_some_and(|b| b.iter().any(|v| !v.is_finite())) {
401 return Err(EditorError::InvalidGeometry);
402 }
403 let PageObject::Ink(ink) = object else {
404 unreachable!()
405 };
406 objects.push(Content::Ink(ink))
407 }
408 PageObject::Unsupported(_) => objects.push(Content::ReadOnly(ReadOnlyObject::new(
409 object,
410 page.margin_origin,
411 "Unsupported content\nRead-only",
412 engine,
413 )?)),
414 }
415 }
416 Ok(Import {
417 objects,
418 outlines,
419 date,
420 areas,
421 provisional,
422 })
423}
424
425/// The painted extent of a page's ink drawing, pen included, as `[x0, y0, x1, y1]` page
426/// points.
427pub(crate) fn ink_bounds(ink: &Ink) -> Option<[f32; 4]> {
428 stroke_bounds(ink).map(|bounds| crate::translated(bounds, crate::origin(&ink.layout)))
429}
430
431/// The painted extent of every stroke before the drawing's offset.
432fn stroke_bounds(ink: &Ink) -> Option<[f32; 4]> {
433 ink.strokes
434 .iter()
435 .flat_map(|stroke| {
436 let size = stroke.width.max(stroke.height) * 0.5;
437 stroke
438 .points
439 .iter()
440 .enumerate()
441 .map(move |(index, [x, y])| {
442 let r = size * stroke.thickness(index);
443 [x - r, y - r, x + r, y + r]
444 })
445 })
446 .chain(ink.groups.iter().filter_map(stroke_bounds))
447 .reduce(|a, b| {
448 [
449 a[0].min(b[0]),
450 a[1].min(b[1]),
451 a[2].max(b[2]),
452 a[3].max(b[3]),
453 ]
454 })
455}