1from pathlib import Path
2import runpy
3import shutil
4from tempfile import TemporaryDirectory
5import unittest
6import xml.etree.ElementTree as ET
7
8from native_xml import ns
9
10ROOT = Path(__file__).resolve().parent.parent
11FIXTURE = ROOT / 'corpus/ink-tools'
12compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
13
14
15def cold_pages(variant):
16 """The cold read of a Rust candidate, after checking it holds what the candidate stored."""
17 with TemporaryDirectory() as temporary:
18 read = Path(temporary) / 'read'
19 shutil.copytree(FIXTURE / variant / 'cold/read', read)
20 compare(FIXTURE / variant / 'candidate', read)
21 return pages(FIXTURE / variant / 'cold/read')
22
23
24def pages(read):
25 return {page.get('name'): page for page in (ET.parse(path).getroot() for path in sorted(read.glob('page-*.xml')))}
26
27
28def drawings(page):
29 return page.findall('one:InkDrawing', ns)
30
31
32def place(drawing):
33 position, size = drawing.find('one:Position', ns), drawing.find('one:Size', ns)
34 return [float(position.get('x')), float(position.get('y')), float(size.get('width')), float(size.get('height'))]
35
36
37class InkToolsTest(unittest.TestCase):
38 def assertPlaced(self, drawing, expected, delta=0.06):
39 for actual, wanted in zip(place(drawing), expected, strict=True):
40 self.assertAlmostEqual(actual, wanted, delta=delta)
41
42 def test_onenote_keeps_each_stroke_and_shape_a_drawing_of_its_own(self):
43 read = pages(FIXTURE / 'native-ui/read')
44 self.assertEqual(len(drawings(read['Gallery'])), 14)
45 shapes = [d.find('one:ShapeInfo', ns) for d in drawings(read['Pens'])]
46 self.assertEqual(sorted(shape.get('isLine') for shape in shapes if shape is not None), ['false', 'false', 'true', 'true'])
47 self.assertTrue((FIXTURE / 'native-ui/read/page-001.png').exists())
48
49 def test_snowbound_pens_highlighters_and_shapes_open_in_onenote(self):
50 page, = cold_pages('drawn').values()
51 found = drawings(page)
52 self.assertEqual(len(found), 6)
53 shapes = {d.get('objectID'): d.find('one:ShapeInfo', ns) for d in found}
54 lines = [s for s in shapes.values() if s is not None and s.get('isLine') == 'true']
55 closed = [s for s in shapes.values() if s is not None and s.get('isLine') == 'false']
56 self.assertEqual((len(lines), len(closed)), (2, 2))
57 rectangle = next(s for s in closed if len(s.findall('one:AnchorPoint', ns)) == 8)
58 corners = [(float(a.get('x')), float(a.get('y'))) for a in rectangle.findall('one:AnchorPoint', ns)]
59 self.assertAlmostEqual(corners[0][0], 252.0, delta=0.01)
60 self.assertAlmostEqual(corners[4][1], 194.4, delta=0.01)
61 # The highlighter's stroke spans its points, one HIMETRIC unit larger, as OneNote reports ink.
62 highlighter = next(d for d in found if abs(place(d)[3]) < 0.1)
63 self.assertPlaced(highlighter, [60.0, 190.0, 160.0, 0.03])
64 self.assertTrue((FIXTURE / 'drawn/cold/read/page-000.png').exists())
65
66 def test_onenote_drawings_keep_their_ink_after_snowbound_erases_moves_and_draws(self):
67 # Object identities differ between clones' reads, so drawings match by where they lie.
68 before = [place(d) for d in drawings(pages(FIXTURE / 'native-ui/read')['Pens'])]
69 after = [place(d) for d in drawings(cold_pages('edited')['Pens'])]
70 self.assertEqual(len(after), len(before) - 1 + 6)
71 near = lambda a, b: all(abs(x - y) < 0.001 for x, y in zip(a, b))
72 zigzag = next(p for p in before if abs(p[2] - 90.03) < 0.01 and abs(p[3] - 22.51) < 0.01)
73 line = next(p for p in before if p[2] < 0.1 and abs(p[3] - 60.04) < 0.01)
74 self.assertFalse(any(near(p, zigzag) for p in after))
75 self.assertTrue(any(near(p, [line[0] + 36.0, line[1] + 18.0, line[2], line[3]]) for p in after))
76 for kept in before:
77 if kept not in (zigzag, line):
78 self.assertTrue(any(near(p, kept) for p in after), kept)
79
80
81if __name__ == '__main__':
82 unittest.main()