1use std::{collections::HashMap, io::Read};
2
3use raqote::{DrawOptions, DrawTarget, LineCap, LineJoin, Source, StrokeStyle};
4
5use crate::{
6 error::{Error, Result},
7 id::{NoteUuid, PageModelUuid, PageUuid, PointsUuid, ShapeGroupUuid, VirtualPageUuid},
8 note_tree::{NoteMetadata, NoteTree},
9 page_model::{PageModel, PageModelGroup},
10 shape::ShapeGroup,
11 utils::convert_timestamp_to_datetime,
12 virtual_doc::VirtualDoc,
13 virtual_page::VirtualPage,
14};
15
16mod container;
17mod json;
18mod note_tree;
19mod page_model;
20mod utils;
21mod virtual_doc;
22
23pub mod error;
24pub mod extra;
25pub mod id;
26pub mod note_assets;
27pub mod points;
28pub mod resource;
29pub mod shape;
30pub mod template;
31pub mod virtual_page;
32
33#[derive(Debug, Clone)]
34struct QuickPenRenderStyle {
35 pen_type: u8,
36 width: f32,
37 color_argb: u32,
38}
39
40#[derive(Debug, Clone)]
41struct PageStyleContext {
42 default_color_argb: u32,
43 note_pen_width: f32,
44 pen_width_map: HashMap<u8, f32>,
45 quick_pen_styles: Vec<QuickPenRenderStyle>,
46}
47
48impl PageStyleContext {
49 fn from_note_metadata(metadata: &NoteMetadata) -> Self {
50 Self {
51 default_color_argb: metadata.pen_settings.fill_color,
52 note_pen_width: metadata.pen_width,
53 pen_width_map: metadata.pen_settings.pen_width_map.clone(),
54 quick_pen_styles: metadata
55 .pen_settings
56 .quick_pen_list
57 .quick_pens
58 .iter()
59 .map(|pen| QuickPenRenderStyle {
60 pen_type: pen.type_,
61 width: pen.width,
62 color_argb: pen.color,
63 })
64 .collect(),
65 }
66 }
67
68 fn fallback_width(&self) -> f32 {
69 if self.note_pen_width.is_finite() && self.note_pen_width > 0.0 {
70 self.note_pen_width
71 } else {
72 1.0
73 }
74 }
75
76 fn resolve_stroke_width(&self, shape_width: f32, pen_type: Option<u8>) -> f32 {
77 if shape_width.is_finite() && shape_width > 0.0 {
78 return shape_width;
79 }
80
81 if let Some(pen_type) = pen_type {
82 if let Some(width) = self.pen_width_map.get(&pen_type) {
83 if width.is_finite() && *width > 0.0 {
84 return *width;
85 }
86 }
87 }
88
89 self.fallback_width()
90 }
91
92 fn resolve_stroke_color(
93 &self,
94 shape_color_argb: u32,
95 pen_type: Option<u8>,
96 shape_width: f32,
97 resolved_width: f32,
98 ) -> u32 {
99 if shape_color_argb != 0 {
101 return shape_color_argb;
102 }
103
104 let Some(pen_type) = pen_type else {
105 return self.default_color_argb;
106 };
107
108 let target_width = if shape_width.is_finite() && shape_width > 0.0 {
109 shape_width
110 } else {
111 resolved_width
112 };
113
114 if let Some(best_match) = self
115 .quick_pen_styles
116 .iter()
117 .filter(|pen| pen.pen_type == pen_type)
118 .min_by(|a, b| {
119 (a.width - target_width)
120 .abs()
121 .total_cmp(&(b.width - target_width).abs())
122 })
123 {
124 return best_match.color_argb;
125 }
126
127 self.default_color_argb
128 }
129}
130
131pub struct NoteFile<R: std::io::Read + std::io::Seek> {
132 container: container::Container<R>,
133 note_tree: NoteTree,
134}
135
136impl<R: std::io::Read + std::io::Seek> NoteFile<R> {
137 pub fn read(reader: R) -> Result<Self> {
138 let mut container = container::Container::open(reader)?;
139
140 let note_tree = if *container.container_type() == container::ContainerType::MultiNote {
141 container.get_file_relative("note_tree", |reader| NoteTree::read(reader))?
142 } else {
143 container.get_file_relative("note/pb/note_info", |reader| NoteTree::read(reader))?
144 };
145
146 Ok(Self {
147 container,
148 note_tree,
149 })
150 }
151
152 pub fn list_notes(&self) -> HashMap<NoteUuid, String> {
153 self.note_tree
154 .notes
155 .iter()
156 .map(|(id, metadata)| (*id, metadata.name.clone()))
157 .collect()
158 }
159
160 pub fn get_note(&self, note_id: &NoteUuid) -> Option<Note<R>> {
161 self.note_tree.notes.get(note_id).map(|metadata| {
162 let note_path_prefix =
163 if *self.container.container_type() == container::ContainerType::MultiNote {
164 metadata.note_id.to_simple_string()
165 } else {
166 String::new()
167 };
168 Note::new(self.container.clone(), metadata.clone(), note_path_prefix)
169 })
170 }
171}
172
173impl<R: std::io::Read + std::io::Seek> std::fmt::Debug for NoteFile<R> {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 f.debug_struct("NoteFile")
176 .field("container_type", &self.container.container_type())
177 .field("note_tree", &self.note_tree)
178 .finish()
179 }
180}
181
182pub struct Note<R: std::io::Read + std::io::Seek> {
183 container: container::Container<R>,
184 metadata: NoteMetadata,
185 note_path_prefix: String,
186 virtual_doc: Option<VirtualDoc>,
187 virtual_pages: Option<HashMap<VirtualPageUuid, VirtualPage>>,
188 page_models: Option<HashMap<PageModelUuid, PageModelGroup>>,
189 extra_metadata: Option<Option<extra::ExtraMetadata>>,
190 templates: Option<template::TemplateSet>,
191 resources: Option<Vec<resource::ResourceRecord>>,
192 assets_metadata: Option<note_assets::NoteAssetsMetadata>,
193}
194
195impl<R: std::io::Read + std::io::Seek> Note<R> {
196 fn new(
197 container: container::Container<R>,
198 metadata: NoteMetadata,
199 note_path_prefix: String,
200 ) -> Self {
201 Self {
202 container,
203 metadata,
204 note_path_prefix,
205 virtual_doc: None,
206 virtual_pages: None,
207 page_models: None,
208 extra_metadata: None,
209 templates: None,
210 resources: None,
211 assets_metadata: None,
212 }
213 }
214
215 pub fn name(&self) -> &str {
216 &self.metadata.name
217 }
218
219 pub fn active_pages(&self) -> &[PageUuid] {
220 &self.metadata.active_pages
221 }
222
223 pub fn reserved_pages(&self) -> &[PageUuid] {
224 &self.metadata.reserved_pages
225 }
226
227 pub fn detached_pages(&self) -> &[PageUuid] {
228 &self.metadata.detached_pages
229 }
230
231 pub fn created(&self) -> chrono::DateTime<chrono::Utc> {
232 self.metadata.created
233 }
234
235 pub fn modified(&self) -> chrono::DateTime<chrono::Utc> {
236 self.metadata.modified
237 }
238
239 pub fn flag(&self) -> u32 {
240 self.metadata.flag
241 }
242
243 pub fn pen_width(&self) -> f32 {
244 self.metadata.pen_width
245 }
246
247 pub fn scale_factor(&self) -> f32 {
248 self.metadata.scale_factor
249 }
250
251 pub fn fill_color(&self) -> &u32 {
252 &self.metadata.fill_color
253 }
254
255 pub fn pen_type(&self) -> &u32 {
256 &self.metadata.pen_type
257 }
258
259 pub fn pen_settings_fill_color(&self) -> &u32 {
260 &self.metadata.pen_settings.fill_color
261 }
262
263 pub fn pen_settings_graphics_shape_color(&self) -> &u32 {
264 &self.metadata.pen_settings.graphics_shape_color
265 }
266
267 pub fn get_page(&mut self, page_id: &PageUuid) -> Option<Page<R>> {
268 let style_context = PageStyleContext::from_note_metadata(&self.metadata);
269
270 let virtual_page = {
271 let virtual_pages = self
272 .virtual_pages()
273 .inspect_err(|_| {
274 log::error!("Failed to get virtual pages for page ID: {}", page_id);
275 })
276 .ok()?;
277
278 virtual_pages
279 .values()
280 .find(|vp| &vp.page_id == page_id)
281 .cloned()
282 };
283
284 let page_model = {
285 let page_models = self
286 .page_models()
287 .inspect_err(|_| {
288 log::error!("Failed to get page models for page ID: {}", page_id);
289 })
290 .ok()?;
291
292 page_models
293 .values()
294 .find_map(|pm| pm.page_models.iter().find(|p| p.page_id == *page_id))?
295 .clone()
296 };
297
298 Some(Page::new(
299 self.container.clone(),
300 self.metadata.note_id,
301 page_id.clone(),
302 self.note_path_prefix.clone(),
303 virtual_page,
304 page_model,
305 style_context,
306 ))
307 }
308
309 pub fn virtual_doc(&mut self) -> Result<&VirtualDoc> {
310 if self.virtual_doc.is_none() {
311 let note_id = self.metadata.note_id.to_simple_string();
312 let virtual_doc_path = join_archive_path(
313 &self.note_path_prefix,
314 &format!("virtual/doc/pb/{}", note_id),
315 );
316 let virtual_doc = self
317 .container
318 .get_file_relative(&virtual_doc_path, |reader| VirtualDoc::read(reader))?;
319 self.virtual_doc = Some(virtual_doc);
320 }
321 Ok(self.virtual_doc.as_ref().unwrap())
322 }
323
324 pub fn virtual_pages(&mut self) -> Result<&HashMap<VirtualPageUuid, VirtualPage>> {
325 if self.virtual_pages.is_none() {
326 let mut virtual_pages = HashMap::new();
327
328 for virtual_page_path in self.container.list_directory(&join_archive_path(
329 &self.note_path_prefix,
330 "virtual/page/pb",
331 )) {
332 let virtual_page_id =
333 VirtualPageUuid::from_str(file_name_from_path(&virtual_page_path)?)?;
334 let virtual_page = self
335 .container
336 .get_file_absolute(&virtual_page_path, |reader| VirtualPage::read(reader))?;
337 virtual_pages.insert(virtual_page_id, virtual_page);
338 }
339 self.virtual_pages = Some(virtual_pages);
340 }
341 Ok(self.virtual_pages.as_ref().unwrap())
342 }
343
344 pub fn page_models(&mut self) -> Result<&HashMap<PageModelUuid, PageModelGroup>> {
345 if self.page_models.is_none() {
346 let mut page_models = HashMap::new();
347
348 for page_model_path in self
349 .container
350 .list_directory(&join_archive_path(&self.note_path_prefix, "pageModel/pb"))
351 {
352 let page_model_id =
353 PageModelUuid::from_str(file_name_from_path(&page_model_path)?)?;
354 let page_model = self
355 .container
356 .get_file_absolute(&page_model_path, |reader| PageModelGroup::read(reader))?;
357 page_models.insert(page_model_id, page_model);
358 }
359 self.page_models = Some(page_models);
360 }
361 Ok(self.page_models.as_ref().unwrap())
362 }
363
364 pub fn extra_metadata(&mut self) -> Result<Option<&extra::ExtraMetadata>> {
365 if self.extra_metadata.is_none() {
366 let extra_path = join_archive_path(&self.note_path_prefix, "extra/pb/extra");
367 let parsed_extra =
368 if self.container.has_entry_relative(&extra_path) {
369 Some(self.container.get_file_relative(&extra_path, |reader| {
370 extra::ExtraMetadata::read(reader)
371 })?)
372 } else {
373 None
374 };
375 self.extra_metadata = Some(parsed_extra);
376 }
377
378 Ok(self.extra_metadata.as_ref().unwrap().as_ref())
379 }
380
381 pub fn templates(&mut self) -> Result<&template::TemplateSet> {
382 if self.templates.is_none() {
383 let mut templates = template::TemplateSet::new();
384
385 let primary_templates_path = join_archive_path(&self.note_path_prefix, "template/json");
386 self.load_templates_from_directory(&primary_templates_path, &mut templates)?;
387
388 let document_templates_path = join_archive_path(
389 &self.note_path_prefix,
390 &format!(
391 "document/{}/template/json",
392 self.metadata.note_id.to_simple_string()
393 ),
394 );
395 self.load_templates_from_directory(&document_templates_path, &mut templates)?;
396
397 self.templates = Some(templates);
398 }
399
400 Ok(self.templates.as_ref().unwrap())
401 }
402
403 pub fn default_template(&mut self) -> Result<Option<&template::TemplateDescriptor>> {
404 Ok(self.templates()?.default.as_ref())
405 }
406
407 pub fn template_for_page(
408 &mut self,
409 page_id: &PageUuid,
410 ) -> Result<Option<&template::TemplateDescriptor>> {
411 Ok(self.templates()?.for_page(page_id))
412 }
413
414 pub fn resources(&mut self) -> Result<&Vec<resource::ResourceRecord>> {
415 if self.resources.is_none() {
416 let mut resources = Vec::new();
417 let resource_path = join_archive_path(&self.note_path_prefix, "resource/pb");
418
419 if self.container.has_entry_relative(&resource_path) {
420 for archive_path in self.container.list_directory(&resource_path) {
421 let container_relative_path =
422 to_container_relative_path(self.container.root_path(), &archive_path);
423 let note_relative_path =
424 to_note_relative_path(&self.note_path_prefix, &container_relative_path);
425 let size_bytes = self
426 .container
427 .file_size_relative(&container_relative_path)?;
428
429 let resource = if size_bytes == 0 {
430 resource::ResourceRecord::read(
431 note_relative_path,
432 std::io::Cursor::new(Vec::<u8>::new()),
433 size_bytes,
434 )?
435 } else {
436 let bytes =
437 self.container
438 .get_file_absolute(&archive_path, |mut reader| {
439 let mut buffer = Vec::new();
440 reader.read_to_end(&mut buffer).map_err(Error::Io)?;
441 Ok(buffer)
442 })?;
443 resource::ResourceRecord::read(
444 note_relative_path,
445 std::io::Cursor::new(bytes),
446 size_bytes,
447 )?
448 };
449 resources.push(resource);
450 }
451 }
452
453 self.resources = Some(resources);
454 }
455
456 Ok(self.resources.as_ref().unwrap())
457 }
458
459 pub fn assets_metadata(&mut self) -> Result<¬e_assets::NoteAssetsMetadata> {
460 if self.assets_metadata.is_none() {
461 let toc_path = join_archive_path(&self.note_path_prefix, "toc");
462 let toc_exists = self.container.has_entry_relative(&toc_path);
463
464 let mut toc_files = Vec::new();
465 for archive_path in self.container.list_directory(&toc_path) {
466 let container_relative_path =
467 to_container_relative_path(self.container.root_path(), &archive_path);
468 let note_relative_path =
469 to_note_relative_path(&self.note_path_prefix, &container_relative_path);
470 let size_bytes = self
471 .container
472 .file_size_relative(&container_relative_path)?;
473 toc_files.push(note_assets::FileMetadata {
474 path: note_relative_path,
475 size_bytes,
476 });
477 }
478
479 let preferred_preview_path = join_archive_path(
480 &self.note_path_prefix,
481 &format!("{}.png", self.metadata.note_id.to_simple_string()),
482 );
483 let preview = if self.container.has_entry_relative(&preferred_preview_path) {
484 let size_bytes = self.container.file_size_relative(&preferred_preview_path)?;
485 Some(note_assets::FileMetadata {
486 path: to_note_relative_path(&self.note_path_prefix, &preferred_preview_path),
487 size_bytes,
488 })
489 } else {
490 self.discover_fallback_preview()?
491 };
492
493 self.assets_metadata = Some(note_assets::NoteAssetsMetadata {
494 toc: note_assets::TocMetadata {
495 exists: toc_exists,
496 files: toc_files,
497 },
498 preview,
499 });
500 }
501
502 Ok(self.assets_metadata.as_ref().unwrap())
503 }
504
505 fn load_templates_from_directory(
506 &mut self,
507 directory_path: &str,
508 templates: &mut template::TemplateSet,
509 ) -> Result {
510 if !self.container.has_entry_relative(directory_path) {
511 return Ok(());
512 }
513
514 for template_path in self.container.list_directory(directory_path) {
515 let file_name = file_name_from_path(&template_path)?;
516 let Some(key) = template::classify_template_file_name(file_name) else {
517 continue;
518 };
519
520 let bytes = self.container.get_file_absolute(&template_path, |mut reader| {
524 let mut buf = Vec::new();
525 reader.read_to_end(&mut buf).map_err(Error::Io)?;
526 Ok(buf)
527 })?;
528 if bytes.is_empty() {
529 continue;
530 }
531 let descriptor =
532 template::TemplateDescriptor::read(std::io::Cursor::new(bytes))?;
533 templates.insert_if_absent(key, descriptor);
534 }
535
536 Ok(())
537 }
538
539 fn discover_fallback_preview(&mut self) -> Result<Option<note_assets::FileMetadata>> {
540 let note_root_path = self.note_path_prefix.as_str();
541 for archive_path in self.container.list_directory(note_root_path) {
542 let container_relative_path =
543 to_container_relative_path(self.container.root_path(), &archive_path);
544 let note_relative_path =
545 to_note_relative_path(&self.note_path_prefix, &container_relative_path);
546
547 if !note_relative_path.ends_with(".png") || note_relative_path.contains('/') {
548 continue;
549 }
550
551 let size_bytes = self
552 .container
553 .file_size_relative(&container_relative_path)?;
554 return Ok(Some(note_assets::FileMetadata {
555 path: note_relative_path,
556 size_bytes,
557 }));
558 }
559
560 Ok(None)
561 }
562}
563
564impl<R: std::io::Read + std::io::Seek> std::fmt::Debug for Note<R> {
565 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566 f.debug_struct("Note")
567 .field("metadata", &self.metadata)
568 .finish()
569 }
570}
571
572pub struct Page<R: std::io::Read + std::io::Seek> {
573 container: container::Container<R>,
574 note_id: NoteUuid,
575 note_path_prefix: String,
576 page_id: PageUuid,
577 virtual_page: Option<VirtualPage>,
578 page_model: PageModel,
579 shape_groups: Option<HashMap<ShapeGroupUuid, ShapeGroup>>,
580 shape_group_draw_order: Option<Vec<ShapeGroupUuid>>,
581 points_files: Option<HashMap<PointsUuid, Vec<points::PointsFile>>>,
582 template: Option<Option<template::TemplateDescriptor>>,
583 style_context: PageStyleContext,
584}
585
586impl<R: std::io::Read + std::io::Seek> Page<R> {
587 fn new(
588 container: container::Container<R>,
589 note_id: NoteUuid,
590 page_id: PageUuid,
591 note_path_prefix: String,
592 virtual_page: Option<VirtualPage>,
593 page_model: PageModel,
594 style_context: PageStyleContext,
595 ) -> Self {
596 Self {
597 container,
598 note_id,
599 page_id,
600 note_path_prefix,
601 virtual_page,
602 page_model,
603 shape_groups: None,
604 shape_group_draw_order: None,
605 points_files: None,
606 template: None,
607 style_context,
608 }
609 }
610
611 pub fn virtual_page(&self) -> &Option<VirtualPage> {
612 &self.virtual_page
613 }
614
615 pub fn page_model(&self) -> &PageModel {
616 &self.page_model
617 }
618
619 pub fn template(&mut self) -> Result<Option<&template::TemplateDescriptor>> {
620 if self.template.is_none() {
621 let page_id_simple = self.page_id.to_simple_string();
622 let page_id_hyphenated = self.page_id.to_hyphenated_string();
623 let note_id_simple = self.note_id.to_simple_string();
624
625 let template_paths = [
626 join_archive_path(
627 &self.note_path_prefix,
628 &format!("template/json/{}.template_json", page_id_simple),
629 ),
630 join_archive_path(
631 &self.note_path_prefix,
632 &format!("template/json/{}.template_json", page_id_hyphenated),
633 ),
634 join_archive_path(
635 &self.note_path_prefix,
636 &format!(
637 "document/{}/template/json/{}.template_json",
638 note_id_simple, page_id_simple
639 ),
640 ),
641 join_archive_path(
642 &self.note_path_prefix,
643 &format!(
644 "document/{}/template/json/{}.template_json",
645 note_id_simple, page_id_hyphenated
646 ),
647 ),
648 ];
649
650 let mut matched_template = None;
651 for template_path in template_paths {
652 if !self.container.has_entry_relative(&template_path) {
653 continue;
654 }
655
656 let bytes = self.container.get_file_relative(&template_path, |mut reader| {
658 let mut buf = Vec::new();
659 reader.read_to_end(&mut buf).map_err(Error::Io)?;
660 Ok(buf)
661 })?;
662 if bytes.is_empty() {
663 continue;
664 }
665 matched_template =
666 Some(template::TemplateDescriptor::read(std::io::Cursor::new(bytes))?);
667 break;
668 }
669
670 self.template = Some(matched_template);
671 }
672
673 Ok(self.template.as_ref().unwrap().as_ref())
674 }
675
676 pub fn shape_groups(&mut self) -> Result<&HashMap<ShapeGroupUuid, ShapeGroup>> {
677 if self.shape_groups.is_none() {
678 let page_id_simple = self.page_id.to_simple_string();
681 let page_id_hyphenated = self.page_id.to_hyphenated_string();
682
683 let mut shape_groups = HashMap::new();
684 let mut shape_group_entries = Vec::new();
685
686 let shape_prefixes = [
687 join_archive_path(&self.note_path_prefix, &format!("shape/{}#", page_id_simple)),
688 join_archive_path(
689 &self.note_path_prefix,
690 &format!("shape/{}#", page_id_hyphenated),
691 ),
692 ];
693 let shape_paths: Vec<String> = shape_prefixes
694 .iter()
695 .flat_map(|prefix| self.container.list_directory(prefix))
696 .collect();
697
698 for (index, shape_group_path) in shape_paths.into_iter().enumerate() {
699 let path_tail = file_name_from_path(&shape_group_path)?;
700 let (shape_group_id, timestamp) = parse_shape_group_file_name(path_tail)?;
701 let _timestamp = convert_timestamp_to_datetime(timestamp)?;
702 let shape_group = self
703 .container
704 .get_file_absolute(&shape_group_path, |reader| ShapeGroup::read(reader))?;
705
706 if !shape_groups.contains_key(&shape_group_id) {
707 shape_group_entries.push((timestamp, index, shape_group_id));
708 }
709 shape_groups.insert(shape_group_id, shape_group);
710 }
711 shape_group_entries.sort_by_key(|(timestamp, index, _)| (*timestamp, *index));
712 self.shape_groups = Some(shape_groups);
713 self.shape_group_draw_order = Some(
714 shape_group_entries
715 .into_iter()
716 .map(|(_, _, shape_group_id)| shape_group_id)
717 .collect(),
718 );
719 }
720 Ok(self.shape_groups.as_ref().unwrap())
721 }
722
723 pub fn points_files(&mut self) -> Result<&HashMap<PointsUuid, Vec<points::PointsFile>>> {
724 if self.points_files.is_none() {
725 let page_id_simple = self.page_id.to_simple_string();
727 let page_id_hyphenated = self.page_id.to_hyphenated_string();
728
729 let mut points_files = HashMap::new();
730
731 let points_prefixes = [
732 join_archive_path(
733 &self.note_path_prefix,
734 &format!("point/{}/{}#", page_id_simple, page_id_simple),
735 ),
736 join_archive_path(
737 &self.note_path_prefix,
738 &format!("point/{}/{}#", page_id_hyphenated, page_id_hyphenated),
739 ),
740 ];
741 let stroke_paths: Vec<String> = points_prefixes
742 .iter()
743 .flat_map(|prefix| self.container.list_directory(prefix))
744 .collect();
745
746 for stroke_path in stroke_paths {
747 let path_tail = file_name_from_path(&stroke_path)?;
748 let shape_id = parse_points_file_name(path_tail)?;
749
750 let file_data = self
751 .container
752 .get_file_absolute(&stroke_path, |mut reader| {
753 let mut buffer = Vec::new();
754 reader.read_to_end(&mut buffer).map_err(Error::Io)?;
755 Ok(buffer)
756 })?;
757
758 let buffer_cursor = std::io::Cursor::new(file_data);
759 let points_file = points::PointsFile::read(buffer_cursor)?;
760
761 points_files
762 .entry(shape_id)
763 .or_insert_with(Vec::new)
764 .push(points_file);
765 }
766 self.points_files = Some(points_files);
767 }
768 Ok(self.points_files.as_ref().unwrap())
769 }
770
771 pub fn is_empty(&mut self) -> Result<bool> {
775 self.shape_groups()?;
776 let shape_groups = self.shape_groups.as_ref().unwrap();
777 Ok(shape_groups.values().all(|group| group.shapes().is_empty()))
778 }
779
780 pub fn render(&mut self) -> Result<DrawTarget> {
781 let page_id = self.page_id.to_hyphenated_string();
782 let width = self.page_model.dimensions.right - self.page_model.dimensions.left;
783 let height = self.page_model.dimensions.bottom - self.page_model.dimensions.top;
784 let mut draw_target = DrawTarget::new(width as i32, height as i32);
785 let draw_options = DrawOptions::new();
786
787 draw_target.fill_rect(
788 0.0,
789 0.0,
790 width,
791 height,
792 &Source::Solid(raqote::Color::new(255, 255, 255, 255).into()),
793 &DrawOptions::new(),
794 );
795
796 self.shape_groups()
797 .inspect_err(|_| log::error!("Failed to get shape groups for page ID: {}", page_id))?;
798 self.points_files()
799 .inspect_err(|_| log::error!("Failed to get points files for page ID: {}", page_id))?;
800
801 let shape_groups = self.shape_groups.as_ref().unwrap();
802 let shape_group_draw_order = self.shape_group_draw_order.as_ref().unwrap();
803 let points_files = self.points_files.as_ref().unwrap();
804
805 let mut last_shape_index_by_stroke = HashMap::new();
808 let mut shape_index = 0usize;
809 for shape_group_id in shape_group_draw_order {
810 let Some(shape_group) = shape_groups.get(shape_group_id) else {
811 continue;
812 };
813 for shape in shape_group.shapes() {
814 last_shape_index_by_stroke.insert(shape.stroke_id, shape_index);
815 shape_index += 1;
816 }
817 }
818
819 let mut layer_draw_order: Vec<u32> = self
820 .page_model
821 .layers
822 .iter()
823 .filter(|layer| layer.show)
824 .map(|layer| layer.id.value())
825 .collect();
826
827 for shape_group_id in shape_group_draw_order {
828 let Some(shape_group) = shape_groups.get(shape_group_id) else {
829 continue;
830 };
831 for shape in shape_group.shapes() {
832 if !layer_draw_order.contains(&shape.unknown_6) {
833 layer_draw_order.push(shape.unknown_6);
834 }
835 }
836 }
837
838 const ALPHA_PASS_TRANSLUCENT: u8 = 0;
843 const ALPHA_PASS_OPAQUE: u8 = 1;
844 for layer_id in layer_draw_order {
845 for alpha_pass in [ALPHA_PASS_TRANSLUCENT, ALPHA_PASS_OPAQUE] {
846 let mut shape_index = 0usize;
847 for shape_group_id in shape_group_draw_order {
848 let Some(shape_group) = shape_groups.get(shape_group_id) else {
849 log::warn!(
850 "Shape group listed in draw order but missing in map: {}",
851 shape_group_id.to_hyphenated_string()
852 );
853 continue;
854 };
855 for shape in shape_group.shapes() {
856 let is_final_shape_revision =
857 last_shape_index_by_stroke.get(&shape.stroke_id).copied()
858 == Some(shape_index);
859 shape_index += 1;
860
861 if !is_final_shape_revision {
862 continue;
863 }
864
865 if shape.unknown_6 != layer_id {
866 continue;
867 }
868
869 let pen_type = u8::try_from(shape.z_order).ok();
870 let stroke_width = self
871 .style_context
872 .resolve_stroke_width(shape.stroke_width, pen_type);
873 let mut stroke_style = StrokeStyle {
874 width: stroke_width,
875 cap: LineCap::Round,
876 join: LineJoin::Round,
877 ..StrokeStyle::default()
878 };
879
880 if let Some(line_style) = shape.line_style.as_ref() {
881 stroke_style.dash_offset = if line_style.phase.is_finite() {
882 line_style.phase
883 } else {
884 0.0
885 };
886 stroke_style.dash_array = match line_style.type_ {
887 0 => Vec::new(),
888 1 => vec![4.0 * stroke_width, 2.0 * stroke_width],
889 2 => vec![stroke_width, 1.5 * stroke_width],
890 3 => vec![
891 4.0 * stroke_width,
892 2.0 * stroke_width,
893 stroke_width,
894 2.0 * stroke_width,
895 ],
896 _ => Vec::new(),
897 };
898 }
899
900 if let Some(points_id) = shape.points_id {
901 if let Some(points_candidates) = points_files.get(&points_id) {
902 let stroke = points_candidates
903 .iter()
904 .find_map(|points_file| points_file.get_stroke(&shape.stroke_id))
905 .ok_or_else(|| {
906 log::error!("Failed to get stroke for shape");
907 Error::StrokeNotFound
908 })?;
909
910 let resolved_argb = self.style_context.resolve_stroke_color(
911 shape.unknown as u32,
912 pen_type,
913 shape.stroke_width,
914 stroke_width,
915 );
916 let stroke_color_argb = apply_pen_type_alpha(resolved_argb, pen_type);
917
918 let stroke_alpha = (stroke_color_argb >> 24) as u8;
919 let is_translucent = stroke_alpha < 255;
920 let pass_matches = match alpha_pass {
921 ALPHA_PASS_TRANSLUCENT => is_translucent,
922 _ => !is_translucent,
923 };
924 if !pass_matches {
925 continue;
926 }
927
928 log::debug!("Rendering stroke for shape");
929 log::debug!(
930 "Shape Group ID: {}, Stroke ID: {}",
931 shape_group_id.to_hyphenated_string(),
932 shape.stroke_id.to_hyphenated_string()
933 );
934 log::debug!("Shape: {:#x?}", shape);
935
936 stroke.render_with_color(
937 &mut draw_target,
938 &draw_options,
939 &stroke_style,
940 argb_to_raqote_color(stroke_color_argb),
941 )?;
942 } else {
943 log::warn!(
944 "No points files found for shape group: {}",
945 shape_group_id.to_hyphenated_string()
946 );
947 }
948 }
949 }
950 }
951 }
952 }
953
954 Ok(draw_target)
955 }
956}
957
958fn argb_to_raqote_color(color_argb: u32) -> raqote::Color {
959 let [a, r, g, b] = color_argb.to_be_bytes();
960 raqote::Color::new(a, r, g, b)
961}
962
963const PEN_TYPE_HIGHLIGHTER: u8 = 15;
967
968const HIGHLIGHTER_ALPHA: u8 = 0x80;
971
972fn apply_pen_type_alpha(color_argb: u32, pen_type: Option<u8>) -> u32 {
975 if pen_type == Some(PEN_TYPE_HIGHLIGHTER) {
976 (color_argb & 0x00FF_FFFF) | ((HIGHLIGHTER_ALPHA as u32) << 24)
977 } else {
978 color_argb
979 }
980}
981
982fn join_archive_path(prefix: &str, tail: &str) -> String {
983 if prefix.is_empty() {
984 tail.to_string()
985 } else {
986 format!("{}/{}", prefix, tail)
987 }
988}
989
990fn file_name_from_path(path: &str) -> Result<&str> {
991 path.rsplit('/')
992 .next()
993 .filter(|name| !name.is_empty())
994 .ok_or_else(|| Error::InvalidArchiveEntryName(path.to_string()))
995}
996
997fn to_container_relative_path(root_path: &str, archive_path: &str) -> String {
998 if root_path.is_empty() {
999 return archive_path.trim_start_matches('/').to_string();
1000 }
1001
1002 archive_path
1003 .trim_start_matches('/')
1004 .strip_prefix(&format!("{}/", root_path))
1005 .unwrap_or_else(|| archive_path.trim_start_matches('/'))
1006 .to_string()
1007}
1008
1009fn to_note_relative_path(note_path_prefix: &str, container_relative_path: &str) -> String {
1010 if note_path_prefix.is_empty() {
1011 return container_relative_path.to_string();
1012 }
1013
1014 container_relative_path
1015 .strip_prefix(&format!("{}/", note_path_prefix))
1016 .unwrap_or(container_relative_path)
1017 .to_string()
1018}
1019
1020fn parse_shape_group_file_name(file_name: &str) -> Result<(ShapeGroupUuid, u64)> {
1021 let mut segments = file_name.split('#');
1022 let _page_id = segments.next();
1023 let shape_group_uuid = segments.next();
1024 let timestamp_with_suffix = segments.next();
1025 let extra = segments.next();
1026
1027 if shape_group_uuid.is_none() || timestamp_with_suffix.is_none() || extra.is_some() {
1028 return Err(Error::InvalidArchiveEntryName(file_name.to_string()));
1029 }
1030
1031 let shape_group_id = ShapeGroupUuid::from_str(shape_group_uuid.unwrap())?;
1032
1033 let timestamp_with_suffix = timestamp_with_suffix.unwrap();
1034 let timestamp_str = timestamp_with_suffix
1035 .strip_suffix(".zip")
1036 .ok_or_else(|| Error::InvalidArchiveEntryName(file_name.to_string()))?;
1037 let timestamp = timestamp_str.parse::<u64>().map_err(|e| {
1038 Error::InvalidTimestampFormat(format!(
1039 "Failed to parse timestamp in '{}': {}",
1040 file_name, e
1041 ))
1042 })?;
1043
1044 Ok((shape_group_id, timestamp))
1045}
1046
1047fn parse_points_file_name(file_name: &str) -> Result<PointsUuid> {
1048 let mut segments = file_name.split('#');
1049 let _page_id = segments.next();
1050 let points_uuid = segments.next();
1051 let marker = segments.next();
1052 let extra = segments.next();
1053
1054 if points_uuid.is_none() || marker != Some("points") || extra.is_some() {
1055 return Err(Error::InvalidArchiveEntryName(file_name.to_string()));
1056 }
1057
1058 PointsUuid::from_str(points_uuid.unwrap())
1059}
1060
1061impl<R: std::io::Read + std::io::Seek> std::fmt::Debug for Page<R> {
1062 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1063 f.debug_struct("Page")
1064 .field("virtual_page", &self.virtual_page)
1065 .field("page_model", &self.page_model)
1066 .finish()
1067 }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use crate::{
1073 id::{PointsUuid, ShapeGroupUuid},
1074 parse_points_file_name, parse_shape_group_file_name,
1075 };
1076
1077 #[test]
1078 fn parses_shape_group_filename() {
1079 let file_name = "ba338e220eda49268c7126a02970a160#537164a1-9052-496a-80d3-a3aadcff339b#1753456222637.zip";
1080 let (shape_group_uuid, timestamp) = parse_shape_group_file_name(file_name).unwrap();
1081
1082 assert_eq!(
1083 shape_group_uuid,
1084 ShapeGroupUuid::from_str("537164a1-9052-496a-80d3-a3aadcff339b").unwrap()
1085 );
1086 assert_eq!(timestamp, 1753456222637);
1087 }
1088
1089 #[test]
1090 fn rejects_invalid_shape_group_filename() {
1091 let file_name =
1092 "ba338e220eda49268c7126a02970a160#537164a1-9052-496a-80d3-a3aadcff339b#1753456222637";
1093 assert!(parse_shape_group_file_name(file_name).is_err());
1094 }
1095
1096 #[test]
1097 fn parses_points_filename() {
1098 let file_name =
1099 "ba338e220eda49268c7126a02970a160#e858c829-d2f3-4994-b9fb-95bcc8003fa3#points";
1100 let points_uuid = parse_points_file_name(file_name).unwrap();
1101
1102 assert_eq!(
1103 points_uuid,
1104 PointsUuid::from_str("e858c829-d2f3-4994-b9fb-95bcc8003fa3").unwrap()
1105 );
1106 }
1107
1108 #[test]
1109 fn rejects_invalid_points_filename() {
1110 let file_name = "ba338e220eda49268c7126a02970a160#e858c829-d2f3-4994-b9fb-95bcc8003fa3";
1111 assert!(parse_points_file_name(file_name).is_err());
1112 }
1113}