onenote_parser/onenote/
ink.rs

1use crate::errors::{ErrorKind, Result};
2use crate::fsshttpb::data::exguid::ExGuid;
3use crate::one::property_set::{
4    ink_container, ink_data_node, ink_stroke_node, stroke_properties_node,
5};
6use crate::onestore::object_space::ObjectSpace;
7
8/// An ink object.
9#[derive(Clone, Debug)]
10pub struct Ink {
11    pub(crate) ink_strokes: Vec<InkStroke>,
12    pub(crate) bounding_box: Option<InkBoundingBox>,
13
14    pub(crate) offset_horizontal: Option<f32>,
15    pub(crate) offset_vertical: Option<f32>,
16}
17
18impl Ink {
19    /// The ink strokes contained in this ink object.
20    pub fn ink_strokes(&self) -> &[InkStroke] {
21        &self.ink_strokes
22    }
23
24    /// The ink object's bounding box.
25    pub fn bounding_box(&self) -> Option<InkBoundingBox> {
26        self.bounding_box
27    }
28
29    /// The horizontal offset from the page origin in half-inch increments.
30    ///
31    /// See [\[MS-ONE\] 2.3.18].
32    ///
33    /// [\[MS-ONE\] 2.3.18]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/5fb9e84a-c9e9-4537-ab14-e5512f24669a
34    pub fn offset_horizontal(&self) -> Option<f32> {
35        self.offset_horizontal
36    }
37
38    /// The vertical offset from the page origin in half-inch increments.
39    ///
40    /// See [\[MS-ONE\] 2.3.19].
41    ///
42    /// [\[MS-ONE\] 2.3.19]: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-one/5c4992ba-1db5-43e9-83dd-7299c562104d
43    pub fn offset_vertical(&self) -> Option<f32> {
44        self.offset_vertical
45    }
46}
47
48/// An ink stroke.
49#[derive(Clone, Debug)]
50pub struct InkStroke {
51    pub(crate) path: Vec<InkPoint>,
52    pub(crate) pen_tip: Option<u8>,
53    pub(crate) transparency: Option<u8>,
54    pub(crate) height: f32,
55    pub(crate) width: f32,
56    pub(crate) color: Option<u32>,
57}
58
59impl InkStroke {
60    /// The ink stroke's path.
61    pub fn path(&self) -> &[InkPoint] {
62        &self.path
63    }
64
65    /// The pen tip used for the ink path.
66    ///
67    /// The exact meaning is not specified.
68    pub fn pen_tip(&self) -> Option<u8> {
69        self.pen_tip
70    }
71
72    /// The path's transparency
73    ///
74    /// The exact meaning is not specified. It seems like 0 means translucent and 255 means opaque.
75    pub fn transparency(&self) -> Option<u8> {
76        self.transparency
77    }
78
79    /// The ink stroke's total height.
80    pub fn height(&self) -> f32 {
81        self.height
82    }
83
84    /// The ink stroke's total width.
85    ///
86    /// The exact meaning is not specified.
87    pub fn width(&self) -> f32 {
88        self.width
89    }
90
91    /// The ink stroke's color.
92    ///
93    /// The exact meaning is not specified.
94    pub fn color(&self) -> Option<u32> {
95        self.color
96    }
97}
98
99/// A point in an ink path.
100#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
101pub struct InkPoint {
102    pub(crate) x: f32,
103    pub(crate) y: f32,
104}
105
106impl InkPoint {
107    /// The point's X coordinates.
108    pub fn x(&self) -> f32 {
109        self.x
110    }
111
112    /// The point's Y coordinates.
113    pub fn y(&self) -> f32 {
114        self.y
115    }
116}
117
118/// The bounding box of an ink object.
119#[derive(Clone, Copy, Debug)]
120pub struct InkBoundingBox {
121    pub(crate) x: f32,
122    pub(crate) y: f32,
123    pub(crate) height: f32,
124    pub(crate) width: f32,
125}
126
127impl InkBoundingBox {
128    /// The initial X coordinate of the bounding box.
129    ///
130    /// The exact meaning and unit are not specified.
131    pub fn x(&self) -> f32 {
132        self.x
133    }
134
135    /// The initial Y coordinate of the bounding box.
136    ///
137    /// The exact meaning and unit are not specified.
138    pub fn y(&self) -> f32 {
139        self.y
140    }
141
142    /// The height of the bounding box.
143    ///
144    /// The exact meaning and unit are not specified.
145    pub fn height(&self) -> f32 {
146        self.height
147    }
148
149    /// The width of the bounding box.
150    ///
151    /// The exact meaning and unit are not specified.
152    pub fn width(&self) -> f32 {
153        self.width
154    }
155
156    /// Scale the bounding box by a constant factor.
157    pub fn scale(&self, factor: f32) -> InkBoundingBox {
158        InkBoundingBox {
159            x: self.x * factor,
160            y: self.y * factor,
161            height: self.height * factor,
162            width: self.width * factor,
163        }
164    }
165}
166
167pub(crate) fn parse_ink(ink_container_id: ExGuid, space: &ObjectSpace) -> Result<Ink> {
168    let container_object = space
169        .get_object(ink_container_id)
170        .ok_or_else(|| ErrorKind::MalformedOneNoteData("ink container is missing".into()))?;
171    let container = ink_container::parse(container_object)?;
172
173    let ink_data_id = match container.ink_data {
174        Some(id) => id,
175        None => {
176            return Ok(Ink {
177                ink_strokes: vec![],
178                bounding_box: None,
179                offset_horizontal: container.offset_from_parent_horiz,
180                offset_vertical: container.offset_from_parent_vert,
181            });
182        }
183    };
184
185    let (ink_strokes, bounding_box) = parse_ink_data(
186        ink_data_id,
187        space,
188        container.ink_scaling_x,
189        container.ink_scaling_y,
190    )?;
191
192    Ok(Ink {
193        ink_strokes,
194        bounding_box,
195        offset_horizontal: container.offset_from_parent_horiz,
196        offset_vertical: container.offset_from_parent_vert,
197    })
198}
199
200pub(crate) fn parse_ink_data(
201    ink_data_id: ExGuid,
202    space: &ObjectSpace,
203    scale_x: Option<f32>,
204    scale_y: Option<f32>,
205) -> Result<(Vec<InkStroke>, Option<InkBoundingBox>)> {
206    let ink_data_object = space
207        .get_object(ink_data_id)
208        .ok_or_else(|| ErrorKind::MalformedOneNoteData("ink data node is missing".into()))?;
209    let ink_data = ink_data_node::parse(ink_data_object)?;
210
211    let strokes = ink_data
212        .strokes
213        .iter()
214        .copied()
215        .map(|ink_stroke_id| parse_ink_stroke(ink_stroke_id, space, scale_x, scale_y))
216        .collect::<Result<_>>()?;
217
218    let scale_x = scale_x.unwrap_or(1.0);
219    let scale_y = scale_y.unwrap_or(1.0);
220
221    let bounding_box = ink_data
222        .bounding_box
223        .map(|[x_min, y_min, x_max, y_max]| InkBoundingBox {
224            x: x_min as f32 * scale_x,
225            y: y_min as f32 * scale_y,
226            width: (x_max as f32 - x_min as f32) * scale_x,
227            height: (y_max as f32 - y_min as f32) * scale_y,
228        });
229
230    Ok((strokes, bounding_box))
231}
232
233fn parse_ink_stroke(
234    ink_stroke_id: ExGuid,
235    space: &ObjectSpace,
236    scale_x: Option<f32>,
237    scale_y: Option<f32>,
238) -> Result<InkStroke> {
239    let object = space
240        .get_object(ink_stroke_id)
241        .ok_or_else(|| ErrorKind::MalformedOneNoteData("ink stroke node is missing".into()))?;
242    let data = ink_stroke_node::parse(object)?;
243
244    let props_object = space.get_object(data.properties).ok_or_else(|| {
245        ErrorKind::MalformedOneNoteData("ink stroke properties node is missing".into())
246    })?;
247    let props = stroke_properties_node::parse(props_object)?;
248
249    let path = parse_ink_path(data.path, &props, scale_x, scale_y)?;
250
251    Ok(InkStroke {
252        path,
253        pen_tip: props.pen_tip,
254        transparency: props.transparency,
255        height: props.ink_height,
256        width: props.ink_width,
257        color: props.color,
258    })
259}
260
261fn parse_ink_path(
262    data: Vec<i64>,
263    props: &stroke_properties_node::Data,
264    scale_x: Option<f32>,
265    scale_y: Option<f32>,
266) -> Result<Vec<InkPoint>> {
267    // Find dimension indexes
268    let idx_x = props
269        .dimensions
270        .iter()
271        .position(|d| d.id == guid!("598a6a8f-52c0-4ba0-93af-af357411a561"))
272        .ok_or_else(|| {
273            ErrorKind::MalformedOneNoteData("ink stroke properties has no x dimension".into())
274        })?;
275    let idx_y = props
276        .dimensions
277        .iter()
278        .position(|d| d.id == guid!("b53f9f75-04e0-4498-a7ee-c30dbb5a9011"))
279        .ok_or_else(|| {
280            ErrorKind::MalformedOneNoteData("ink stroke properties has no y dimension".into())
281        })?;
282
283    // Find dimensions data
284    let dimension_offset = data.len() / props.dimensions.len();
285
286    let start_x = dimension_offset * idx_x;
287    let start_y = dimension_offset * idx_y;
288
289    let x = &data[start_x..start_x + dimension_offset];
290    let y = &data[start_y..start_y + dimension_offset];
291
292    let scale_x = scale_x.unwrap_or(1.0);
293    let scale_y = scale_y.unwrap_or(1.0);
294
295    let path = x
296        .iter()
297        .copied()
298        .zip(y.iter().copied())
299        .map(|(x, y)| InkPoint {
300            x: scale_x * x as f32,
301            y: scale_y * y as f32,
302        })
303        .collect();
304
305    Ok(path)
306}