1//! Ink drawings and handwriting: strokes as page-coordinate polylines in points, decoded from
2//! the stroke packets OneNote stores (ISF multi-byte first differences in HIMETRIC).
3
4use super::Error;
5use crate::{
6 ExGuid,
7 document::{Kind, Layout, Revision},
8};
9
10/// Points from a HIMETRIC coordinate, the one conversion every stroke coordinate goes through.
11fn points(himetric: i64, scale: f32) -> f32 {
12 himetric as f32 * scale * 72.0 / 2540.0
13}
14
15/// Rounds a page point to the HIMETRIC grid OneNote stores, so a stroke written from it reads
16/// back equal.
17pub fn snap(value: f32) -> f32 {
18 points((value * 2540.0 / 72.0).round() as i64, 1.0)
19}
20const DIMENSION_X: [u8; 16] = [
21 0x8f, 0x6a, 0x8a, 0x59, 0xc0, 0x52, 0xa0, 0x4b, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61,
22];
23const DIMENSION_Y: [u8; 16] = [
24 0x75, 0x9f, 0x3f, 0xb5, 0xe0, 0x04, 0x98, 0x44, 0xa7, 0xee, 0xc3, 0x0d, 0xbb, 0x5a, 0x90, 0x11,
25];
26/// The highest pressure level new strokes store, as a 1024-level pen reports it.
27const PRESSURE_LEVELS: f32 = 1023.0;
28
29/// Rounds a pressure to the level a stroke stores, so a stroke written from it reads back
30/// equal.
31pub fn level(pressure: f32) -> f32 {
32 (pressure.clamp(0.0, 1.0) * PRESSURE_LEVELS).round() / PRESSURE_LEVELS
33}
34
35#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
36pub struct Ink {
37 pub id: ExGuid,
38 /// A drawing's `x` and `y` offset every stroke point, as OneNote moves ink.
39 pub layout: Layout,
40 pub strokes: Vec<InkStroke>,
41 /// Nested ink containers, as newer OneNote versions group handwriting.
42 pub groups: Vec<Ink>,
43 /// The shape drawn, for a drawing made from Draw's Insert Shapes.
44 #[serde(default)]
45 pub shape: Option<InkShape>,
46}
47
48/// The shapes of Draw's Insert Shapes that Snowbound draws.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum ShapeKind {
51 Line,
52 Arrow,
53 Rectangle,
54 Ellipse,
55}
56
57impl Ink {
58 /// A shape dragged from `from` to `to` in page points, drawn with `pen`'s width and
59 /// colour as OneNote 2010 draws it (`corpus/ink-tools`): a rectangle clockwise from its
60 /// top left corner, an ellipse in 50 steps clockwise from its right end, and an arrow's
61 /// head as a second stroke of two barbs at atan(1/2) to the line, 8 points plus twice
62 /// the pen's width long. The result reads back unchanged once stored.
63 pub fn drawn(
64 kind: ShapeKind,
65 from: [f32; 2],
66 to: [f32; 2],
67 pen: &InkStroke,
68 ) -> Result<Self, Error> {
69 let [x0, y0] = [from[0].min(to[0]), from[1].min(to[1])];
70 let [x1, y1] = [from[0].max(to[0]), from[1].max(to[1])];
71 let (shape, paths) = match kind {
72 ShapeKind::Line | ShapeKind::Arrow => {
73 let mut paths = vec![vec![from, to]];
74 let [dx, dy] = [to[0] - from[0], to[1] - from[1]];
75 if kind == ShapeKind::Arrow && dx.hypot(dy) > 0.0 {
76 let back = (-dy).atan2(-dx);
77 let length = 8.0 + 2.0 * pen.width;
78 let barb = |turn: f32| {
79 let angle = back + turn;
80 [to[0] + length * angle.cos(), to[1] + length * angle.sin()]
81 };
82 let spread = 0.5f32.atan();
83 paths.push(vec![barb(spread), to, barb(-spread)]);
84 }
85 (InkShape::Line([from, to]), paths)
86 }
87 ShapeKind::Rectangle | ShapeKind::Ellipse => {
88 let [w, h] = [x1 - x0, y1 - y0];
89 let transform = [w, 0.0, 0.0, h, x0, y0];
90 let (anchors, path): (&[[f32; 2]], Vec<[f32; 2]>) = if kind == ShapeKind::Rectangle
91 {
92 (
93 &[
94 [0.0, 0.0],
95 [0.5, 0.0],
96 [1.0, 0.0],
97 [1.0, 0.5],
98 [1.0, 1.0],
99 [0.5, 1.0],
100 [0.0, 1.0],
101 [0.0, 0.5],
102 ],
103 vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]],
104 )
105 } else {
106 let [cx, cy, rx, ry] = [x0 + w / 2.0, y0 + h / 2.0, w / 2.0, h / 2.0];
107 (
108 &[[0.5, 0.0], [1.0, 0.5], [0.5, 1.0], [0.0, 0.5]],
109 (0..=50)
110 .map(|step| {
111 let angle = step as f32 * std::f32::consts::TAU / 50.0;
112 [cx + rx * angle.cos(), cy + ry * angle.sin()]
113 })
114 .collect(),
115 )
116 };
117 (
118 InkShape::Closed {
119 transform,
120 anchors: anchors.to_vec(),
121 },
122 vec![path],
123 )
124 }
125 };
126 let mut strokes = Vec::new();
127 for path in paths {
128 strokes.push(InkStroke {
129 id: super::text::new_id()?,
130 points: path.into_iter().map(|p| p.map(snap)).collect(),
131 pressure: Vec::new(),
132 ..pen.clone()
133 });
134 }
135 Ok(Self {
136 id: super::text::new_id()?,
137 layout: Layout::default(),
138 strokes,
139 groups: Vec::new(),
140 shape: Some(shape.snapped()),
141 })
142 }
143}
144
145/// What OneNote keeps beside a drawn shape's strokes to edit it by (`corpus/ink-tools`), in
146/// page points before the drawing's offset.
147#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
148pub enum InkShape {
149 /// A line or arrow from one end to the other.
150 Line([[f32; 2]; 2]),
151 /// A closed shape: `transform` (`[a, b, c, d, x, y]`) takes its anchors, in the unit
152 /// square, onto the page.
153 Closed {
154 transform: [f32; 6],
155 anchors: Vec<[f32; 2]>,
156 },
157}
158
159#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
160pub struct InkStroke {
161 pub id: ExGuid,
162 /// Page coordinates in points, before the drawing's offset.
163 pub points: Vec<[f32; 2]>,
164 /// Pen width and height in points.
165 pub width: f32,
166 pub height: f32,
167 /// COLORREF; absent means the window text colour.
168 pub color: Option<u32>,
169 /// 0 is opaque, 255 fully transparent.
170 pub transparency: Option<u8>,
171 /// 0 is a round (ball) tip, 1 a rectangle.
172 pub pen_tip: Option<u8>,
173 /// The ISF raster operation: 9 (MaskPen) for a highlighter, whose colour multiplies
174 /// what lies beneath.
175 #[serde(default)]
176 pub raster_operation: Option<u8>,
177 /// The pen's pressure at each point, from none at 0 to full at 1, for a stroke whose
178 /// width follows it; empty where the pen ignores pressure, as a mouse's does.
179 #[serde(default)]
180 pub pressure: Vec<f32>,
181}
182
183impl Ink {
184 pub(crate) fn read(
185 revision: &Revision<'_>,
186 id: ExGuid,
187 node: &crate::document::Element<'_>,
188 ) -> Result<Self, Error> {
189 let invalid = |message| Error { offset: 0, message };
190 let Kind::Ink {
191 data,
192 scale_x,
193 scale_y,
194 shape_kind,
195 line,
196 anchors,
197 } = &node.kind
198 else {
199 unreachable!()
200 };
201 let scale = [scale_x.unwrap_or(1.0), scale_y.unwrap_or(1.0)];
202 let mut strokes = Vec::new();
203 if let Some(data) = data {
204 let data = revision
205 .nodes
206 .get(data)
207 .ok_or_else(|| invalid("Missing ink data"))?;
208 let Kind::InkData { strokes: ids, .. } = &data.kind else {
209 return Err(invalid("Ink data has the wrong type"));
210 };
211 for stroke_id in ids {
212 let stroke = revision
213 .nodes
214 .get(stroke_id)
215 .ok_or_else(|| invalid("Missing ink stroke"))?;
216 strokes.push(InkStroke::read(revision, *stroke_id, stroke, scale)?);
217 }
218 }
219 let mut groups = Vec::new();
220 for child in &node.content {
221 let element = revision
222 .nodes
223 .get(child)
224 .ok_or_else(|| invalid("Missing nested ink"))?;
225 if matches!(element.kind, Kind::Ink { .. }) {
226 groups.push(Ink::read(revision, *child, element)?);
227 }
228 }
229 Ok(Self {
230 id,
231 layout: node.layout.clone(),
232 strokes,
233 groups,
234 shape: InkShape::read(*shape_kind, line.as_deref(), anchors.as_deref()),
235 })
236 }
237
238 /// The extent of every stroke point, as `[left, top, width, height]` in points.
239 pub fn bounds(&self) -> Option<[f32; 4]> {
240 let mut extent: Option<[f32; 4]> = None;
241 for point in self
242 .strokes
243 .iter()
244 .flat_map(|stroke| stroke.points.iter())
245 .copied()
246 .chain(
247 self.groups
248 .iter()
249 .filter_map(Ink::bounds)
250 .flat_map(|[x, y, w, h]| [[x, y], [x + w, y + h]]),
251 )
252 {
253 extent = Some(match extent {
254 None => [point[0], point[1], point[0], point[1]],
255 Some([x0, y0, x1, y1]) => [
256 x0.min(point[0]),
257 y0.min(point[1]),
258 x1.max(point[0]),
259 y1.max(point[1]),
260 ],
261 });
262 }
263 extent.map(|[x0, y0, x1, y1]| [x0, y0, x1 - x0, y1 - y0])
264 }
265}
266
267impl InkStroke {
268 fn read(
269 revision: &Revision<'_>,
270 id: ExGuid,
271 node: &crate::document::Element<'_>,
272 scale: [f32; 2],
273 ) -> Result<Self, Error> {
274 let invalid = |message| Error { offset: 0, message };
275 let Kind::InkStroke { path, style, .. } = &node.kind else {
276 return Err(invalid("Ink stroke has the wrong type"));
277 };
278 let style = style
279 .and_then(|style| revision.nodes.get(&style))
280 .ok_or_else(|| invalid("Missing ink stroke style"))?;
281 let Kind::InkStyle {
282 dimensions,
283 width,
284 height,
285 color,
286 transparency,
287 pen_tip,
288 raster_operation,
289 ignore_pressure,
290 ..
291 } = &style.kind
292 else {
293 return Err(invalid("Ink stroke style has the wrong type"));
294 };
295 if dimensions.len() % 32 != 0 || dimensions.is_empty() {
296 return Err(invalid("Ink dimension table has an invalid length"));
297 }
298 let entries: Vec<&[u8]> = dimensions
299 .as_chunks::<32>()
300 .0
301 .iter()
302 .map(|entry| entry.as_slice())
303 .collect();
304 let axis = |guid: &[u8]| entries.iter().position(|entry| entry[..16] == *guid);
305 let (Some(x), Some(y)) = (axis(&DIMENSION_X), axis(&DIMENSION_Y)) else {
306 return Err(invalid("Ink dimension table lacks X and Y"));
307 };
308 let values = multi_byte(path).ok_or_else(|| invalid("Ink stroke packet is malformed"))?;
309 if values.len() % entries.len() != 0 {
310 return Err(invalid("Ink stroke packet does not cover its dimensions"));
311 }
312 let per_dimension = values.len() / entries.len();
313 let coordinates = |dimension: usize, factor: f32| {
314 let mut position = 0i64;
315 values[dimension * per_dimension..(dimension + 1) * per_dimension]
316 .iter()
317 .map(move |delta| {
318 position += delta;
319 points(position, factor)
320 })
321 };
322 let points = coordinates(x, scale[0])
323 .zip(coordinates(y, scale[1]))
324 .map(|(x, y)| [x, y])
325 .collect();
326 let mut pressure = Vec::new();
327 if let Some(dimension) = axis(&PRESSURE_DIMENSION[..16])
328 && *ignore_pressure != Some(true)
329 {
330 // In f32, as `level` divides, so a level written reads back equal.
331 let [lower, upper] = [16, 20].map(|at| {
332 i32::from_le_bytes(entries[dimension][at..at + 4].try_into().unwrap()) as f32
333 });
334 if upper > lower {
335 let mut level = 0i64;
336 pressure = values[dimension * per_dimension..(dimension + 1) * per_dimension]
337 .iter()
338 .map(|delta| {
339 level += delta;
340 (level as f32 - lower) / (upper - lower)
341 })
342 .collect();
343 }
344 }
345 Ok(Self {
346 id,
347 points,
348 width: width.unwrap_or(0.0) * 72.0 / 2540.0,
349 height: height.unwrap_or(0.0) * 72.0 / 2540.0,
350 color: *color,
351 transparency: *transparency,
352 pen_tip: *pen_tip,
353 raster_operation: *raster_operation,
354 pressure,
355 })
356 }
357
358 /// How many times its pen's size the stroke draws at point `index`, as OneNote 2010
359 /// draws pressure (`corpus/ink-pressure`): a quarter at none, all of it at half and 1.75
360 /// times at full.
361 pub fn thickness(&self, index: usize) -> f32 {
362 self.pressure
363 .get(index)
364 .map_or(1.0, |pressure| 0.25 + 1.5 * pressure)
365 }
366}
367
368impl InkShape {
369 /// Any geometry this does not recognise reads as no shape: the strokes still draw it.
370 fn read(kind: Option<u8>, line: Option<&[u8]>, anchors: Option<&[u8]>) -> Option<Self> {
371 let floats = |bytes: &[u8]| -> Vec<f32> {
372 bytes
373 .as_chunks::<4>()
374 .0
375 .iter()
376 .map(|b| f32::from_le_bytes(*b))
377 .collect()
378 };
379 match (kind?, line, anchors) {
380 (11, Some(line), _) if line.len() == 16 => {
381 let v = floats(line);
382 Some(Self::Line(
383 [[v[0], v[1]], [v[2], v[3]]].map(|end| end.map(|v| v * 36.0)),
384 ))
385 .filter(Self::finite)
386 }
387 (12, _, Some(bytes)) if bytes.len() >= 24 && (bytes.len() - 24) % 8 == 0 => {
388 let v = floats(bytes);
389 Some(Self::Closed {
390 transform: std::array::from_fn(|i| v[i] * 36.0),
391 anchors: v[6..].as_chunks::<2>().0.to_vec(),
392 })
393 .filter(Self::finite)
394 }
395 _ => None,
396 }
397 }
398
399 fn finite(&self) -> bool {
400 match self {
401 Self::Line(ends) => ends.iter().flatten().all(|v| v.is_finite()),
402 Self::Closed { transform, anchors } => transform
403 .iter()
404 .chain(anchors.iter().flatten())
405 .all(|v| v.is_finite()),
406 }
407 }
408
409 /// The kind, property and bytes OneNote stores the shape as.
410 pub(crate) fn stored(&self) -> (u8, u32, Vec<u8>) {
411 let half = |v: &f32| (v / 36.0).to_le_bytes();
412 match self {
413 Self::Line(ends) => (
414 11,
415 0x1c001dac,
416 ends.iter().flatten().flat_map(half).collect(),
417 ),
418 Self::Closed { transform, anchors } => (
419 12,
420 0x1c001daa,
421 transform
422 .iter()
423 .flat_map(half)
424 .chain(anchors.iter().flatten().flat_map(|v| v.to_le_bytes()))
425 .collect(),
426 ),
427 }
428 }
429
430 /// This shape as it reads back once stored.
431 pub fn snapped(&self) -> Self {
432 let half = |v: f32| (v / 36.0) * 36.0;
433 match self {
434 Self::Line(ends) => Self::Line(ends.map(|end| end.map(half))),
435 Self::Closed { transform, anchors } => Self::Closed {
436 transform: transform.map(half),
437 anchors: anchors.clone(),
438 },
439 }
440 }
441}
442
443/// ISF multi-byte decoding: a count, then that many 7-bit little-endian varints whose low bit
444/// is the sign.
445fn multi_byte(bytes: &[u8]) -> Option<Vec<i64>> {
446 let mut cursor = 0;
447 let mut next = || {
448 let mut value = 0u64;
449 let mut shift = 0;
450 loop {
451 let byte = *bytes.get(cursor)?;
452 cursor += 1;
453 if shift >= 64 {
454 return None;
455 }
456 value |= u64::from(byte & 0x7f) << shift;
457 shift += 7;
458 if byte & 0x80 == 0 {
459 return Some(value);
460 }
461 }
462 };
463 let count = next()? >> 1;
464 let mut values = Vec::with_capacity(usize::try_from(count).ok()?.min(1 << 20));
465 for _ in 0..count {
466 let raw = next()?;
467 let magnitude = i64::try_from(raw >> 1).ok()?;
468 values.push(if raw & 1 == 1 { -magnitude } else { magnitude });
469 }
470 (cursor == bytes.len()).then_some(values)
471}
472
473impl InkStroke {
474 /// The stroke packet OneNote stores: X, Y and any pressure as ISF multi-byte first
475 /// differences, of the points rounded to HIMETRIC and the pressure's levels.
476 pub(crate) fn packet(&self) -> Vec<u8> {
477 let differences = |values: Vec<i64>| {
478 let mut previous = 0i64;
479 values.into_iter().map(move |value| {
480 let delta = value - previous;
481 previous = value;
482 delta
483 })
484 };
485 let axis = |index: usize| {
486 differences(
487 self.points
488 .iter()
489 .map(|point| (point[index] * 2540.0 / 72.0).round() as i64)
490 .collect(),
491 )
492 };
493 let pressure = differences(
494 self.pressure
495 .iter()
496 .map(|pressure| (pressure.clamp(0.0, 1.0) * PRESSURE_LEVELS).round() as i64)
497 .collect(),
498 );
499 let values: Vec<i64> = axis(0).chain(axis(1)).chain(pressure).collect();
500 let mut out = Vec::new();
501 let mut push = |raw: u64| {
502 let mut raw = raw;
503 loop {
504 let byte = (raw & 0x7f) as u8;
505 raw >>= 7;
506 if raw == 0 {
507 out.push(byte);
508 break;
509 }
510 out.push(byte | 0x80);
511 }
512 };
513 push((values.len() as u64) << 1);
514 for value in values {
515 push(((value.unsigned_abs()) << 1) | u64::from(value < 0));
516 }
517 out
518 }
519}
520
521/// The dimension table OneNote 2010 writes for mouse ink: X and Y in HIMETRIC with the limits
522/// and resolution of the authoring screen.
523pub(crate) const DIMENSIONS: [u8; 64] = [
524 0x8f, 0x6a, 0x8a, 0x59, 0xc0, 0x52, 0xa0, 0x4b, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61,
525 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0xe2, 0xe2, 0x41,
526 0x75, 0x9f, 0x3f, 0xb5, 0xe0, 0x04, 0x98, 0x44, 0xa7, 0xee, 0xc3, 0x0d, 0xbb, 0x5a, 0x90, 0x11,
527 0x00, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x8b, 0xc5, 0xe2, 0x41,
528];
529
530/// The entry a pressure-sensitive pen adds to `DIMENSIONS`: NormalPressure from 0 to 1023, of
531/// no unit and resolution 1, as OneNote keeps it (`corpus/ink-pressure`).
532pub(crate) const PRESSURE_DIMENSION: [u8; 32] = [
533 0x2d, 0x50, 0x07, 0x73, 0xf4, 0xf9, 0x18, 0x4e, 0xb3, 0xf2, 0x2c, 0xe1, 0xb1, 0xa3, 0x61, 0x0c,
534 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f,
535];
536
537/// The dimension table OneNote 2010 writes for a drawn shape's pen: X and Y in HIMETRIC over
538/// the whole 32-bit range, at a resolution of 1000.
539pub(crate) const SHAPE_DIMENSIONS: [u8; 64] = [
540 0x8f, 0x6a, 0x8a, 0x59, 0xc0, 0x52, 0xa0, 0x4b, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61,
541 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0x7f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0x44,
542 0x75, 0x9f, 0x3f, 0xb5, 0xe0, 0x04, 0x98, 0x44, 0xa7, 0xee, 0xc3, 0x0d, 0xbb, 0x5a, 0x90, 0x11,
543 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0x7f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0x44,
544];
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn packets_round_trip_through_the_multi_byte_coding() {
552 let stroke = InkStroke {
553 id: ExGuid::default(),
554 points: vec![[100.0, 50.0], [100.5, 49.0], [99.0, 49.0]],
555 width: 1.0,
556 height: 1.0,
557 color: None,
558 transparency: None,
559 pen_tip: None,
560 raster_operation: None,
561 pressure: Vec::new(),
562 };
563 let values = multi_byte(&stroke.packet()).unwrap();
564 assert_eq!(values.len(), 6);
565 assert_eq!(&values[..3], &[3528, 17, -52]);
566 assert_eq!(&values[3..], &[1764, -35, 0]);
567 }
568}