boox_note_parser/
lib.rs

1use std::{collections::HashMap, io::Read};
2
3use raqote::{DrawOptions, DrawTarget, 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 id;
25pub mod points;
26pub mod shape;
27pub mod virtual_page;
28
29pub struct NoteFile<R: std::io::Read + std::io::Seek> {
30    container: container::Container<R>,
31    note_tree: NoteTree,
32}
33
34impl<R: std::io::Read + std::io::Seek> NoteFile<R> {
35    pub fn read(reader: R) -> Result<Self> {
36        let mut container = container::Container::open(reader).expect("Failed to open container");
37
38        let note_tree = if *container.container_type() == container::ContainerType::MultiNote {
39            container.get_file_relative("note_tree", |reader| NoteTree::read(reader))?
40        } else {
41            container.get_file_relative(
42                &format!("{}/note/pb/note_info", container.root_path()),
43                |reader| NoteTree::read(reader),
44            )?
45        };
46
47        Ok(Self {
48            container,
49            note_tree,
50        })
51    }
52
53    pub fn list_notes(&self) -> HashMap<NoteUuid, String> {
54        self.note_tree
55            .notes
56            .iter()
57            .map(|(id, metadata)| (*id, metadata.name.clone()))
58            .collect()
59    }
60
61    pub fn get_note(&self, note_id: &NoteUuid) -> Option<Note<R>> {
62        self.note_tree
63            .notes
64            .get(note_id)
65            .map(|metadata| Note::new(self.container.clone(), metadata.clone()))
66    }
67}
68
69impl<R: std::io::Read + std::io::Seek> std::fmt::Debug for NoteFile<R> {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("NoteFile")
72            .field("container_type", &self.container.container_type())
73            .field("note_tree", &self.note_tree)
74            .finish()
75    }
76}
77
78pub struct Note<R: std::io::Read + std::io::Seek> {
79    container: container::Container<R>,
80    metadata: NoteMetadata,
81    virtual_doc: Option<VirtualDoc>,
82    virtual_pages: Option<HashMap<VirtualPageUuid, VirtualPage>>,
83    page_models: Option<HashMap<PageModelUuid, PageModelGroup>>,
84}
85
86impl<R: std::io::Read + std::io::Seek> Note<R> {
87    fn new(container: container::Container<R>, metadata: NoteMetadata) -> Self {
88        Self {
89            container,
90            metadata,
91            virtual_doc: None,
92            virtual_pages: None,
93            page_models: None,
94        }
95    }
96
97    pub fn name(&self) -> &str {
98        &self.metadata.name
99    }
100
101    pub fn active_pages(&self) -> &[PageUuid] {
102        &self.metadata.active_pages
103    }
104
105    pub fn reserved_pages(&self) -> &[PageUuid] {
106        &self.metadata.reserved_pages
107    }
108
109    pub fn detached_pages(&self) -> &[PageUuid] {
110        &self.metadata.detached_pages
111    }
112
113    pub fn created(&self) -> chrono::DateTime<chrono::Utc> {
114        self.metadata.created
115    }
116
117    pub fn modified(&self) -> chrono::DateTime<chrono::Utc> {
118        self.metadata.modified
119    }
120
121    pub fn flag(&self) -> u32 {
122        self.metadata.flag
123    }
124
125    pub fn pen_width(&self) -> f32 {
126        self.metadata.pen_width
127    }
128
129    pub fn scale_factor(&self) -> f32 {
130        self.metadata.scale_factor
131    }
132
133    pub fn fill_color(&self) -> &u32 {
134        &self.metadata.fill_color
135    }
136
137    pub fn pen_type(&self) -> &u32 {
138        &self.metadata.pen_type
139    }
140
141    pub fn pen_settings_fill_color(&self) -> &u32 {
142        &self.metadata.pen_settings.fill_color
143    }
144
145    pub fn pen_settings_graphics_shape_color(&self) -> &u32 {
146        &self.metadata.pen_settings.graphics_shape_color
147    }
148
149    pub fn get_page(&mut self, page_id: &PageUuid) -> Option<Page<R>> {
150        let virtual_page = {
151            let virtual_pages = self
152                .virtual_pages()
153                .inspect_err(|_| {
154                    log::error!("Failed to get virtual pages for page ID: {}", page_id);
155                })
156                .ok()?;
157
158            virtual_pages
159                .values()
160                .find(|vp| &vp.page_id == page_id)
161                .cloned()
162        };
163
164        let page_model = {
165            let page_models = self
166                .page_models()
167                .inspect_err(|_| {
168                    log::error!("Failed to get page models for page ID: {}", page_id);
169                })
170                .ok()?;
171
172            page_models
173                .values()
174                .find_map(|pm| pm.page_models.iter().find(|p| p.page_id == *page_id))?
175                .clone()
176        };
177
178        Some(Page::new(
179            self.container.clone(),
180            page_id.clone(),
181            self.metadata.note_id.clone(),
182            virtual_page,
183            page_model,
184        ))
185    }
186
187    pub fn virtual_doc(&mut self) -> Result<&VirtualDoc> {
188        if self.virtual_doc.is_none() {
189            let note_id = self.metadata.note_id.to_simple_string();
190            let virtual_doc = self.container.get_file_relative(
191                &format!("{}/virtual/doc/pb/{}", note_id, note_id),
192                |reader| VirtualDoc::read(reader),
193            )?;
194            self.virtual_doc = Some(virtual_doc);
195        }
196        Ok(self.virtual_doc.as_ref().unwrap())
197    }
198
199    pub fn virtual_pages(&mut self) -> Result<&HashMap<VirtualPageUuid, VirtualPage>> {
200        if self.virtual_pages.is_none() {
201            let note_id = self.metadata.note_id.to_simple_string();
202
203            let mut virtual_pages = HashMap::new();
204
205            for virtual_page_path in self
206                .container
207                .list_directory(&format!("{}/virtual/page/pb", note_id))
208            {
209                let virtual_page_id =
210                    VirtualPageUuid::from_str(&virtual_page_path.rsplit('/').next().unwrap())?;
211                let virtual_page = self
212                    .container
213                    .get_file_absolute(&virtual_page_path, |reader| VirtualPage::read(reader))?;
214                virtual_pages.insert(virtual_page_id, virtual_page);
215            }
216            self.virtual_pages = Some(virtual_pages);
217        }
218        Ok(self.virtual_pages.as_ref().unwrap())
219    }
220
221    pub fn page_models(&mut self) -> Result<&HashMap<PageModelUuid, PageModelGroup>> {
222        if self.page_models.is_none() {
223            let note_id = self.metadata.note_id.to_simple_string();
224
225            let mut page_models = HashMap::new();
226
227            for page_model_path in self
228                .container
229                .list_directory(&format!("{}/pageModel/pb", note_id))
230            {
231                let page_model_id =
232                    PageModelUuid::from_str(&page_model_path.rsplit('/').next().unwrap())?;
233                let page_model = self
234                    .container
235                    .get_file_absolute(&page_model_path, |reader| PageModelGroup::read(reader))?;
236                page_models.insert(page_model_id, page_model);
237            }
238            self.page_models = Some(page_models);
239        }
240        Ok(self.page_models.as_ref().unwrap())
241    }
242}
243
244impl<R: std::io::Read + std::io::Seek> std::fmt::Debug for Note<R> {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        f.debug_struct("Note")
247            .field("metadata", &self.metadata)
248            .finish()
249    }
250}
251
252pub struct Page<R: std::io::Read + std::io::Seek> {
253    container: container::Container<R>,
254    note_id: NoteUuid,
255    page_id: PageUuid,
256    virtual_page: Option<VirtualPage>,
257    page_model: PageModel,
258    shape_groups: Option<HashMap<ShapeGroupUuid, ShapeGroup>>,
259    points_files: Option<HashMap<PointsUuid, Vec<points::PointsFile>>>,
260}
261
262impl<R: std::io::Read + std::io::Seek> Page<R> {
263    fn new(
264        container: container::Container<R>,
265        page_id: PageUuid,
266        note_id: NoteUuid,
267        virtual_page: Option<VirtualPage>,
268        page_model: PageModel,
269    ) -> Self {
270        Self {
271            container,
272            page_id,
273            note_id,
274            virtual_page,
275            page_model,
276            shape_groups: None,
277            points_files: None,
278        }
279    }
280
281    pub fn virtual_page(&self) -> &Option<VirtualPage> {
282        &self.virtual_page
283    }
284
285    pub fn page_model(&self) -> &PageModel {
286        &self.page_model
287    }
288
289    pub fn shape_groups(&mut self) -> Result<&HashMap<ShapeGroupUuid, ShapeGroup>> {
290        if self.shape_groups.is_none() {
291            let note_id = self.note_id.to_simple_string();
292            let page_id = self.page_id.to_simple_string();
293
294            let mut shape_groups = HashMap::new();
295
296            for shape_group_path in self
297                .container
298                .list_directory(&format!("{}/shape/{}#", note_id, page_id))
299            {
300                let path_tail = shape_group_path.rsplit('/').next().unwrap();
301                let parts = path_tail.split('#').collect::<Vec<_>>();
302                let shape_group_id = ShapeGroupUuid::from_str(parts[1])?;
303                let _timestamp = convert_timestamp_to_datetime(
304                    parts[2].replace(".zip", "").parse::<u64>().map_err(|e| {
305                        Error::InvalidTimestampFormat(format!("Failed to parse timestamp: {}", e))
306                    })?,
307                );
308                let shape_group = self
309                    .container
310                    .get_file_absolute(&shape_group_path, |reader| ShapeGroup::read(reader))?;
311                shape_groups.insert(shape_group_id, shape_group);
312            }
313            self.shape_groups = Some(shape_groups);
314        }
315        Ok(self.shape_groups.as_ref().unwrap())
316    }
317
318    pub fn points_files(&mut self) -> Result<&HashMap<PointsUuid, Vec<points::PointsFile>>> {
319        if self.points_files.is_none() {
320            let note_id = self.note_id.to_simple_string();
321            let page_id = self.page_id.to_simple_string();
322
323            let mut points_files = HashMap::new();
324
325            for stroke_path in self
326                .container
327                .list_directory(&format!("{}/point/{}/{}#", note_id, page_id, page_id))
328            {
329                let path_tail = stroke_path.rsplit('/').next().unwrap();
330                let parts = path_tail.split('#').collect::<Vec<_>>();
331                let shape_id = PointsUuid::from_str(parts[1])?;
332
333                let file_data = self
334                    .container
335                    .get_file_absolute(&stroke_path, |mut reader| {
336                        let mut buffer = Vec::new();
337                        reader.read_to_end(&mut buffer).map_err(Error::Io)?;
338                        Ok(buffer)
339                    })?;
340
341                let buffer_cursor = std::io::Cursor::new(file_data);
342                let points_file = points::PointsFile::read(buffer_cursor)?;
343
344                points_files
345                    .entry(shape_id)
346                    .or_insert_with(Vec::new)
347                    .push(points_file);
348            }
349            self.points_files = Some(points_files);
350        }
351        Ok(self.points_files.as_ref().unwrap())
352    }
353
354    pub fn render(&mut self) -> Result<DrawTarget> {
355        let page_id = self.page_id.to_hyphenated_string();
356        let width = self.page_model.dimensions.right - self.page_model.dimensions.left;
357        let height = self.page_model.dimensions.bottom - self.page_model.dimensions.top;
358        let mut draw_target = DrawTarget::new(width as i32, height as i32);
359        let draw_options = DrawOptions::new();
360
361        draw_target.fill_rect(
362            0.0,
363            0.0,
364            width,
365            height,
366            &Source::Solid(raqote::Color::new(255, 255, 255, 255).into()),
367            &DrawOptions::new(),
368        );
369
370        // Extract shape_groups and points_files into local variables to avoid multiple mutable borrows.
371        let shape_groups = {
372            let sg = self.shape_groups().inspect_err(|_| {
373                log::error!("Failed to get shape groups for page ID: {}", page_id)
374            })?;
375            sg.clone()
376        };
377
378        let points_files_vec = {
379            let pf = self.points_files().inspect_err(|_| {
380                log::error!("Failed to get points files for page ID: {}", page_id)
381            })?;
382            pf.values().flatten().collect::<Vec<_>>()
383        };
384
385        for (shape_group_id, shape_group) in &shape_groups {
386            let mut shapes = shape_group.shapes().to_vec();
387            shapes.sort_by(|a, b| a.z_order.cmp(&b.z_order));
388
389            for shape in shapes {
390                if let Some(points_id) = shape.points_id {
391                    if let Some(points) = points_files_vec
392                        .iter()
393                        .find(|pf| pf.header().points_id == points_id)
394                    {
395                        points
396                            .get_stroke(&shape.stroke_id)
397                            .ok_or_else(|| {
398                                log::error!("Failed to get stroke for shape");
399                                Error::StrokeNotFound
400                            })
401                            .and_then(|stroke| {
402                                log::debug!("Rendering stroke for shape");
403                                log::debug!(
404                                    "Shape Group ID: {}, Stroke ID: {}",
405                                    shape_group_id.to_hyphenated_string(),
406                                    shape.stroke_id.to_hyphenated_string()
407                                );
408                                log::debug!("Shape: {:#x?}", shape);
409                                stroke.render(
410                                    &mut draw_target,
411                                    &draw_options,
412                                    &StrokeStyle::default(),
413                                )
414                            })?;
415                    } else {
416                        log::warn!(
417                            "No points files found for shape group: {}",
418                            shape_group_id.to_hyphenated_string()
419                        );
420                    }
421                }
422            }
423        }
424
425        Ok(draw_target)
426    }
427}
428
429impl<R: std::io::Read + std::io::Seek> std::fmt::Debug for Page<R> {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        f.debug_struct("Page")
432            .field("virtual_page", &self.virtual_page)
433            .field("page_model", &self.page_model)
434            .finish()
435    }
436}