Skip to main content

boox_note_parser/
points.rs

1use std::collections::HashMap;
2
3use byteorder::{BE, ReadBytesExt};
4use raqote::{DrawOptions, DrawTarget, PathBuilder, Source, StrokeStyle};
5
6use crate::{
7    error::Result,
8    id::{PageUuid, PointsUuid, StrokeUuid},
9};
10
11#[derive(Debug, Clone, PartialEq)]
12pub struct Header {
13    pub version: u32,
14    pub page_id: PageUuid,
15    pub points_id: PointsUuid,
16}
17
18impl Header {
19    pub fn read(reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
20        let mut reader = reader;
21        reader.seek(std::io::SeekFrom::Start(0))?;
22
23        let version = reader.read_u32::<BE>()?;
24
25        let mut buffer = [0; 36];
26
27        reader.read_exact(&mut buffer)?;
28        let page_id = PageUuid::from_byte_str(&buffer)?;
29
30        // Clear buffer for the next read
31        buffer.fill(0);
32        reader.read_exact(&mut buffer)?;
33        let points_id = PointsUuid::from_byte_str(&buffer)?;
34
35        Ok(Self {
36            version,
37            page_id,
38            points_id,
39        })
40    }
41}
42
43#[derive(Debug, Clone, PartialEq)]
44pub struct PointsTableEntry {
45    pub stroke_id: StrokeUuid,
46    /// Byte offset of the first point in the file
47    pub start_addr: u32,
48    /// Number of points for this stroke (extracted from bits 31:4 of the packed field)
49    pub point_count: u32,
50    /// Lowest nibble (bits 3:0) of the packed field
51    pub flag: u8,
52}
53
54impl PointsTableEntry {
55    pub fn read(reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
56        let mut reader = reader;
57
58        let mut buffer = [0; 36];
59        reader.read_exact(&mut buffer)?;
60        let stroke_uuid = StrokeUuid::from_byte_str(&buffer)?;
61
62        let start_addr = reader.read_u32::<BE>()?;
63        let packed = reader.read_u32::<BE>()?;
64
65        let point_count = (packed >> 4) & 0x0FFFFFFF; // Bits 31:4
66        let flag = (packed & 0xF) as u8; // Bits 3:0
67
68        Ok(Self {
69            stroke_id: stroke_uuid,
70            start_addr,
71            point_count,
72            flag,
73        })
74    }
75}
76
77#[derive(Debug, Clone, PartialEq)]
78pub struct Point {
79    pub timestamp_rel: u32,
80    pub x: f32,
81    pub y: f32,
82    pub tilt_x: i8,
83    pub tilt_y: i8,
84    pub pressure: u16,
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub struct Stroke {
89    pub points: Vec<Point>,
90}
91
92impl Stroke {
93    pub fn read(
94        reader: impl std::io::Read + std::io::Seek,
95        entry: &PointsTableEntry,
96    ) -> Result<Self> {
97        let mut reader = reader;
98
99        reader.seek(std::io::SeekFrom::Start(entry.start_addr as u64))?;
100
101        let mut points = Vec::with_capacity(entry.point_count as usize);
102        for _ in 0..entry.point_count {
103            let timestamp_rel = reader.read_u32::<BE>()?;
104            let x = reader.read_f32::<BE>()?;
105            let y = reader.read_f32::<BE>()?;
106            let tilt_x = reader.read_i8()?;
107            let tilt_y = reader.read_i8()?;
108            let pressure = reader.read_u16::<BE>()?;
109
110            points.push(Point {
111                timestamp_rel,
112                x,
113                y,
114                tilt_x,
115                tilt_y,
116                pressure,
117            });
118        }
119
120        Ok(Self { points })
121    }
122
123    pub fn render(
124        &self,
125        draw_target: &mut DrawTarget,
126        draw_options: &DrawOptions,
127        stroke_style: &StrokeStyle,
128    ) -> Result<()> {
129        self.render_with_color(
130            draw_target,
131            draw_options,
132            stroke_style,
133            raqote::Color::new(255, 0, 0, 0),
134        )
135    }
136
137    pub fn render_with_color(
138        &self,
139        draw_target: &mut DrawTarget,
140        draw_options: &DrawOptions,
141        stroke_style: &StrokeStyle,
142        color: raqote::Color,
143    ) -> Result<()> {
144        if self.points.is_empty() {
145            log::warn!("No points to draw for stroke");
146            return Ok(());
147        }
148
149        let mut path = PathBuilder::new();
150        let mut first_point = true;
151
152        for point in &self.points {
153            if first_point {
154                path.move_to(point.x, point.y);
155                first_point = false;
156            } else {
157                path.line_to(point.x, point.y);
158            }
159        }
160
161        draw_target.stroke(
162            &path.finish(),
163            &Source::Solid(color.into()),
164            stroke_style,
165            draw_options,
166        );
167
168        Ok(())
169    }
170}
171
172#[derive(Debug, Clone, PartialEq)]
173pub struct PointsFile {
174    header: Header,
175    points: HashMap<StrokeUuid, Stroke>,
176}
177
178impl PointsFile {
179    pub fn read(mut reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
180        let header = Header::read(&mut reader)?;
181
182        let points_table_end = reader.seek(std::io::SeekFrom::End(-4))?;
183        let points_table_start = reader.read_u32::<BE>()?;
184
185        let mut points_table = Vec::new();
186
187        reader.seek(std::io::SeekFrom::Start(points_table_start as u64))?;
188        while reader.stream_position()? < points_table_end {
189            let entry = PointsTableEntry::read(&mut reader)?;
190            points_table.push(entry);
191        }
192
193        let mut points = HashMap::new();
194        for entry in points_table {
195            let stroke = Stroke::read(&mut reader, &entry)?;
196            points.insert(entry.stroke_id, stroke);
197        }
198
199        Ok(Self { header, points })
200    }
201
202    pub fn header(&self) -> &Header {
203        &self.header
204    }
205
206    pub fn get_stroke(&self, stroke_id: &StrokeUuid) -> Option<&Stroke> {
207        self.points.get(stroke_id)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use byteorder::{BE, WriteBytesExt};
214
215    use super::{Header, PointsTableEntry};
216    use crate::id::{PageUuid, PointsUuid, StrokeUuid};
217
218    #[test]
219    fn header_parses_padded_uuid_fields() {
220        let mut bytes = Vec::new();
221        bytes.write_u32::<BE>(1).unwrap();
222        bytes.extend_from_slice(b"ba338e220eda49268c7126a02970a160    ");
223        bytes.extend_from_slice(b"e858c829-d2f3-4994-b9fb-95bcc8003fa3");
224
225        let header = Header::read(std::io::Cursor::new(bytes)).unwrap();
226
227        assert_eq!(header.version, 1);
228        assert_eq!(
229            header.page_id,
230            PageUuid::from_str("ba338e220eda49268c7126a02970a160").unwrap()
231        );
232        assert_eq!(
233            header.points_id,
234            PointsUuid::from_str("e858c829-d2f3-4994-b9fb-95bcc8003fa3").unwrap()
235        );
236    }
237
238    #[test]
239    fn points_table_entry_parses_padded_stroke_id() {
240        let mut bytes = Vec::new();
241        bytes.extend_from_slice(b"92c1ab734ec14f70907adc11dcb0806d    ");
242        bytes.write_u32::<BE>(64).unwrap();
243        bytes.write_u32::<BE>((3 << 4) | 5).unwrap();
244
245        let entry = PointsTableEntry::read(std::io::Cursor::new(bytes)).unwrap();
246
247        assert_eq!(
248            entry.stroke_id,
249            StrokeUuid::from_str("92c1ab73-4ec1-4f70-907a-dc11dcb0806d").unwrap()
250        );
251        assert_eq!(entry.start_addr, 64);
252        assert_eq!(entry.point_count, 3);
253        assert_eq!(entry.flag, 5);
254    }
255}