Skip to main content

boox_note_parser/
shape.rs

1use zip::ZipArchive;
2
3use crate::{
4    id::{PointsUuid, ShapeGroupUuid, StrokeUuid},
5    json::Dimensions,
6    shape::json::{DisplayScale, LineStyle, LineStyleContainer},
7    utils::{convert_timestamp_to_datetime, parse_json},
8};
9
10#[derive(Debug, Clone)]
11pub struct ShapeGroup {
12    shapes: Vec<Shape>,
13}
14
15impl ShapeGroup {
16    pub fn read(mut reader: impl std::io::Read) -> crate::error::Result<Self> {
17        let mut buf = Vec::new();
18        reader.read_to_end(&mut buf)?;
19
20        let reader = std::io::Cursor::new(buf);
21
22        let mut archive = ZipArchive::new(reader)?;
23
24        let mut reader = archive.by_index(0)?;
25
26        let container = protobuf::ShapeContainer::read(&mut reader)?;
27        let shapes = container
28            .shapes
29            .iter()
30            .map(Shape::from_protobuf)
31            .collect::<crate::error::Result<_>>()?;
32        Ok(Self { shapes })
33    }
34
35    pub fn shapes(&self) -> &[Shape] {
36        &self.shapes
37    }
38}
39
40#[derive(Debug, Clone)]
41pub struct Shape {
42    pub stroke_id: StrokeUuid,
43    pub created: chrono::DateTime<chrono::Utc>,
44    pub modified: chrono::DateTime<chrono::Utc>,
45    pub unknown: i64,
46    pub stroke_width: f32,
47    pub unknown_6: u32,
48    pub bbox: Dimensions,
49    pub render_scale: DisplayScale,
50    pub z_order: i64,
51    pub unknown_15: u32,
52    pub points_id: Option<PointsUuid>,
53    pub line_style: Option<LineStyle>,
54    pub shape_group_id: ShapeGroupUuid,
55    pub points_json: String,
56}
57
58impl Shape {
59    fn from_protobuf(shape: &protobuf::Shape) -> crate::error::Result<Self> {
60        Ok(Self {
61            stroke_id: StrokeUuid::from_str(&shape.stroke_uuid)?,
62            created: convert_timestamp_to_datetime(shape.created)?,
63            modified: convert_timestamp_to_datetime(shape.modified)?,
64            unknown: shape.unknown,
65            stroke_width: shape.stroke_width,
66            unknown_6: shape.unknown_6,
67            bbox: parse_json(&shape.bbox_json)?,
68            render_scale: parse_json(&shape.render_scale_json)?,
69            z_order: shape.z_order,
70            unknown_15: shape.unknown_15,
71            points_id: if shape.points_uuid.is_empty() {
72                None
73            } else {
74                Some(PointsUuid::from_str(&shape.points_uuid)?)
75            },
76            line_style: if shape.line_style_json.is_empty() {
77                None
78            } else {
79                let line_style_container: LineStyleContainer = parse_json(&shape.line_style_json)?;
80                Some(line_style_container.line_style)
81            },
82            shape_group_id: ShapeGroupUuid::from_str(&shape.shape_group_uuid)?,
83            points_json: shape.empty_array_json.clone(),
84        })
85    }
86}
87
88mod json {
89    use serde::Deserialize;
90
91    #[derive(Debug, Clone, Deserialize)]
92    // Retained for future render-scale fidelity work even when not yet consumed.
93    #[allow(dead_code)]
94    #[serde(rename_all = "camelCase")]
95    pub struct DisplayScale {
96        display_scale: f32,
97        max_pressure: f32,
98        #[serde(default)]
99        revised_display_scale: f32,
100        source: u32,
101    }
102
103    #[derive(Debug, Clone, Deserialize)]
104    #[serde(rename_all = "camelCase")]
105    pub struct LineStyleContainer {
106        pub line_style: LineStyle,
107    }
108
109    #[derive(Debug, Clone, Deserialize)]
110    #[serde(rename_all = "camelCase")]
111    pub struct LineStyle {
112        pub phase: f32,
113        pub type_: u8,
114    }
115}
116
117mod protobuf {
118    use prost::Message;
119
120    use crate::error::Result;
121
122    #[derive(Clone, PartialEq, Message)]
123    pub struct ShapeContainer {
124        #[prost(message, repeated, tag = "1")]
125        pub shapes: Vec<Shape>,
126    }
127
128    impl ShapeContainer {
129        pub fn read(mut reader: impl std::io::Read) -> Result<Self> {
130            let mut buf = Vec::new();
131            reader.read_to_end(&mut buf)?;
132            Ok(ShapeContainer::decode(&buf[..])?)
133        }
134    }
135
136    #[derive(Clone, PartialEq, Message)]
137    pub struct Shape {
138        // Confirmed
139        #[prost(string, tag = "1")]
140        pub stroke_uuid: String,
141        // Confirmed
142        #[prost(uint64, tag = "2")]
143        pub created: u64,
144        // Confirmed
145        #[prost(uint64, tag = "3")]
146        pub modified: u64,
147        // Observed as ARGB color encoded as signed integer value
148        #[prost(int64, tag = "4")]
149        pub unknown: i64,
150        // Uncertain
151        #[prost(float, tag = "5")]
152        pub stroke_width: f32,
153        // Observed on multi-layer pages; likely layer/state metadata.
154        #[prost(uint32, tag = "6")]
155        pub unknown_6: u32,
156        // Confirmed
157        #[prost(string, tag = "7")]
158        pub bbox_json: String,
159        // Confirmed
160        #[prost(string, tag = "11")]
161        pub render_scale_json: String,
162        // Observed as pen/style type key (non-zigzag integer)
163        #[prost(int64, tag = "12")]
164        pub z_order: i64,
165        // Observed on entries missing points UUID; likely tombstone/state marker.
166        #[prost(uint32, tag = "15")]
167        pub unknown_15: u32,
168        // Confirmed
169        #[prost(string, tag = "16")]
170        pub points_uuid: String,
171        // Confirmed
172        #[prost(string, tag = "17")]
173        pub line_style_json: String,
174        // Confirmed
175        #[prost(string, tag = "18")]
176        pub shape_group_uuid: String,
177        // Uncertain
178        #[prost(string, tag = "21")]
179        pub empty_array_json: String,
180    }
181}