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.chunks_exact(32).collect();
299 let axis = |guid: &[u8]| entries.iter().position(|entry| entry[..16] == *guid);
300 let (Some(x), Some(y)) = (axis(&DIMENSION_X), axis(&DIMENSION_Y)) else {
301 return Err(invalid("Ink dimension table lacks X and Y"));
302 };
303 let values = multi_byte(path).ok_or_else(|| invalid("Ink stroke packet is malformed"))?;
304 if values.len() % entries.len() != 0 {
305 return Err(invalid("Ink stroke packet does not cover its dimensions"));
306 }
307 let per_dimension = values.len() / entries.len();
308 let coordinates = |dimension: usize, factor: f32| {
309 let mut position = 0i64;
310 values[dimension * per_dimension..(dimension + 1) * per_dimension]
311 .iter()
312 .map(move |delta| {
313 position += delta;
314 points(position, factor)
315 })
316 };
317 let points = coordinates(x, scale[0])
318 .zip(coordinates(y, scale[1]))
319 .map(|(x, y)| [x, y])
320 .collect();
321 let mut pressure = Vec::new();
322 if let Some(dimension) = axis(&PRESSURE_DIMENSION[..16])
323 && *ignore_pressure != Some(true)
324 {
325 // In f32, as `level` divides, so a level written reads back equal.
326 let [lower, upper] = [16, 20].map(|at| {
327 i32::from_le_bytes(entries[dimension][at..at + 4].try_into().unwrap()) as f32
328 });
329 if upper > lower {
330 let mut level = 0i64;
331 pressure = values[dimension * per_dimension..(dimension + 1) * per_dimension]
332 .iter()
333 .map(|delta| {
334 level += delta;
335 (level as f32 - lower) / (upper - lower)
336 })
337 .collect();
338 }
339 }
340 Ok(Self {
341 id,
342 points,
343 width: width.unwrap_or(0.0) * 72.0 / 2540.0,
344 height: height.unwrap_or(0.0) * 72.0 / 2540.0,
345 color: *color,
346 transparency: *transparency,
347 pen_tip: *pen_tip,
348 raster_operation: *raster_operation,
349 pressure,
350 })
351 }
352
353 /// How many times its pen's size the stroke draws at point `index`, as OneNote 2010
354 /// draws pressure (`corpus/ink-pressure`): a quarter at none, all of it at half and 1.75
355 /// times at full.
356 pub fn thickness(&self, index: usize) -> f32 {
357 self.pressure
358 .get(index)
359 .map_or(1.0, |pressure| 0.25 + 1.5 * pressure)
360 }
361}
362
363impl InkShape {
364 /// Any geometry this does not recognise reads as no shape: the strokes still draw it.
365 fn read(kind: Option<u8>, line: Option<&[u8]>, anchors: Option<&[u8]>) -> Option<Self> {
366 let floats = |bytes: &[u8]| -> Vec<f32> {
367 bytes
368 .chunks_exact(4)
369 .map(|b| f32::from_le_bytes(b.try_into().unwrap()))
370 .collect()
371 };
372 match (kind?, line, anchors) {
373 (11, Some(line), _) if line.len() == 16 => {
374 let v = floats(line);
375 Some(Self::Line(
376 [[v[0], v[1]], [v[2], v[3]]].map(|end| end.map(|v| v * 36.0)),
377 ))
378 .filter(Self::finite)
379 }
380 (12, _, Some(bytes)) if bytes.len() >= 24 && (bytes.len() - 24) % 8 == 0 => {
381 let v = floats(bytes);
382 Some(Self::Closed {
383 transform: std::array::from_fn(|i| v[i] * 36.0),
384 anchors: v[6..].chunks_exact(2).map(|p| [p[0], p[1]]).collect(),
385 })
386 .filter(Self::finite)
387 }
388 _ => None,
389 }
390 }
391
392 fn finite(&self) -> bool {
393 match self {
394 Self::Line(ends) => ends.iter().flatten().all(|v| v.is_finite()),
395 Self::Closed { transform, anchors } => transform
396 .iter()
397 .chain(anchors.iter().flatten())
398 .all(|v| v.is_finite()),
399 }
400 }
401
402 /// The kind, property and bytes OneNote stores the shape as.
403 pub(crate) fn stored(&self) -> (u8, u32, Vec<u8>) {
404 let half = |v: &f32| (v / 36.0).to_le_bytes();
405 match self {
406 Self::Line(ends) => (
407 11,
408 0x1c001dac,
409 ends.iter().flatten().flat_map(half).collect(),
410 ),
411 Self::Closed { transform, anchors } => (
412 12,
413 0x1c001daa,
414 transform
415 .iter()
416 .flat_map(half)
417 .chain(anchors.iter().flatten().flat_map(|v| v.to_le_bytes()))
418 .collect(),
419 ),
420 }
421 }
422
423 /// This shape as it reads back once stored.
424 pub fn snapped(&self) -> Self {
425 let half = |v: f32| (v / 36.0) * 36.0;
426 match self {
427 Self::Line(ends) => Self::Line(ends.map(|end| end.map(half))),
428 Self::Closed { transform, anchors } => Self::Closed {
429 transform: transform.map(half),
430 anchors: anchors.clone(),
431 },
432 }
433 }
434}
435
436/// ISF multi-byte decoding: a count, then that many 7-bit little-endian varints whose low bit
437/// is the sign.
438fn multi_byte(bytes: &[u8]) -> Option<Vec<i64>> {
439 let mut cursor = 0;
440 let mut next = || {
441 let mut value = 0u64;
442 let mut shift = 0;
443 loop {
444 let byte = *bytes.get(cursor)?;
445 cursor += 1;
446 if shift >= 64 {
447 return None;
448 }
449 value |= u64::from(byte & 0x7f) << shift;
450 shift += 7;
451 if byte & 0x80 == 0 {
452 return Some(value);
453 }
454 }
455 };
456 let count = next()? >> 1;
457 let mut values = Vec::with_capacity(usize::try_from(count).ok()?.min(1 << 20));
458 for _ in 0..count {
459 let raw = next()?;
460 let magnitude = i64::try_from(raw >> 1).ok()?;
461 values.push(if raw & 1 == 1 { -magnitude } else { magnitude });
462 }
463 (cursor == bytes.len()).then_some(values)
464}
465
466impl InkStroke {
467 /// The stroke packet OneNote stores: X, Y and any pressure as ISF multi-byte first
468 /// differences, of the points rounded to HIMETRIC and the pressure's levels.
469 pub(crate) fn packet(&self) -> Vec<u8> {
470 let differences = |values: Vec<i64>| {
471 let mut previous = 0i64;
472 values.into_iter().map(move |value| {
473 let delta = value - previous;
474 previous = value;
475 delta
476 })
477 };
478 let axis = |index: usize| {
479 differences(
480 self.points
481 .iter()
482 .map(|point| (point[index] * 2540.0 / 72.0).round() as i64)
483 .collect(),
484 )
485 };
486 let pressure = differences(
487 self.pressure
488 .iter()
489 .map(|pressure| (pressure.clamp(0.0, 1.0) * PRESSURE_LEVELS).round() as i64)
490 .collect(),
491 );
492 let values: Vec<i64> = axis(0).chain(axis(1)).chain(pressure).collect();
493 let mut out = Vec::new();
494 let mut push = |raw: u64| {
495 let mut raw = raw;
496 loop {
497 let byte = (raw & 0x7f) as u8;
498 raw >>= 7;
499 if raw == 0 {
500 out.push(byte);
501 break;
502 }
503 out.push(byte | 0x80);
504 }
505 };
506 push((values.len() as u64) << 1);
507 for value in values {
508 push(((value.unsigned_abs()) << 1) | u64::from(value < 0));
509 }
510 out
511 }
512}
513
514/// The dimension table OneNote 2010 writes for mouse ink: X and Y in HIMETRIC with the limits
515/// and resolution of the authoring screen.
516pub(crate) const DIMENSIONS: [u8; 64] = [
517 0x8f, 0x6a, 0x8a, 0x59, 0xc0, 0x52, 0xa0, 0x4b, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61,
518 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0xe2, 0xe2, 0x41,
519 0x75, 0x9f, 0x3f, 0xb5, 0xe0, 0x04, 0x98, 0x44, 0xa7, 0xee, 0xc3, 0x0d, 0xbb, 0x5a, 0x90, 0x11,
520 0x00, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x8b, 0xc5, 0xe2, 0x41,
521];
522
523/// The entry a pressure-sensitive pen adds to `DIMENSIONS`: NormalPressure from 0 to 1023, of
524/// no unit and resolution 1, as OneNote keeps it (`corpus/ink-pressure`).
525pub(crate) const PRESSURE_DIMENSION: [u8; 32] = [
526 0x2d, 0x50, 0x07, 0x73, 0xf4, 0xf9, 0x18, 0x4e, 0xb3, 0xf2, 0x2c, 0xe1, 0xb1, 0xa3, 0x61, 0x0c,
527 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x3f,
528];
529
530/// The dimension table OneNote 2010 writes for a drawn shape's pen: X and Y in HIMETRIC over
531/// the whole 32-bit range, at a resolution of 1000.
532pub(crate) const SHAPE_DIMENSIONS: [u8; 64] = [
533 0x8f, 0x6a, 0x8a, 0x59, 0xc0, 0x52, 0xa0, 0x4b, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61,
534 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0x7f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0x44,
535 0x75, 0x9f, 0x3f, 0xb5, 0xe0, 0x04, 0x98, 0x44, 0xa7, 0xee, 0xc3, 0x0d, 0xbb, 0x5a, 0x90, 0x11,
536 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0x7f, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7a, 0x44,
537];
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn packets_round_trip_through_the_multi_byte_coding() {
545 let stroke = InkStroke {
546 id: ExGuid::default(),
547 points: vec![[100.0, 50.0], [100.5, 49.0], [99.0, 49.0]],
548 width: 1.0,
549 height: 1.0,
550 color: None,
551 transparency: None,
552 pen_tip: None,
553 raster_operation: None,
554 pressure: Vec::new(),
555 };
556 let values = multi_byte(&stroke.packet()).unwrap();
557 assert_eq!(values.len(), 6);
558 assert_eq!(&values[..3], &[3528, 17, -52]);
559 assert_eq!(&values[3..], &[1764, -35, 0]);
560 }
561}