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::{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_str = str::from_utf8(&buffer).map_err(|e| Error::UuidInvalidUtf8(e))?;
29        let page_id = PageUuid::from_str(page_id_str.trim())?;
30
31        // Clear buffer for the next read
32        buffer.fill(0);
33        reader.read_exact(&mut buffer)?;
34        let points_id_str = str::from_utf8(&buffer).map_err(|e| Error::UuidInvalidUtf8(e))?;
35        let points_id = PointsUuid::from_str(points_id_str)?;
36
37        Ok(Self {
38            version,
39            page_id,
40            points_id,
41        })
42    }
43}
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct PointsTableEntry {
47    pub stroke_id: StrokeUuid,
48    /// Byte offset of the first point in the file
49    pub start_addr: u32,
50    /// Number of points for this stroke (extracted from bits 31:4 of the packed field)
51    pub point_count: u32,
52    /// Lowest nibble (bits 3:0) of the packed field
53    pub flag: u8,
54}
55
56impl PointsTableEntry {
57    pub fn read(reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
58        let mut reader = reader;
59
60        let mut buffer = [0; 36];
61        reader.read_exact(&mut buffer)?;
62        let stroke_uuid_str = str::from_utf8(&buffer).map_err(|e| Error::UuidInvalidUtf8(e))?;
63        let stroke_uuid = StrokeUuid::from_str(stroke_uuid_str)?;
64
65        let start_addr = reader.read_u32::<BE>()?;
66        let packed = reader.read_u32::<BE>()?;
67
68        let point_count = (packed >> 4) & 0x0FFFFFFF; // Bits 31:4
69        let flag = (packed & 0xF) as u8; // Bits 3:0
70
71        Ok(Self {
72            stroke_id: stroke_uuid,
73            start_addr,
74            point_count,
75            flag,
76        })
77    }
78}
79
80#[derive(Debug, Clone, PartialEq)]
81pub struct Point {
82    pub timestamp_rel: u32,
83    pub x: f32,
84    pub y: f32,
85    pub tilt_x: i8,
86    pub tilt_y: i8,
87    pub pressure: u16,
88}
89
90#[derive(Debug, Clone, PartialEq)]
91pub struct Stroke {
92    pub points: Vec<Point>,
93}
94
95impl Stroke {
96    pub fn read(
97        reader: impl std::io::Read + std::io::Seek,
98        entry: &PointsTableEntry,
99    ) -> Result<Self> {
100        let mut reader = reader;
101
102        reader.seek(std::io::SeekFrom::Start(entry.start_addr as u64))?;
103
104        let mut points = Vec::with_capacity(entry.point_count as usize);
105        for _ in 0..entry.point_count {
106            let timestamp_rel = reader.read_u32::<BE>()?;
107            let x = reader.read_f32::<BE>()?;
108            let y = reader.read_f32::<BE>()?;
109            let tilt_x = reader.read_i8()?;
110            let tilt_y = reader.read_i8()?;
111            let pressure = reader.read_u16::<BE>()?;
112
113            points.push(Point {
114                timestamp_rel,
115                x,
116                y,
117                tilt_x,
118                tilt_y,
119                pressure,
120            });
121        }
122
123        Ok(Self { points })
124    }
125
126    pub fn render(
127        &self,
128        draw_target: &mut DrawTarget,
129        draw_options: &DrawOptions,
130        stroke_style: &StrokeStyle,
131    ) -> Result<()> {
132        if self.points.is_empty() {
133            log::warn!("No points to draw for stroke");
134            return Ok(());
135        }
136
137        let mut path = PathBuilder::new();
138        let mut first_point = true;
139
140        for point in &self.points {
141            if first_point {
142                path.move_to(point.x, point.y);
143                first_point = false;
144            } else {
145                path.line_to(point.x, point.y);
146            }
147        }
148
149        draw_target.stroke(
150            &path.finish(),
151            &Source::Solid(raqote::Color::new(255, 0, 0, 0).into()),
152            stroke_style,
153            draw_options,
154        );
155
156        Ok(())
157    }
158}
159
160#[derive(Debug, Clone, PartialEq)]
161pub struct PointsFile {
162    header: Header,
163    points: HashMap<StrokeUuid, Stroke>,
164}
165
166impl PointsFile {
167    pub fn read(mut reader: impl std::io::Read + std::io::Seek) -> Result<Self> {
168        let header = Header::read(&mut reader)?;
169
170        let points_table_end = reader.seek(std::io::SeekFrom::End(-4))?;
171        let points_table_start = reader.read_u32::<BE>()?;
172
173        let mut points_table = Vec::new();
174
175        reader.seek(std::io::SeekFrom::Start(points_table_start as u64))?;
176        while reader.stream_position()? < points_table_end {
177            let entry = PointsTableEntry::read(&mut reader)?;
178            points_table.push(entry);
179        }
180
181        let mut points = HashMap::new();
182        for entry in points_table {
183            let stroke = Stroke::read(&mut reader, &entry)?;
184            points.insert(entry.stroke_id, stroke);
185        }
186
187        Ok(Self { header, points })
188    }
189
190    pub fn header(&self) -> &Header {
191        &self.header
192    }
193
194    pub fn get_stroke(&self, stroke_id: &StrokeUuid) -> Option<&Stroke> {
195        self.points.get(stroke_id)
196    }
197}