From 237d4668ad21d8c6275da60e59bc4acb066fb853 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 12 Sep 2026 17:37:36 -0700 Subject: [PATCH] feat: write ink drawings and handwriting through the page model A page::Ink object in the model is written the way OneNote 2010 stores a mouse drawing: an ink container (jcid 0x60014) the page lists as a child or a paragraph holds as content, its data node (0x2003b) listing stroke objects (0x20047) whose coordinates are ISF multi-byte first differences in HIMETRIC with index, language, identity, time and half-inch origin, and one drawing-attribute object (0x120048) per distinct pen carrying the dimension table OneNote wrote for its own ink. Points are rounded to the HIMETRIC grid (page::ink::snap gives callers the same rounding), so a written stroke reads back equal. Editing rewrites the stroke list: stored strokes keep their paths and pens, removed ones leave the list, new ones are created; removing the object goes through the tree editor, which now accepts ink as a page child. corpus/ink-edit holds cold reads of a Rust square-and-diagonal drawing on the page and inside an outline paragraph, with screenshots of OneNote 2010 rendering both; tools/test_ink_edit.py checks them without a VM. Assisted-by: claude-fable-5.1 --- corpus/ink-edit/README.md | 26 ++ .../drawing/candidate/Open Notebook.onetoc2 | Bin 0 -> 1596 bytes corpus/ink-edit/drawing/candidate/ink.one | Bin 0 -> 6664 bytes corpus/ink-edit/drawing/cold/commands.jsonl | 3 + corpus/ink-edit/drawing/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3748 bytes corpus/ink-edit/drawing/cold/notebook/ink.one | Bin 0 -> 8160 bytes .../drawing/cold/read/environment.json | 7 + .../ink-edit/drawing/cold/read/hierarchy.xml | 2 + .../cold/read/page-000.navigation.json | 13 + .../ink-edit/drawing/cold/read/page-000.png | Bin 0 -> 37085 bytes .../ink-edit/drawing/cold/read/page-000.xml | 7 + .../ink-edit/drawing/cold/read/payloads.json | 3 + corpus/ink-edit/drawing/cold/run.json | 18 + corpus/ink-edit/drawing/cold/scripts/cold.ps1 | 27 ++ corpus/ink-edit/drawing/cold/scripts/read.ps1 | 142 +++++++ corpus/ink-edit/drawing/cold/source.json | 14 + corpus/ink-edit/drawing/cold/teardown.json | 1 + .../candidate/Open Notebook.onetoc2 | Bin 0 -> 1596 bytes corpus/ink-edit/handwriting/candidate/ink.one | Bin 0 -> 7240 bytes .../ink-edit/handwriting/cold/commands.jsonl | 3 + corpus/ink-edit/handwriting/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3748 bytes .../handwriting/cold/notebook/ink.one | Bin 0 -> 8744 bytes .../handwriting/cold/read/environment.json | 7 + .../handwriting/cold/read/hierarchy.xml | 2 + .../cold/read/page-000.navigation.json | 13 + .../handwriting/cold/read/page-000.png | Bin 0 -> 37444 bytes .../handwriting/cold/read/page-000.xml | 7 + .../handwriting/cold/read/payloads.json | 3 + corpus/ink-edit/handwriting/cold/run.json | 18 + .../handwriting/cold/scripts/cold.ps1 | 27 ++ .../handwriting/cold/scripts/read.ps1 | 142 +++++++ corpus/ink-edit/handwriting/cold/source.json | 14 + .../ink-edit/handwriting/cold/teardown.json | 1 + crates/notebook/README.md | 2 +- crates/onestore/README.md | 2 +- crates/onestore/src/page/ink.rs | 83 +++- crates/onestore/src/page/write.rs | 362 +++++++++++++++++- crates/onestore/src/tree.rs | 2 +- crates/onestore/tests/page_ink.rs | 162 +++++++- tools/test_ink_edit.py | 41 ++ 42 files changed, 1128 insertions(+), 28 deletions(-) create mode 100644 corpus/ink-edit/README.md create mode 100644 corpus/ink-edit/drawing/candidate/Open Notebook.onetoc2 create mode 100644 corpus/ink-edit/drawing/candidate/ink.one create mode 100644 corpus/ink-edit/drawing/cold/commands.jsonl create mode 100644 corpus/ink-edit/drawing/cold/machine.json create mode 100644 corpus/ink-edit/drawing/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/ink-edit/drawing/cold/notebook/ink.one create mode 100644 corpus/ink-edit/drawing/cold/read/environment.json create mode 100644 corpus/ink-edit/drawing/cold/read/hierarchy.xml create mode 100644 corpus/ink-edit/drawing/cold/read/page-000.navigation.json create mode 100644 corpus/ink-edit/drawing/cold/read/page-000.png create mode 100644 corpus/ink-edit/drawing/cold/read/page-000.xml create mode 100644 corpus/ink-edit/drawing/cold/read/payloads.json create mode 100644 corpus/ink-edit/drawing/cold/run.json create mode 100644 corpus/ink-edit/drawing/cold/scripts/cold.ps1 create mode 100644 corpus/ink-edit/drawing/cold/scripts/read.ps1 create mode 100644 corpus/ink-edit/drawing/cold/source.json create mode 100644 corpus/ink-edit/drawing/cold/teardown.json create mode 100644 corpus/ink-edit/handwriting/candidate/Open Notebook.onetoc2 create mode 100644 corpus/ink-edit/handwriting/candidate/ink.one create mode 100644 corpus/ink-edit/handwriting/cold/commands.jsonl create mode 100644 corpus/ink-edit/handwriting/cold/machine.json create mode 100644 corpus/ink-edit/handwriting/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/ink-edit/handwriting/cold/notebook/ink.one create mode 100644 corpus/ink-edit/handwriting/cold/read/environment.json create mode 100644 corpus/ink-edit/handwriting/cold/read/hierarchy.xml create mode 100644 corpus/ink-edit/handwriting/cold/read/page-000.navigation.json create mode 100644 corpus/ink-edit/handwriting/cold/read/page-000.png create mode 100644 corpus/ink-edit/handwriting/cold/read/page-000.xml create mode 100644 corpus/ink-edit/handwriting/cold/read/payloads.json create mode 100644 corpus/ink-edit/handwriting/cold/run.json create mode 100644 corpus/ink-edit/handwriting/cold/scripts/cold.ps1 create mode 100644 corpus/ink-edit/handwriting/cold/scripts/read.ps1 create mode 100644 corpus/ink-edit/handwriting/cold/source.json create mode 100644 corpus/ink-edit/handwriting/cold/teardown.json create mode 100644 tools/test_ink_edit.py diff --git a/corpus/ink-edit/README.md b/corpus/ink-edit/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6c197490dcf039a5f94109e14922dd906bd360c8 --- /dev/null +++ b/corpus/ink-edit/README.md @@ -0,0 +1,26 @@ +# Rust ink authoring + +`drawing/candidate` is the page writer's output for +`an_ink_drawing_is_written_erased_stroke_by_stroke_and_removed` in +`crates/onestore/tests/page_ink.rs`: on a section created in Rust, a page-level +ink object with two strokes, a 60 pt square at (300, 120) and a red diagonal, +stored the way OneNote 2010 stores a mouse drawing +(`corpus/native-ink/cold-ui-ink`): an ink container listed as a page child, its +data node listing stroke objects, each stroke's coordinates as ISF multi-byte +first differences in HIMETRIC with its index, language, identity, time and +half-inch origin, and one drawing-attribute object per distinct pen carrying +the dimension table OneNote wrote. `drawing/cold` is a fresh OneNote 2010 +read: the `InkDrawing` reports the stroke extent as its position and size (one +HIMETRIC unit larger, as OneNote does) and `page-000.png` shows the square and +diagonal rendered. + +`handwriting/candidate` is the same drawing held by a new paragraph of the body +outline (`handwriting_is_written_as_paragraph_content`); `handwriting/cold` +shows OneNote accepting it as an `InkDrawing` inside the outline element, the +outline grown to contain the strokes' absolute page coordinates. + +`tools/test_ink_edit.py` checks both without a VM. Regenerate with +`ONESTORE_INK_EXPORT` and `ONESTORE_INK_PARAGRAPH_EXPORT` set to new absolute +directories while running the tests, then cold-open each with +`tools/native_runner.py OUTPUT COLD --expected-pages 1 --collect-notebook +--screenshots`. diff --git a/corpus/ink-edit/drawing/candidate/Open Notebook.onetoc2 b/corpus/ink-edit/drawing/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..4c88d541f9ef0d102d422398ad28b98221fa87f9 GIT binary patch literal 1596 zcmZ3e|KIuM`!b(-j|5(YE0x6`T=w45ew(l7eErWN^RB41GN1wbyR!tOCw}!@dO>{R z+Y{?NVl=B*B%%Ut%ZWnOoF0ZjcUJsJWdI0PD47^cWF z$n2D15XlD8`+&3nFgR+Tg>%I^ia7Mt=WRXTUi1x=i$LO;40#OM40;UtKspu3hvaCW zYy%gNU}X3v!=M7oaSRMczI^|>u_|ndAW$U;h!ik1NdfEeNh5D%mSBo_t#X&M49BBabGL$U? zGz%OOu=34&mvPf!(L>!Z{|f;Xf&dTDG3X8i87APsFd+eI71(s37|1LT0O>`v0FaN(Y!)1LLQF?B9~4g@Pl4Ra1jHbFKs3mW%eK`|LDmBR>LJz` literal 0 HcmV?d00001 diff --git a/corpus/ink-edit/drawing/candidate/ink.one b/corpus/ink-edit/drawing/candidate/ink.one new file mode 100644 index 0000000000000000000000000000000000000000..3f091445c573b00d4d36068af8702b61b5c28f8b GIT binary patch literal 6664 zcmeHKdr(x@8UI$0EH7Pjd4*kF7z)Is?Iw_F1{>H#P(dC;shUSKm}0Wp7Kx8ad|tJQ zE10+uqdZzM24ZRqLIA_4BT6E*W1J8>j<$pjp^8ldi6&yTB}u>EeZX?P0yFI&oeAH} z*?aH#9_M?V-#L3Z`^mN2FC9p)sryM`+kLzLw7z?R&(Cbe#O)jN8$aB-{3fX=p`+Xo z_DbK}>bBrlde78cn#O$hp@@IWg!ggEg#1+IRnr5XA3XF_0r^Rv4t_Y~_Z)sN^p)nF z(qrIPfA7Gn__F=?PiL)I+HNcPvN5~s>WlX)J{&dbzWz&4a%P#KV!|JLQksTpjeU(} z;JcRr9g%=7=0R2ynHHNU#A2dI1J7)^Xm0*&^6B&<``i=yt|fbf%jRDNC3WanLn6V0 zHk-*<97o0zCNjqGEJo3rmt_G+LVVEn&&~7zZ2KHcUSG7A=51TLv92|uHu?_R^!|;1 z_SLPKUj=DULoNwt9`);39Mn9!`qf?al~>a6zb&;!7_Nk4TTABJy0XZeVB+nEZ|L%Qe*W()AAS1A@ z#X~}XswRXxY`l|L3gpMN(2CcPNG`{CN#>gL(6q;%2tmx((NylwA=wbT#NPdi`&a45 zZ@Dkg%jh2Gmyy{wTxCgzV%wuFsmrfK@tmC&(+XNbdHl4{N^*$LJX%hRXf@C0)3Yq4 zbTn3T>rf$>zddOUuYtX?%1ox2JYJf8aRDSJ@UYP;(dsg`NHqEt#f%FftfL>W?_nap zNgSJz_gEm0Fdilz%6d9Iiw`D5NML7qWZBBgLKArJkr2uPI3h8;M7zi#>=v5I@;nc3 zj%C0eUSLoNV|gjg2ws2k6pH-DLBuu~ScG9P75hcNQhelzXF zV|@+1Pt={-zjv{10|3n(HPm>9ynujbYMg~V5b$hGcFx&3r#vzb)smY%a1hV_E;p5hr_}L65(GTciWhh`1JKEI zO?qhJnQ|UT-OT%)BVH)C6w0fGm5_;%@4%4JTHllX($a-1!_^Uu2x<|Ac9Obzz;|yR zb-%!d);b2B_vV4~4=v8^CT1L|1|pz3m{%%+yfAi&8w8a><$51Z57H|MlTSOI7jVbW zlW!cELisKd7DonC9QobL-)0_9@bHc!3yPwzsn!*R@3a?%!zLpP+{&){!@Cu?uB7bo z>@F^e)z_f!6u8IxM`7b_$;)yQ`E?L?us1=tmt4?)bS0qXx#Z*1QmW3ignj{E-EqhI zT*>wgoA=FWe)PiTf~4u0k*oum_bHk`grZ}WrxX}UcG;Z zkRAKg8s)xnQvNSgz%oeQZ2l3iW8$qtJMS#Xxaz|>@Z;S0^GM{W9ZQQIJDI&Nt>p0G zhgSwvFXAZ@IQ>{XysOK$sxqOm)3bEefzRLbKlZpwytLp#1b*@9E*tm1T^dRs1CBt* z$Ci($mFLw5W**zyR&o7o&p~xQesrXN*ViGfY5Ny=Wx3TM5=MEDudnlgIXddk|8Zq2?21W7g=xzlp$fU%r{PjfK{?F)t zT3*d@+7K3#g9otBpyZ+R+ z{ERJ`We=Ut_kC8KFZKRV`b6BdYy)u)>66^u-TH{=FV3y|VNFHE{C>wUakHL?8y@0) zD1+}mfs7H#9cZw=COFFkf3sbi{=A^R%f9u~t)*Fio9d1x^(xjO!M#-96+ zoZWD(DM z4>C(*F6wqnsR$kw9-84R6Y>;a`!B11Qs}I0*TyyYdI~>K=joIBt7#2ON;6)6XGcft z!e6v1`#_BgN}hVo7tVTlQ>*>JIlJStJ<}#B{eDLNGgXL%IZva(L*RpWurQkGC@791 zfd_;2o+5t9aqNpu`DS(R?48BGUK{tx{zd9IwCV~Ot@yBt);WOvtd0+H<9NH|LUx$c|SuhVWZ<5$2An^8t!YrYkUI-!N)$w6Z-@= hD)dt@5A;atrPmaA=dUpu%fMI${$DfT{vQoK{s(1xMCbqj literal 0 HcmV?d00001 diff --git a/corpus/ink-edit/drawing/cold/commands.jsonl b/corpus/ink-edit/drawing/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..6f539dbc7fc2c9194652f93285b8f18d9f1634bc --- /dev/null +++ b/corpus/ink-edit/drawing/cold/commands.jsonl @@ -0,0 +1,3 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-57607115 -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/ink-edit/drawing/cold/machine.json b/corpus/ink-edit/drawing/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..60a3f8ff65ba8b0f17c7d9d88539882209d81ad6 --- /dev/null +++ b/corpus/ink-edit/drawing/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-57607115", "hostname": "ONE-M6-57607115"} diff --git a/corpus/ink-edit/drawing/cold/notebook/Open Notebook.onetoc2 b/corpus/ink-edit/drawing/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..188af44a99a7a91f5df5c223c563bd0cddb3bb0c GIT binary patch literal 3748 zcmZ3e|KIuM`!b(-j|5(YE0x6`T=w45ew(l7eErWN^RB41GN1wbyR!tOCw}!@dO>{R z+Y{?Pnbu=@$$Q?H`g=ru<6uSbBPKcWe3X4wxLwJaif&uPDOMz@U-gA)rDK-~l=Y-GLy(1RNM9BtWeK zn+_BM7jV!r4&6LqkYPZ;F#)I#S}q0v`Pj^6!C@yh^Fi?h@)XFupt2ie4~Pc2aoM)| zDN8_-AOLhLP#hcppg>~+$2%zPfK+dpcMq(Hp#gi^y7YSLem<*Gm%VqVL@wX9at=5I zfI6_JEm#D5Eu=C5fB0Z^Y%rp literal 0 HcmV?d00001 diff --git a/corpus/ink-edit/drawing/cold/notebook/ink.one b/corpus/ink-edit/drawing/cold/notebook/ink.one new file mode 100644 index 0000000000000000000000000000000000000000..1decb42ab31514130a2e667a704e5e9e550fa7a4 GIT binary patch literal 8160 zcmeHL2~btn89vVk_>f&$1afx0vCI}=V zZY)NWfTYHt6r_wA5fUk>aY|^dHcE@srK#dlC6Shx{@=a#z~gyYX41^GGyF5>-Fwgf z|L2^4Ip6u;xsv$PHTf5+VykPO&2F2z>ksWWb~al7I4k_ajvWiC>VLI|8BuUYiLL9l zo;j6mPTRU$t1tKw->`AE&d+eV5-amS8{`84O8O&1R zrqd%`eovzJYzwJBhaMOQgr3eRQa}Hl*7xBvtCrP9HvVo=TTk!$ZDum#n7!)j&mAM; zi)~A-Ke-v%z~zT})=k}S!`o0YWBp?u_<#0*iZKCOOwIhYOq-@<&hA>~WlKGq&g&X) zOgbH#tDkdH)3tQBa7FhE$FL3+X-Fif*+w0+&kJPsC$-GpmwM51-@76UKoa5uWB**o zet@wj9E^A^cQ2c}<;C?iEuKew9vPc`eE+MSn$`0vAPuTZVM6QEmK|x1jZu|5chr_$ zkB!6lp0-TzA|7OFq6o1QT9V(+y+^h3sV*7bP@IyXn`GLN?oI@)~SmvkyK zSq?LS7GpE0O`tZOnThLVV9t=4?!*A@JkG|EHS_{zvdj#jZ+K5bn&ZH1prHq0Q_Qh| zjUPEQ=W%=NNC;$~!35*=VgQg^D-#+QC8gf|NkCXxdvSb4N`xKfNfHCUQvZW-`z`gEL2zxJc7}ccyI{0gK1$d>vW}85Us%2-iZ>2SyC*N z?6nPH1?i@e&>dOUaFu*KT8ZFDd=;BQ`8miNf|;1R_b7jPK7K=aiCjkZ6msFwYQ?q&xfQnwNRENj=F+IWNh>^iXIo7Nk`K^zFNyHCz5UUw8tE%xl+?o z zY5qxW%ymOFN|XH62;_s^5s(c zMi#Jse3&GDN%)}6{@(i^)b9@~oLjQKcl!!w%>8Zf;XS!#(E)14TI zwu;f-!zTTqzvDUYGd(#ry@C6Df_1M!I7b%tCqBX)3t{-{;+HWe7xdJ1KUdRKzBes& z9RN*fKXN>SUqHY!W1Pi!AmG{R#H2H`o7C|KYrIsq8g_};EpZ3`N;<%^26>5;HcTn9 zL*dywaj!NjVHQ@3(Uo`z-~gWeT}~=@>f-pr?S&)=c&218@N5R4ljx5081_7nvYGZf zS)44V6vC^7m5}k3YhcJI&2K{MQqsjzhKnN-5!5UU&LrdH0pBorjPnI!D9vL_{pQqg z{vpLF-9(SW)j$M92mL$}@C*JsU|om|BG)}F)=|S9CSeNt1=JWCW)aA=E>w%S1~OZ1 zAhVoI-#Ti~Q8N!@?g)yex>A+vQK--Lz?g(ESXN@iZ+}zz;A-SX#!<_(fKnZFaehwHo4%|I6F+*KOQ4yYcCB z8#BVD$9s_uWIn#|MoP>FzM+TyvL!HjmhA)dV_lcloLF&~>0&F(w+e{?_g3?I<&^wi zD1&6+lqmWU%Q3O)Q1&}X60S}lA6Sxatf+-h*VY$v7o1Am7qk8F;c1z6m5Zs13FLmv z?$Om1npGBjw9}+0yz16RR>v0>h(!wuBJlH0cZE{^>!rr`1>gt-e{8zsSCU%m5Py7c zTj||1HxC;7(DbY^oPxttIq1vZ|wG)$n(8b{xg-0iOEl+!9(DKc(4$fshDFR^8y|W#M@2$lI_^%6Zvpu zcT{2CFV_TKE?;bHhf-W2qhuduQ91{(pN;K<-8kNcItT6`ghV)lFkcWEKhinC@rqYL zsL8y4@0hyIze;~RAS~&m-H{rMrBtV(pE1;A{xU)zp>L&AZ>XAVRl|c~=!eJ%RY3Ym zr^HD5Gi>N&C`maukiuD*<^X7w|da-K}88a2E$% z+{5uZIo!vMzRJc-#y59Zfv?>7A2fch0dGKcDS}`=(dw_y-ka@HY7X+N!#&x?djg?P2X-k@NBl=omPdw9> zdnM|VdW=yK&fbbvTjN!_5kg0(+T4-`!Y#2{)j- + diff --git a/corpus/ink-edit/drawing/cold/read/page-000.navigation.json b/corpus/ink-edit/drawing/cold/read/page-000.navigation.json new file mode 100644 index 0000000000000000000000000000000000000000..957beaafccbcba166fba87cf91fc907f33c74e81 --- /dev/null +++ b/corpus/ink-edit/drawing/cold/read/page-000.navigation.json @@ -0,0 +1,13 @@ +{ + "exit": 0, + "stdout": "", + "stderr": "", + "w": 800, + "h": 600, + "error": null, + "win": { + "title": "Beside the drawing - Microsoft OneNote", + "class": "Framework::CFrame", + "dialog": false + } +} \ No newline at end of file diff --git a/corpus/ink-edit/drawing/cold/read/page-000.png b/corpus/ink-edit/drawing/cold/read/page-000.png new file mode 100644 index 0000000000000000000000000000000000000000..ca1ae460dfe067fb6efe3217010295ff3b95584e GIT binary patch literal 37085 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 frEh*#U(j>7fSF<5hF4&NHi)FCoJi$QJ^%j)x_J +AM8CHQSiAaIBAWAqABofi0JwRYR1gL7QRdkE+oeVMvLsF0G4wBa/8yv0et67E4vlXrNInJuhJVq3 +URK/7HHMzCKxQYkOuxMs7nqhWM9UiuaXxU+PBvi60uGbIsRmbacNslhJmh1OQDQGbBACEQZIEEUj +RiMJSBBE/wFFI0YjBQgLZU9mZ2hkaRkUMggAgB4CIeLiQTMIAPAQAovF4kFkAxVGAEAKXAWH8gln +jCj7wnvCe8IAh/B8h4Pke8I7wnvCQAUURgBAAAAJBAUURgAAAAASABEg7Ks5v90DzE+YKNcBjx6b +FwoAESCA5B1tFkPdAQMVRgBACj9AIm9GCC70WAqACQEKTAIPUq7GcA5CJYzgBRRGAIAAAAkEBRRG +AAAAABIAESBSpMe3m/jlRpcE5ouTMwiuCgARIIDkHW0WQ90BAxVGAEAKP0Aib0YILvRYCoA= diff --git a/corpus/ink-edit/drawing/cold/read/payloads.json b/corpus/ink-edit/drawing/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/ink-edit/drawing/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/ink-edit/drawing/cold/run.json b/corpus/ink-edit/drawing/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..cfbc7c81c4e591015915bf49041e1f218ec27586 --- /dev/null +++ b/corpus/ink-edit/drawing/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/ink", + "expected_pages": 1, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/ink-edit/drawing/cold/scripts/cold.ps1 b/corpus/ink-edit/drawing/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/ink-edit/drawing/cold/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/ink-edit/drawing/cold/scripts/read.ps1 b/corpus/ink-edit/drawing/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/ink-edit/drawing/cold/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/ink-edit/drawing/cold/source.json b/corpus/ink-edit/drawing/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..45c00a79533d54a675d39df7c75d77d0f868dc25 --- /dev/null +++ b/corpus/ink-edit/drawing/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1596, + "sha256": "0c5c0a1f79fddde5a4dca355b718618a7ad7fc6f24e1a41900f08083adf2e657", + "mtime_ns": 1789259149540453267 + }, + { + "path": "ink.one", + "bytes": 6664, + "sha256": "9b662a8f4beb2bad0d547c251101887c245b8ac49a80a00efe49c70ab60c1236", + "mtime_ns": 1789259149539962603 + } +] diff --git a/corpus/ink-edit/drawing/cold/teardown.json b/corpus/ink-edit/drawing/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/ink-edit/drawing/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/ink-edit/handwriting/candidate/Open Notebook.onetoc2 b/corpus/ink-edit/handwriting/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5a34c6ad7c3ee394c8a88f2be219f766b78ea39c GIT binary patch literal 1596 zcmZ3e|KIuM`!b(-j|5(YE0x91=;)KmcMh!UUi+OdL%Z&RtpfsshddG6dKlJRKmlffd3|fKYJK2C4=o z@b2`9rkf#Iu5)wt8P5MLRts|jQ^>7=T~C|bmhGH%a?ZOmi(%?V>Cq4v!6DGV!Z1aa zL1w26gGe@z-Up-wfWe{Dc`fRTnYwR-YsHa{nC-Vgxd_;W2m)1tfJgyD18dQV{@1yCeKvnIX4l+($pfZZ z`UOM7BePwJn$8~nGv6?^91^NefXP90W0DYgMG=Mu29*RL0R-sAgTz6~rmd^}I#)N^ zeVK@CvHg_=3>pFq4SE#}9SkfWT?`B!Kn(E$hzHUElJ$VfN`Pd+;vg9ijx+#z8OoLc zngtFCSo!9?%ed*V=%H?y|Al}GL4XJ77<31M3=?o*n2-Rq3T!%13}hAvfb^l8Ckzq< v0*(nleb90-0LaH?HVY0rA*Q364~i#{r$Fvy0%DLoAR6SxW!vhfAnO4DcWBf> literal 0 HcmV?d00001 diff --git a/corpus/ink-edit/handwriting/candidate/ink.one b/corpus/ink-edit/handwriting/candidate/ink.one new file mode 100644 index 0000000000000000000000000000000000000000..17995aa5ffa2b84eca114d31a61529f89e6612ec GIT binary patch literal 7240 zcmeHLc~F(t6~71~n}T7N_w+#tprwEnmx=}+@W2NGVu%H?ji#DBMG7tmxWrEfS`d*% zMIl6qh6zYQFu{nn8h3GPi4Fy8oyjP%nHbO-A=+5&`F+dd`TSTi?H`>fXXd=``|dgC z-g}n&y9YYvE?Bp<{ezhLgRf?_j4!+9oBx;j52tv?aGI<$c?%BTU`7;tv%=1;q+46v z;#|_%T;D!{@&s6rAZY^;eq&-Yu=Uf&V5)j&SF{NW?lg8_LtD&!<2>ky=z(hP^ zrYu0g6iXD$MWtY#b`%KBd}qmj*~IJhaKZQNd}!N?Yxv^Kaa={v)5g^-MaB3j6Q$mBnReCe1tMLpIf`SspWk6=Tzgt%yf3bAG=J!fB#VMFQZ?L_is4gD_DP zhR`THAMJ#5BDo10NA>w!G&pms z)g@{f)$_`Crr%%Rx+G-twTPf^F1v>N(wLgP!P3|owuGtJYNq3#I<|=YmMvzPY$aPt zqf6=QEz*_h8f!{*%14HCrV%r?me#_V$xt%I4C2DbwqHVb6tS9R@bjn9oQ(b0mB|Dd z;9Tfwp(EGm6-~7%x%+5zYjW ze^#`nHLV9bbI7e@N8^^nxEE34RBn94aejda$PUJ(Odu|VVf+C>W{|ng$uUl| za40|$st|4nxB?3;VR;Hu5n>qPG1R4aOXlb^@-L3D`X((UtV|dC`gqF~9$3Q=FDyDc zO?)+$5ufgP)OhCi>t~~%z~N~}D?sE!jBV&onu~wsx}Tod)#=rqef#c$%lr5)K|UV9 zX9NKDU;&?j$G~Ucw1m;XT^^&cJ-r_|{vi0VJm&9S4(&0T!q|!S>22rvTeCkuJjn)= zO+NoC+Z#6KVHIh^?|7%(S&ebT`nq3UM6GvMSWU)}BMDl~Cm2UAf<3eU>KNY?TiW5a zbJG*QT&ce*9!Wx?Y4{;UJ!bXeKdsN9XS86}flT2_y`@_qvr_~zn=$mQBu*x_3}h-) zibH+1$rg83;Lb`z8UKR)(+z>LdT+z%?Z<1ZqAEA$7MiC%hyrd%7dYO(a2u}Hq$PyV zYj6C3kRZhGcIuTihYQ`qcKC<;-PV;%!rX@Ovu*x*PtlBuxU&7f3-c`YF_q#M8+X?( zVE-#|`K>9LewOczm*U6me=TEELsG~ewOtL@zwJ4$j}r==C<{Ln=t%nE+w4Ka$&|mp zW2rtIDJFb9U>1En0K`G?C+b||XGbhlYA@2ZXck;^-DR;Z3XU3T}Lx^AWG z)Wv^dQIbE#^&t7vWXmtfRCtg-4$^hhk3XbIwPZ!PwV|AkBo3jWqGel>PRy-}-v05& zlUI+fUPMET8zCTs`>k8*jJ?5!zBJn%{z2F2k$;$9%s1C)+)=FsXK$(L5)BntI_J{A z3J{N=U)bL~_Hswqu^Ik{en^oAt}rtirIS;XdW^ z10A{g#41&vmbfnhu#ifYfbI;cWPl$UM{h>$C?D;{9;xV^eKaFw5&^yx=%u_M_%5^z?_0GGTV+vs3EZR4d zyY!N@XQ#z2ni}+^>{Fkfd{_KVS^^l=#9}i5&pDE}+(a)og0`Q>! xz!Uc_T)=7Q0e2u*5krpph7D@AYnfRFJ%(r~0z(n_AB%wD-}2bnzX6-Q)Xo3^ literal 0 HcmV?d00001 diff --git a/corpus/ink-edit/handwriting/cold/commands.jsonl b/corpus/ink-edit/handwriting/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..7dc72498256ad298ee6307198e095fa04d1945f1 --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/commands.jsonl @@ -0,0 +1,3 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-9E674848 -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/ink-edit/handwriting/cold/machine.json b/corpus/ink-edit/handwriting/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..89e41bbd3141e690b970fb6575da62a450a171ea --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-9e674848", "hostname": "ONE-M6-9E674848"} diff --git a/corpus/ink-edit/handwriting/cold/notebook/Open Notebook.onetoc2 b/corpus/ink-edit/handwriting/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..8155eda12682a33b9d1661269c32be43602e4860 GIT binary patch literal 3748 zcmZ3e|KIuM`!b(-j|5(YE0x91=;)KmcMh!33Z*OdL%Z&RtpfsshddG6dKlJRKmlffd3|08$`xKma7N zgbzd_fXivsW&bDKbY8_Qu%YU#=5=PM6jR8pfL%|U+?MT}b#l(TGm8;Au(P&kF64({ z>?%e@M?+x9hd=`h!xUKtnVm8WBH2KCACMLRdP=ABTGSUab>9ZpiX$B{+ix=h#X#bj z40#OM40;UtKspu3hvY(_Yy%gNU}X3v!=M6E3js&IeE+(!Dr|`$lrK`i(7;-BqW^X7 zUZ2e$joCGKU-E#dmVUv|@W^aeqNcNl|I9Z`Er*2a6JT;M^U!ICyrKw01A|HekN^U7 z<3ZveWz*Kxex0iu?Y>Mzw%Gp40tO8Mh6cR~h7JZ6kS+!W4gA)rDK-~l=Y-GLy(1RNM9BtWeK zn+_BM7jV!r4&6LqkYPZ;F#)I#S}q0v`Pj^6!C@yh^Fi?h@)XFupt2ie4~Pc2aoM)| zDN8_-AOLhLP#hcppg>~+$2%zPfK+dpcMq(Hp#gi^S}FH<()1U-E_+TtjlCK!d>0%7 zKpoiA7Ay=wZUd>5e$miy_vE`*_G!xkPQqqnhd{WfX0G3 qqr_+kjD`R-1dy`|tjq)Ddr=?;WjM{bA2OgyN2$>e7!83D5CQ-^U_FEY literal 0 HcmV?d00001 diff --git a/corpus/ink-edit/handwriting/cold/notebook/ink.one b/corpus/ink-edit/handwriting/cold/notebook/ink.one new file mode 100644 index 0000000000000000000000000000000000000000..ee15a8a652a5570ffe1cf389e8eead6de8179d9a GIT binary patch literal 8744 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 literal 0 HcmV?d00001 diff --git a/corpus/ink-edit/handwriting/cold/read/environment.json b/corpus/ink-edit/handwriting/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..73f73ea5c28e2c1d5e0aeadbc5e4548b46177bff --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-9E674848", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/ink-edit/handwriting/cold/read/hierarchy.xml b/corpus/ink-edit/handwriting/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..e43874a460a76679f03af7bf782023d4d9e27491 --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/ink-edit/handwriting/cold/read/page-000.navigation.json b/corpus/ink-edit/handwriting/cold/read/page-000.navigation.json new file mode 100644 index 0000000000000000000000000000000000000000..647b22d81dfa9db6e998b9b274523af9ca783a71 --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/read/page-000.navigation.json @@ -0,0 +1,13 @@ +{ + "exit": 0, + "stdout": "", + "stderr": "", + "w": 800, + "h": 600, + "error": null, + "win": { + "title": "Above the handwriting - Microsoft OneNote", + "class": "Framework::CFrame", + "dialog": false + } +} \ No newline at end of file diff --git a/corpus/ink-edit/handwriting/cold/read/page-000.png b/corpus/ink-edit/handwriting/cold/read/page-000.png new file mode 100644 index 0000000000000000000000000000000000000000..a70b8884261ec35c022f9a14c287fcd483b82406 GIT binary patch literal 37444 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 t{LFKQ-ihnaS&(6bhn$Gim{m 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 QCn +AM8CHQSiAaIBAWAqABofi0JwRYR1gL7QRdkE+oeVMvLsF0G4wBa/8yv0et67E4vlXrNInJuhJVq3 +URK/7HHMzCKxQYkOuxMs7nqhWM9UiuaXxU+PBvi60uGbIsRmbacNslhJmh1OQDQGbBACEQZIEEUj +RiMJSBBE/wFFI0YjBQgLZU9mZ2hkaRkUMggAgB4CIeLiQTMIAPAQAovF4kFkAxVGAEAKXAWH8gln +jCj7wnvCe8IAh/B8h4Pke8I7wnvCQAUURgBAAAAJBAUURgAAAAASABEgoX0Rk76oCkqcf4WNBm3y +SgoAESCA5B1tFkPdAQMVRgBACj9AIm9GCC70WAqACQEKTAIPUq7GcA5CJYzgBRRGAIAAAAkEBRRG +AAAAABIAESCehGefqfnSRo1JDkRPsA5FCgARIIDkHW0WQ90BAxVGAEAKP0Aib0YILvRYCoA= diff --git a/corpus/ink-edit/handwriting/cold/read/payloads.json b/corpus/ink-edit/handwriting/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/ink-edit/handwriting/cold/run.json b/corpus/ink-edit/handwriting/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..538b773d85f83281076d7bb4d42d505314123c59 --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/ink-para", + "expected_pages": 1, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/ink-edit/handwriting/cold/scripts/cold.ps1 b/corpus/ink-edit/handwriting/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/ink-edit/handwriting/cold/scripts/read.ps1 b/corpus/ink-edit/handwriting/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/ink-edit/handwriting/cold/source.json b/corpus/ink-edit/handwriting/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..dbc9c7a4b3dfe65970e1559a835eb0da32b0599e --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1596, + "sha256": "fc5a7b9e81d5d3e5e4328faa25445795335490f9ef35c0abc047880f90c24699", + "mtime_ns": 1789259149540474767 + }, + { + "path": "ink.one", + "bytes": 7240, + "sha256": "62ab52907aaef6066817c2598a9e6ca399813f02ab8004971ee1462360eeb555", + "mtime_ns": 1789259149540106435 + } +] diff --git a/corpus/ink-edit/handwriting/cold/teardown.json b/corpus/ink-edit/handwriting/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/ink-edit/handwriting/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/notebook/README.md b/crates/notebook/README.md index cd1073986e9ef784ae93d96983a051ab006d9636..e9b9ee4d9efe9223362d8366610e00664737645e 100644 --- a/crates/notebook/README.md +++ b/crates/notebook/README.md @@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot, writes the difference into the working image and queues one `Operation::Page(PageIntent { before, after, author })`; `before` is the page the edit started from and is the precondition reconciliation checks. Text, styles, -paragraph formatting, hyperlinks (external and internal), bullets, numbering, note tags, table rows, columns, cell shading, nested tables, pictures (insertion in paragraphs or on the page, position, size, description) and attachments, paragraph structure, +paragraph formatting, hyperlinks (external and internal), bullets, numbering, note tags, table rows, columns, cell shading, nested tables, pictures (insertion in paragraphs or on the page, position, size, description), attachments, ink strokes, paragraph structure, outline layout, insertions and deletions are all differences between `before` and `after`; the library never sees editor operations. diff --git a/crates/onestore/README.md b/crates/onestore/README.md index 049734dcee99c9139261bfbbe627beea8d82d2cf..1da730d746a2bdae2cecb646bdabd6a064670fb9 100644 --- a/crates/onestore/README.md +++ b/crates/onestore/README.md @@ -57,7 +57,7 @@ harness also accepts `--client-profile release`. | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | -| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks (external and, via `page::link`, to pages, paragraphs and sections), bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | +| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks (external and, via `page::link`, to pages, paragraphs and sections), bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, ink drawings and handwriting (strokes added and erased), paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | | `read_file` | Read a snapshot under whole-file exclusion | diff --git a/crates/onestore/src/page/ink.rs b/crates/onestore/src/page/ink.rs index b787423b0869a4774243dd20081abaa5c1163ce1..c947855a3541e84ef4cc2d6f9a02795c5f2449ed 100644 --- a/crates/onestore/src/page/ink.rs +++ b/crates/onestore/src/page/ink.rs @@ -7,7 +7,16 @@ use crate::{ document::{Kind, Layout, Revision}, }; -const HIMETRIC_PER_POINT: f32 = 2540.0 / 72.0; +/// Points from a HIMETRIC coordinate, the one conversion every stroke coordinate goes through. +fn points(himetric: i64, scale: f32) -> f32 { + himetric as f32 * scale * 72.0 / 2540.0 +} + +/// Rounds a page point to the HIMETRIC grid OneNote stores, so a stroke written from it reads +/// back equal. +pub fn snap(value: f32) -> f32 { + points((value * 2540.0 / 72.0).round() as i64, 1.0) +} const DIMENSION_X: [u8; 16] = [ 0x8f, 0x6a, 0x8a, 0x59, 0xc0, 0x52, 0xa0, 0x4b, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61, ]; @@ -165,7 +174,7 @@ impl InkStroke { .iter() .map(move |delta| { position += delta; - position as f32 * factor / HIMETRIC_PER_POINT + points(position, factor) }) }; let points = coordinates(x, scale[0]) @@ -175,8 +184,8 @@ impl InkStroke { Ok(Self { id, points, - width: width.unwrap_or(0.0) / HIMETRIC_PER_POINT, - height: height.unwrap_or(0.0) / HIMETRIC_PER_POINT, + width: width.unwrap_or(0.0) * 72.0 / 2540.0, + height: height.unwrap_or(0.0) * 72.0 / 2540.0, color: *color, transparency: *transparency, pen_tip: *pen_tip, @@ -213,3 +222,69 @@ fn multi_byte(bytes: &[u8]) -> Option> { } (cursor == bytes.len()).then_some(values) } + +impl InkStroke { + /// The stroke packet OneNote stores: X then Y as ISF multi-byte first differences of the + /// points rounded to HIMETRIC. + pub(crate) fn packet(&self) -> Vec { + let axis = |index: usize| { + let mut previous = 0i64; + self.points.iter().map(move |point| { + let value = (point[index] * 2540.0 / 72.0).round() as i64; + let delta = value - previous; + previous = value; + delta + }) + }; + let values: Vec = axis(0).chain(axis(1)).collect(); + let mut out = Vec::new(); + let mut push = |raw: u64| { + let mut raw = raw; + loop { + let byte = (raw & 0x7f) as u8; + raw >>= 7; + if raw == 0 { + out.push(byte); + break; + } + out.push(byte | 0x80); + } + }; + push((values.len() as u64) << 1); + for value in values { + push(((value.unsigned_abs()) << 1) | u64::from(value < 0)); + } + out + } +} + +/// The dimension table OneNote 2010 writes for mouse ink: X and Y in HIMETRIC with the limits +/// and resolution of the authoring screen. +pub(crate) const DIMENSIONS: [u8; 64] = [ + 0x8f, 0x6a, 0x8a, 0x59, 0xc0, 0x52, 0xa0, 0x4b, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x21, 0xe2, 0xe2, 0x41, + 0x75, 0x9f, 0x3f, 0xb5, 0xe0, 0x04, 0x98, 0x44, 0xa7, 0xee, 0xc3, 0x0d, 0xbb, 0x5a, 0x90, 0x11, + 0x00, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x8b, 0xc5, 0xe2, 0x41, +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn packets_round_trip_through_the_multi_byte_coding() { + let stroke = InkStroke { + id: ExGuid::default(), + points: vec![[100.0, 50.0], [100.5, 49.0], [99.0, 49.0]], + width: 1.0, + height: 1.0, + color: None, + transparency: None, + pen_tip: None, + }; + let values = multi_byte(&stroke.packet()).unwrap(); + assert_eq!(values.len(), 6); + assert_eq!(&values[..3], &[3528, 17, -52]); + assert_eq!(&values[3..], &[1764, -35, 0]); + } +} diff --git a/crates/onestore/src/page/write.rs b/crates/onestore/src/page/write.rs index 70c47fe6d5cf45bbdfc9b91e24d1fff287d1783d..35abf42da6ecd2792374cb28cac9005215e7d432 100644 --- a/crates/onestore/src/page/write.rs +++ b/crates/onestore/src/page/write.rs @@ -1,7 +1,7 @@ //! Publishes an edited page model by lowering the difference from the stored page onto //! the typed writers, then squashing their transactions into one revision per space. -use super::{Image, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; +use super::{Image, Ink, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; use crate::{ Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets, RevisionIndex, Store, TextAttribute, TreeEdit, Value, @@ -35,6 +35,155 @@ fn page_images(page: &Page) -> impl Iterator { }) } +fn page_ink(page: &Page) -> impl Iterator { + page.objects.iter().filter_map(|object| match object { + PageObject::Ink(ink) => Some((ink.id, ink)), + _ => None, + }) +} + +/// The compact child references a page object holds. +fn page_children(object: &PropertyObject) -> Result, Error> { + let properties = crate::PropertySets::parse(&object.bytes)?; + match properties.sets[0] + .iter() + .find(|p| p.id == 0x24001c20) + .map(|p| &p.value) + { + Some(crate::Value::References { compact_ids, .. }) => Ok(compact_ids.to_vec()), + None => Ok(Vec::new()), + _ => Err(invalid("The page has an invalid child list")), + } +} + +/// Creates stroke objects (numbered from `first`) and one drawing-attribute object per +/// distinct pen; returns the compact references for the data node's stroke list. +fn write_strokes( + changed: &mut BTreeMap, + data_object: &mut PropertyObject, + strokes: &[(ExGuid, &super::InkStroke)], + first: usize, + filetime: u64, +) -> Result, Error> { + let mut styles: Vec<(InkPen, ExGuid)> = Vec::new(); + let mut references = Vec::new(); + for (offset, (stroke_id, stroke)) in strokes.iter().enumerate() { + let pen = InkPen::of(stroke); + let style = match styles.iter().find(|(known, _)| *known == pen) { + Some((_, id)) => *id, + None => { + let id = ExGuid { + guid: crate::write::fresh_guid()?, + n: 1, + }; + let mut style = PropertyObject { + jcid: 0x120048, + bytes: crate::create::properties(&pen.values())?, + global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])), + }; + style.reference(id)?; + changed.insert(id, style); + styles.push((pen, id)); + id + } + }; + let mut object = PropertyObject { + jcid: 0x20047, + bytes: crate::create::properties(&stroke_values( + stroke, + (first + offset) as u32 + 1, + filetime, + )?)?, + global_ids: std::sync::Arc::new(BTreeMap::from([(0, stroke_id.guid)])), + }; + object.reference(*stroke_id)?; + let style_reference = object.reference(style)?; + object.set(&[(0x20003409, &style_reference)])?; + changed.insert(*stroke_id, object); + references.extend(data_object.reference(*stroke_id)?); + } + Ok(references) +} + +/// The drawing attributes OneNote shares between strokes drawn with the same pen. +#[derive(PartialEq)] +struct InkPen { + width: u32, + height: u32, + color: Option, + transparency: Option, + pen_tip: Option, +} + +impl InkPen { + fn of(stroke: &super::InkStroke) -> Self { + Self { + width: (stroke.width * 2540.0 / 72.0).to_bits(), + height: (stroke.height * 2540.0 / 72.0).to_bits(), + color: stroke.color, + transparency: stroke.transparency, + pen_tip: stroke.pen_tip, + } + } + + fn values(&self) -> Values { + let mut values: Values = vec![ + (0x1c00340a, super::ink::DIMENSIONS.to_vec()), + (0x1400340c, self.height.to_le_bytes().to_vec()), + (0x1400340d, self.width.to_le_bytes().to_vec()), + ]; + if let Some(color) = self.color { + values.push((0x1400340f, color.to_le_bytes().to_vec())); + } + if let Some(transparency) = self.transparency { + values.push((0x0c003414, vec![transparency])); + } + if let Some(tip) = self.pen_tip { + values.push((0x0c003412, vec![tip])); + } + values + } +} + +fn stroke_values(stroke: &super::InkStroke, index: u32, filetime: u64) -> Result { + if stroke.points.is_empty() { + return Err(invalid("A stroke needs at least one point")); + } + if stroke + .points + .iter() + .any(|p| !p[0].is_finite() || !p[1].is_finite()) + || !(stroke.width.is_finite() && stroke.height.is_finite()) + || stroke.width <= 0.0 + || stroke.height <= 0.0 + { + return Err(invalid( + "Stroke points and pen size must be finite and positive", + )); + } + let left = stroke + .points + .iter() + .map(|p| p[0]) + .fold(f32::INFINITY, f32::min); + let top = stroke + .points + .iter() + .map(|p| p[1]) + .fold(f32::INFINITY, f32::min); + let mut origin = ((left - stroke.width / 2.0) / 36.0).to_le_bytes().to_vec(); + origin.extend_from_slice(&((top - stroke.height / 2.0) / 36.0).to_le_bytes()); + Ok(vec![ + (0x1c00340b, stroke.packet()), + (0x14003419, index.to_le_bytes().to_vec()), + (0x1000341b, 0x409_u16.to_le_bytes().to_vec()), + (0x0c00341c, vec![0]), + (0x1c00341a, crate::write::fresh_guid()?.to_vec()), + (0x1c00341d, filetime.to_le_bytes().to_vec()), + (0x1c00345b, origin), + ]) +} + fn picture_fixed_fields(stored: &Image, image: &Image) -> Result<(), Error> { if stored.id != image.id || stored.bytes != image.bytes @@ -366,7 +515,6 @@ impl Lowering<'_> { .iter() .filter_map(|object| match object { PageObject::Unsupported(unsupported) => Some(format!("{unsupported:?}")), - PageObject::Ink(ink) => Some(format!("{ink:?}")), PageObject::Title(title) => Some(format!( "{:?} {:?} {:?} {:?}", title.id, @@ -374,7 +522,7 @@ impl Lowering<'_> { title.layout, title.outlines.iter().map(|o| o.id).collect::>() )), - PageObject::Outline(_) | PageObject::Image(_) => None, + PageObject::Outline(_) | PageObject::Image(_) | PageObject::Ink(_) => None, }) .collect() }; @@ -384,7 +532,7 @@ impl Lowering<'_> { after_fixed.sort(); if before_fixed != after_fixed { return Err(invalid( - "Titles, ink and unsupported objects cannot be edited through the page model", + "Titles and unsupported objects cannot be edited through the page model", )); } for (id, outline) in &new.outlines { @@ -447,6 +595,11 @@ impl Lowering<'_> { } } (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {} + (ParagraphContent::Ink(a), ParagraphContent::Ink(b)) => { + if a.id != b.id { + return Err(invalid("Ink identity cannot change")); + } + } (ParagraphContent::Image(a), ParagraphContent::Image(b)) => { picture_fixed_fields(a, b)?; if (a.layout.x, a.layout.y) != (b.layout.x, b.layout.y) { @@ -637,6 +790,7 @@ impl Lowering<'_> { .any(|object| matches!(object, PageObject::Title(title) if title.id == id)) }; self.edit_page_images(old, new)?; + self.edit_page_ink(old, new)?; let survivors: Vec = page_order .iter() .copied() @@ -693,6 +847,7 @@ impl Lowering<'_> { self.edit_table_structure(old, new)?; self.edit_cells(old, new)?; self.edit_images(old, new)?; + self.edit_ink_paragraphs(old, new)?; self.edit_attachments(old, new)?; self.place_containers(old, new, placed, &deferred)?; Ok(()) @@ -860,6 +1015,179 @@ impl Lowering<'_> { Ok(()) } + /// Ink placed on the page: new drawings are appended for the placement pass to order, + /// changed ones have their stroke list rewritten, removed ones go in the delete pass. + fn edit_page_ink(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> { + let stored: BTreeMap = page_ink(old.page).collect(); + for (id, ink) in page_ink(new.page) { + match stored.get(&id) { + Some(previous) => { + if *previous != ink { + self.edit_ink(previous, ink)?; + } + } + None => self.insert_ink(ink, None)?, + } + } + Ok(()) + } + + fn edit_ink_paragraphs(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> { + for (paragraph_id, paragraph) in &new.paragraphs { + let ParagraphContent::Ink(ink) = ¶graph.content else { + continue; + }; + match old.paragraphs.get(paragraph_id) { + Some(previous) => { + let ParagraphContent::Ink(stored) = &previous.content else { + return Err(invalid("Paragraph content type cannot change")); + }; + if stored != ink { + self.edit_ink(stored, ink)?; + } + } + None => { + let holder = self.id(*paragraph_id); + self.insert_ink(ink, Some(holder))?; + } + } + } + Ok(()) + } + + /// Writes new ink the way OneNote 2010 stores a drawing: a container the page lists as a + /// child (or a paragraph holds as content), its data node listing stroke objects, each + /// stroke's packet and half-inch origin, and one drawing-attribute object per distinct pen. + fn insert_ink(&mut self, ink: &Ink, holder: Option) -> Result<(), Error> { + if !ink.groups.is_empty() { + return Err(invalid("New ink holds strokes, not nested groups")); + } + if ink.strokes.is_empty() { + return Err(invalid("New ink needs at least one stroke")); + } + if ink.layout != Default::default() { + return Err(invalid("Ink positions come from its strokes")); + } + let container = self.allocate(ink.id)?; + let data = ExGuid { + guid: crate::write::fresh_guid()?, + n: 1, + }; + let mut strokes = Vec::new(); + for stroke in &ink.strokes { + strokes.push((self.allocate(stroke.id)?, stroke)); + } + let (modified, filetime) = crate::create::current_timestamps()?; + let modified = modified.to_le_bytes(); + let (space, page) = (self.space, self.page); + self.apply(|current| { + crate::write::write_revision(current, space, |raw| { + let mut changed = BTreeMap::new(); + let mut data_object = PropertyObject { + jcid: 0x2003b, + bytes: crate::create::properties(&[])?, + global_ids: std::sync::Arc::new(BTreeMap::from([(0, data.guid)])), + }; + data_object.reference(data)?; + let references = + write_strokes(&mut changed, &mut data_object, &strokes, 0, filetime)?; + data_object.set(&[(0x24003416, &references)])?; + changed.insert(data, data_object); + let mut object = PropertyObject { + jcid: 0x60014, + bytes: crate::create::properties(&[ + (0x14001d7a, modified.to_vec()), + (0x14001d4e, 1u32.to_le_bytes().to_vec()), + ])?, + global_ids: std::sync::Arc::new(BTreeMap::from([(0, container.guid)])), + }; + object.reference(container)?; + let data_reference = object.reference(data)?; + object.set(&[(0x20003415, &data_reference)])?; + changed.insert(container, object); + match holder { + Some(holder) => { + let mut object = PropertyObject::from_object(&raw.objects[&holder])?; + let content = object.reference(container)?; + object.set(&[(0x24001c1f, &content), (0x14001d7a, &modified)])?; + changed.insert(holder, object); + } + None => { + let mut object = PropertyObject::from_object(&raw.objects[&page])?; + let mut children = page_children(&object)?; + children.extend(object.reference(container)?); + object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?; + changed.insert(page, object); + } + } + Ok(changed) + }) + }) + } + + /// Rewrites a drawing's stroke list: stored strokes stay as they are (OneNote erases whole + /// strokes rather than editing them), removed ones leave the list, new ones are created. + fn edit_ink(&mut self, stored: &Ink, ink: &Ink) -> Result<(), Error> { + if stored.id != ink.id { + return Err(invalid("Ink identity cannot change")); + } + if stored.layout != ink.layout || stored.groups != ink.groups { + return Err(invalid("Ink position and groups stay as stored")); + } + let mut kept = Vec::new(); + let mut added = Vec::new(); + for stroke in &ink.strokes { + match stored.strokes.iter().find(|s| s.id == stroke.id) { + Some(previous) if previous == stroke => kept.push(self.id(stroke.id)), + Some(_) => return Err(invalid("A stored stroke keeps its path and pen")), + None => added.push((self.allocate(stroke.id)?, stroke)), + } + } + let container = self.id(ink.id); + let data = { + let store = Store::parse(&self.image)?; + let index = RevisionIndex::parse(&store)?; + let document = Document::parse(&index)?; + let revision = document + .spaces + .get(&self.space) + .and_then(crate::document::Space::active) + .ok_or_else(|| invalid("The page space is unavailable"))?; + match revision.nodes.get(&container).map(|node| &node.kind) { + Some(crate::document::Kind::Ink { + data: Some(data), .. + }) => *data, + _ => return Err(invalid("Stored ink has no stroke data to rewrite")), + } + }; + let (modified, filetime) = crate::create::current_timestamps()?; + let modified = modified.to_le_bytes(); + let space = self.space; + self.apply(|current| { + crate::write::write_revision(current, space, |raw| { + let mut changed = BTreeMap::new(); + let mut data_object = PropertyObject::from_object(&raw.objects[&data])?; + let mut references = Vec::new(); + for id in &kept { + references.extend(data_object.reference(*id)?); + } + references.extend(write_strokes( + &mut changed, + &mut data_object, + &added, + kept.len(), + filetime, + )?); + data_object.set(&[(0x24003416, &references)])?; + changed.insert(data, data_object); + let mut object = PropertyObject::from_object(&raw.objects[&container])?; + object.set(&[(0x14001d7a, &modified)])?; + changed.insert(container, object); + Ok(changed) + }) + }) + } + /// Gives a new picture what OneNote stores for an inserted one: the payload embedded /// in the section's file-data store, a file-data object declaring it by identity and /// extension, and a picture object that a paragraph holds as content or the page @@ -928,18 +1256,7 @@ impl Lowering<'_> { } None => { let mut object = PropertyObject::from_object(&raw.objects[&page])?; - let properties = crate::PropertySets::parse(&object.bytes)?; - let mut children = match properties.sets[0] - .iter() - .find(|p| p.id == 0x24001c20) - .map(|p| &p.value) - { - Some(crate::Value::References { compact_ids, .. }) => { - compact_ids.to_vec() - } - None => Vec::new(), - _ => return Err(invalid("The page has an invalid child list")), - }; + let mut children = page_children(&object)?; children.extend(object.reference(image_id)?); object.set(&[(0x24001c20, &children), (0x14001d7a, &modified)])?; changed.insert(page, object); @@ -1023,7 +1340,8 @@ impl Lowering<'_> { } ParagraphContent::Table(_) | ParagraphContent::Image(_) - | ParagraphContent::Attachment(_) => ("", None), + | ParagraphContent::Attachment(_) + | ParagraphContent::Ink(_) => ("", None), _ => { return Err(invalid( "New paragraphs contain plain text without fields or styles", @@ -1334,8 +1652,14 @@ impl Lowering<'_> { let space = self.space; self.apply(|image| edit.apply(image, space))?; } - let kept: BTreeSet = page_images(new.page).map(|(id, _)| id).collect(); - for (id, _) in page_images(old.page) { + let kept: BTreeSet = page_images(new.page) + .map(|(id, _)| id) + .chain(page_ink(new.page).map(|(id, _)| id)) + .collect(); + for id in page_images(old.page) + .map(|(id, _)| id) + .chain(page_ink(old.page).map(|(id, _)| id)) + { if !kept.contains(&id) { let edit = TreeEdit::delete(id, self.author)?; let space = self.space; diff --git a/crates/onestore/src/tree.rs b/crates/onestore/src/tree.rs index 73822329e127c179e4e62d33ef1af3292ced9e47..e3d4d8d8e576d34af0506289cb86299d095834ba 100644 --- a/crates/onestore/src/tree.rs +++ b/crates/onestore/src/tree.rs @@ -115,7 +115,7 @@ impl TreeEdit { .get(1) .ok_or_else(|| invalid("Select a paragraph or ordinary page outline"))?; let compatible = |parent: ExGuid| match view.nodes[&self.object].kind { - Kind::Outline { .. } | Kind::Image { .. } => parent == *page, + Kind::Outline { .. } | Kind::Image { .. } | Kind::Ink { .. } => parent == *page, Kind::Paragraph { .. } => matches!( view.nodes[&parent].kind, Kind::Outline { .. } diff --git a/crates/onestore/tests/page_ink.rs b/crates/onestore/tests/page_ink.rs index 4b7f9935b1030c70363a25d85e3a00532f460cc2..a27a31732ea125864e778834dab907ad05fbee22 100644 --- a/crates/onestore/tests/page_ink.rs +++ b/crates/onestore/tests/page_ink.rs @@ -1,9 +1,11 @@ use onestore::{ - ExGuid, RevisionIndex, Store, + ExGuid, PreparedEdit, RevisionIndex, Store, document::Document, - page::{Ink, Page, PageObject}, + page::{Ink, InkStroke, Page, PageObject, text::new_id}, }; +const AUTHOR: &str = "Ink author"; + /// Two drawings made with the mouse in OneNote 2010 (`tools/native/ink.ahk`): a diamond and a /// diagonal line, whose native read reports their positions and sizes. const NATIVE: &[u8] = @@ -74,3 +76,159 @@ fn native_ink_drawings_read_as_strokes_in_page_coordinates() { })); } } + +use onestore::page::ink::snap; + +fn stroke(points: &[[f32; 2]], color: Option) -> InkStroke { + InkStroke { + id: new_id().unwrap(), + points: points.iter().map(|p| [snap(p[0]), snap(p[1])]).collect(), + width: snap(1.0), + height: snap(1.0), + color, + transparency: None, + pen_tip: None, + } +} + +fn drawing() -> Ink { + Ink { + id: new_id().unwrap(), + layout: Default::default(), + strokes: vec![ + stroke( + &[ + [300.0, 120.0], + [360.0, 120.0], + [360.0, 180.0], + [300.0, 180.0], + [300.0, 120.0], + ], + None, + ), + stroke(&[[300.0, 120.0], [360.0, 180.0]], Some(0x0000ff)), + ], + groups: Vec::new(), + } +} + +fn page_in(bytes: &[u8], space: ExGuid) -> Page { + let store = Store::parse(bytes).unwrap(); + assert!(store.checksum_mismatches.is_empty()); + let index = RevisionIndex::parse(&store).unwrap(); + Page::from_space(&Document::parse(&index).unwrap(), space).unwrap() +} + +fn export(variable: &str, name: &str, written: &[u8]) { + if let Some(directory) = std::env::var_os(variable) { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir(&directory).unwrap(); + std::fs::write(directory.join(name), written).unwrap(); + let file_id = Store::parse(written).unwrap().header.file_id; + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents("Open Notebook.onetoc2", &[(name, file_id)]) + .unwrap(), + ) + .unwrap(); + } +} + +/// `ONESTORE_INK_EXPORT` names a new directory receiving the candidate for a cold reopen. +#[test] +fn an_ink_drawing_is_written_erased_stroke_by_stroke_and_removed() { + let source = onestore::create_section("ink.one", "Beside the drawing", "Author").unwrap(); + let (space, before) = first_page(&source); + let mut after = before.clone(); + let ink = drawing(); + after.objects.push(PageObject::Ink(ink.clone())); + let written = PreparedEdit::page(&source, space, &after, AUTHOR).unwrap(); + let stored = page_in(written.as_bytes(), space); + let mut expected = after.clone(); + expected.title = stored.title.clone(); + assert_eq!(stored, expected); + assert!(close( + drawings(&stored)[0].bounds().unwrap(), + [ + snap(300.0), + snap(120.0), + snap(360.0) - snap(300.0), + snap(180.0) - snap(120.0) + ] + )); + assert_eq!( + PreparedEdit::page(written.as_bytes(), space, &stored, AUTHOR) + .unwrap() + .as_bytes(), + written.as_bytes() + ); + export("ONESTORE_INK_EXPORT", "ink.one", written.as_bytes()); + let mut erased = stored.clone(); + for object in &mut erased.objects { + if let PageObject::Ink(ink) = object { + ink.strokes.remove(1); + ink.strokes + .push(stroke(&[[330.0, 100.0], [330.0, 200.0]], Some(0xff0000))); + } + } + let again = PreparedEdit::page(written.as_bytes(), space, &erased, AUTHOR).unwrap(); + let stored = page_in(again.as_bytes(), space); + assert_eq!(stored, erased); + let mut removed = stored.clone(); + removed.objects.retain(|object| object.id() != ink.id); + let last = PreparedEdit::page(again.as_bytes(), space, &removed, AUTHOR).unwrap(); + let stored = page_in(last.as_bytes(), space); + assert!(drawings(&stored).is_empty()); + assert_eq!(stored, removed); +} + +/// `ONESTORE_INK_PARAGRAPH_EXPORT` names a new directory receiving the candidate for a cold +/// reopen. +#[test] +fn handwriting_is_written_as_paragraph_content() { + use onestore::page::{PageParagraph, ParagraphContent}; + let source = onestore::create_section("ink.one", "Above the handwriting", "Author").unwrap(); + let (space, before) = first_page(&source); + let mut after = before.clone(); + let outline = after + .objects + .iter_mut() + .find_map(|object| match object { + PageObject::Outline(outline) if !outline.title => Some(outline), + _ => None, + }) + .unwrap(); + let mut paragraph: PageParagraph = outline.paragraphs[0].clone(); + paragraph.id = new_id().unwrap(); + paragraph.parent = None; + paragraph.level = 1; + paragraph.lists.clear(); + paragraph.tags.clear(); + paragraph.style = None; + paragraph.collapsed = false; + paragraph.format = Default::default(); + paragraph.content = ParagraphContent::Ink(drawing()); + outline.paragraphs.push(paragraph); + let written = PreparedEdit::page(&source, space, &after, AUTHOR).unwrap(); + let stored = page_in(written.as_bytes(), space); + let mut expected = after.clone(); + expected.title = stored.title.clone(); + assert_eq!(stored, expected); + export( + "ONESTORE_INK_PARAGRAPH_EXPORT", + "ink.one", + written.as_bytes(), + ); +} + +#[test] +fn stored_strokes_keep_their_paths() { + let (space, page) = first_page(NATIVE); + let mut moved = page.clone(); + for object in &mut moved.objects { + if let PageObject::Ink(ink) = object { + ink.strokes[0].points[0][0] += 1.0; + } + } + assert!(PreparedEdit::page(NATIVE, space, &moved, AUTHOR).is_err()); +} diff --git a/tools/test_ink_edit.py b/tools/test_ink_edit.py new file mode 100644 index 0000000000000000000000000000000000000000..eae34b3b762686cb155f20c8efd991c233cd1fa0 --- /dev/null +++ b/tools/test_ink_edit.py @@ -0,0 +1,41 @@ +from pathlib import Path +import runpy +import shutil +from tempfile import TemporaryDirectory +import unittest +import xml.etree.ElementTree as ET + +from native_xml import ns + +ROOT = Path(__file__).resolve().parent.parent +FIXTURE = ROOT / 'corpus/ink-edit' +compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] + + +def native_page(variant): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(FIXTURE / variant / 'cold/read', read) + compare(FIXTURE / variant / 'candidate', read) + page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / variant / 'cold/read').glob('page-*.xml'))) + return page + + +class InkEditTest(unittest.TestCase): + def test_a_rust_drawing_renders_natively_at_its_stroke_extent(self): + page = native_page('drawing') + drawing, = page.findall('one:InkDrawing', ns) + position, size = drawing.find('one:Position', ns), drawing.find('one:Size', ns) + self.assertAlmostEqual(float(position.get('x')), 300.0, delta=0.03) + self.assertAlmostEqual(float(position.get('y')), 120.0, delta=0.03) + self.assertAlmostEqual(float(size.get('width')), 60.0, delta=0.06) + self.assertAlmostEqual(float(size.get('height')), 60.0, delta=0.06) + self.assertTrue((FIXTURE / 'drawing/cold/read/page-000.png').exists()) + + def test_rust_handwriting_renders_natively_as_an_ink_paragraph(self): + page = native_page('handwriting') + self.assertEqual(len(list(page.iter('{%s}InkParagraph' % ns['one']))) + len(list(page.iter('{%s}InkDrawing' % ns['one']))), 1) + + +if __name__ == '__main__': + unittest.main() -- 2.54.0