From d0ec66f9b4d3023668fa156d8a0903b3b5b8a316 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 12 Sep 2026 14:31:17 -0700 Subject: [PATCH] feat: resize and describe stored pictures through the page model A picture paragraph in the model can change its layout width and height, the user-set flag and its alternative text; the writer stores exactly what OneNote 2010 was observed to store when it resized a Rust-written picture through the COM API (corpus/picture-edit/native-resize, authored by tools/native/picture-edit.ps1): the layout size and user flag on the picture object, with the intrinsic picture size untouched. Clearing the size or the description removes the properties, so a reset page equals the original. The payload, intrinsic size, background state and position stay fixed. corpus/picture-edit now holds three captures: inserted (the Rust-inserted picture cold read), native-resize (OneNote editing that picture, kept as a reader fixture for a Rust picture rewritten natively with its DPAPI blob and hash) and resized (a Rust resize of the native picture cold read with its size and description). tools/test_picture_edit.py checks all three. Assisted-by: claude-fable-5.1 --- corpus/picture-edit/README.md | 32 +++- .../candidate/Open Notebook.onetoc2 | Bin .../{ => inserted}/candidate/pictures.one | Bin .../{ => inserted}/cold/commands.jsonl | 0 .../{ => inserted}/cold/machine.json | 0 .../cold/notebook/Open Notebook.onetoc2 | Bin .../{ => inserted}/cold/notebook/pictures.one | Bin .../{ => inserted}/cold/read/environment.json | 0 .../{ => inserted}/cold/read/hierarchy.xml | 0 .../{ => inserted}/cold/read/page-000.xml | 0 .../{ => inserted}/cold/read/payloads.json | 0 .../picture-edit/{ => inserted}/cold/run.json | 0 .../{ => inserted}/cold/scripts/cold.ps1 | 0 .../{ => inserted}/cold/scripts/read.ps1 | 0 .../{ => inserted}/cold/source.json | 0 .../{ => inserted}/cold/teardown.json | 0 corpus/picture-edit/native-resize/before.xml | 4 + .../picture-edit/native-resize/commands.jsonl | 4 + .../picture-edit/native-resize/machine.json | 1 + .../notebook/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes .../native-resize/notebook/pictures.one | Bin 0 -> 16784 bytes .../native-resize/read/environment.json | 7 + .../native-resize/read/hierarchy.xml | 2 + .../native-resize/read/page-000.xml | 4 + .../native-resize/read/payloads.json | 3 + corpus/picture-edit/native-resize/run.json | 19 +++ .../native-resize/scripts/author.ps1 | 49 ++++++ .../native-resize/scripts/cold.ps1 | 27 ++++ .../native-resize/scripts/read.ps1 | 142 ++++++++++++++++++ corpus/picture-edit/native-resize/source.json | 14 ++ .../picture-edit/native-resize/teardown.json | 1 + corpus/picture-edit/native-resize/update.xml | 3 + .../resized/candidate/Open Notebook.onetoc2 | Bin 0 -> 1612 bytes .../resized/candidate/synthetic.one | Bin 0 -> 19040 bytes .../picture-edit/resized/cold/commands.jsonl | 3 + corpus/picture-edit/resized/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3780 bytes .../resized/cold/notebook/synthetic.one | Bin 0 -> 19040 bytes .../resized/cold/read/environment.json | 7 + .../resized/cold/read/hierarchy.xml | 2 + .../resized/cold/read/page-000.xml | 8 + .../resized/cold/read/payloads.json | 3 + corpus/picture-edit/resized/cold/run.json | 18 +++ .../resized/cold/scripts/cold.ps1 | 27 ++++ .../resized/cold/scripts/read.ps1 | 142 ++++++++++++++++++ corpus/picture-edit/resized/cold/source.json | 14 ++ .../picture-edit/resized/cold/teardown.json | 1 + crates/notebook/README.md | 2 +- crates/onestore/README.md | 2 +- crates/onestore/src/page/write.rs | 67 ++++++++- crates/onestore/tests/page_images.rs | 83 +++++++++- tools/native/picture-edit.ps1 | 49 ++++++ tools/test_picture_edit.py | 38 ++++- 53 files changed, 756 insertions(+), 23 deletions(-) rename corpus/picture-edit/{ => inserted}/candidate/Open Notebook.onetoc2 (100%) rename corpus/picture-edit/{ => inserted}/candidate/pictures.one (100%) rename corpus/picture-edit/{ => inserted}/cold/commands.jsonl (100%) rename corpus/picture-edit/{ => inserted}/cold/machine.json (100%) rename corpus/picture-edit/{ => inserted}/cold/notebook/Open Notebook.onetoc2 (100%) rename corpus/picture-edit/{ => inserted}/cold/notebook/pictures.one (100%) rename corpus/picture-edit/{ => inserted}/cold/read/environment.json (100%) rename corpus/picture-edit/{ => inserted}/cold/read/hierarchy.xml (100%) rename corpus/picture-edit/{ => inserted}/cold/read/page-000.xml (100%) rename corpus/picture-edit/{ => inserted}/cold/read/payloads.json (100%) rename corpus/picture-edit/{ => inserted}/cold/run.json (100%) rename corpus/picture-edit/{ => inserted}/cold/scripts/cold.ps1 (100%) rename corpus/picture-edit/{ => inserted}/cold/scripts/read.ps1 (100%) rename corpus/picture-edit/{ => inserted}/cold/source.json (100%) rename corpus/picture-edit/{ => inserted}/cold/teardown.json (100%) create mode 100644 corpus/picture-edit/native-resize/before.xml create mode 100644 corpus/picture-edit/native-resize/commands.jsonl create mode 100644 corpus/picture-edit/native-resize/machine.json create mode 100644 corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 create mode 100644 corpus/picture-edit/native-resize/notebook/pictures.one create mode 100644 corpus/picture-edit/native-resize/read/environment.json create mode 100644 corpus/picture-edit/native-resize/read/hierarchy.xml create mode 100644 corpus/picture-edit/native-resize/read/page-000.xml create mode 100644 corpus/picture-edit/native-resize/read/payloads.json create mode 100644 corpus/picture-edit/native-resize/run.json create mode 100644 corpus/picture-edit/native-resize/scripts/author.ps1 create mode 100644 corpus/picture-edit/native-resize/scripts/cold.ps1 create mode 100644 corpus/picture-edit/native-resize/scripts/read.ps1 create mode 100644 corpus/picture-edit/native-resize/source.json create mode 100644 corpus/picture-edit/native-resize/teardown.json create mode 100644 corpus/picture-edit/native-resize/update.xml create mode 100644 corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 create mode 100644 corpus/picture-edit/resized/candidate/synthetic.one create mode 100644 corpus/picture-edit/resized/cold/commands.jsonl create mode 100644 corpus/picture-edit/resized/cold/machine.json create mode 100644 corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/picture-edit/resized/cold/notebook/synthetic.one create mode 100644 corpus/picture-edit/resized/cold/read/environment.json create mode 100644 corpus/picture-edit/resized/cold/read/hierarchy.xml create mode 100644 corpus/picture-edit/resized/cold/read/page-000.xml create mode 100644 corpus/picture-edit/resized/cold/read/payloads.json create mode 100644 corpus/picture-edit/resized/cold/run.json create mode 100644 corpus/picture-edit/resized/cold/scripts/cold.ps1 create mode 100644 corpus/picture-edit/resized/cold/scripts/read.ps1 create mode 100644 corpus/picture-edit/resized/cold/source.json create mode 100644 corpus/picture-edit/resized/cold/teardown.json create mode 100644 tools/native/picture-edit.ps1 diff --git a/corpus/picture-edit/README.md b/corpus/picture-edit/README.md index d4793d321000015fda144dd5745d11cbf10c2985..dff677ee50977d55b8098d7c17b27ee03bdb848d 100644 --- a/corpus/picture-edit/README.md +++ b/corpus/picture-edit/README.md @@ -1,6 +1,6 @@ # Rust picture authoring -`candidate/` is the page writer's output for +`inserted/candidate` is the page writer's output for `a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed` in `crates/onestore/tests/page_images.rs`: on a section created in Rust, a paragraph between two text paragraphs becomes a picture, stored the way @@ -10,11 +10,27 @@ object, a picture-container file object declaring it by identity and extension, and a picture object with the displayed size that the paragraph holds as content. OneNote's own picture objects also carry a DPAPI-protected blob (`0x1c001dfb`) that only the authoring Windows user can decrypt; the -writer omits it. +writer omits it. `inserted/cold` is a fresh OneNote 2010 read: the image +reads back as `format="png"` with the exact payload bytes, between the two +text paragraphs. -`cold/` is a fresh OneNote 2010 read: the image reads back as `format="png"` -with the exact payload bytes, between the two text paragraphs. -`tools/test_picture_edit.py` checks this without a VM. Regenerate with -`ONESTORE_IMAGE_EXPORT` set to a new absolute directory while running the -test, then cold-open it with `tools/native_runner.py OUTPUT COLD ---expected-pages 1 --collect-notebook`. +`native-resize` is OneNote 2010 editing that Rust-written picture +(`tools/native/picture-edit.ps1` on `inserted/candidate`): the picture gets +a 144 by 108 point user-set size and alternative text through the COM API +(`before.xml`, `update.xml`), and `read/` is the read after the edit. In +`notebook/`, OneNote stored the size as the picture object's layout width +and height with the user flag (`0x14001c1b`, `0x14001c1c`, `0x08001cbd`) and +left the intrinsic size (`0x140034cd/ce`) alone; it also rewrote the picture +container under a native identity with its DPAPI blob and hash. + +`resized/candidate` is the writer doing the same on the native picture of +`cold-05-05-image` (`a_native_picture_is_resized_and_described_then_reset`): +layout width, height, user flag and alternative text on the picture object. +`resized/cold` is its cold read with the size and description. + +`tools/test_picture_edit.py` checks all three without a VM. Regenerate the +candidates with `ONESTORE_IMAGE_EXPORT` and `ONESTORE_IMAGE_RESIZE_EXPORT` +set to new absolute directories while running the tests, then cold-open +them with `tools/native_runner.py OUTPUT COLD --expected-pages 1 +--collect-notebook`; regenerate `native-resize` with `--author +tools/native/picture-edit.ps1` on `inserted/candidate`. diff --git a/corpus/picture-edit/candidate/Open Notebook.onetoc2 b/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 similarity index 100% rename from corpus/picture-edit/candidate/Open Notebook.onetoc2 rename to corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 diff --git a/corpus/picture-edit/candidate/pictures.one b/corpus/picture-edit/inserted/candidate/pictures.one similarity index 100% rename from corpus/picture-edit/candidate/pictures.one rename to corpus/picture-edit/inserted/candidate/pictures.one diff --git a/corpus/picture-edit/cold/commands.jsonl b/corpus/picture-edit/inserted/cold/commands.jsonl similarity index 100% rename from corpus/picture-edit/cold/commands.jsonl rename to corpus/picture-edit/inserted/cold/commands.jsonl diff --git a/corpus/picture-edit/cold/machine.json b/corpus/picture-edit/inserted/cold/machine.json similarity index 100% rename from corpus/picture-edit/cold/machine.json rename to corpus/picture-edit/inserted/cold/machine.json diff --git a/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 similarity index 100% rename from corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 rename to corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 diff --git a/corpus/picture-edit/cold/notebook/pictures.one b/corpus/picture-edit/inserted/cold/notebook/pictures.one similarity index 100% rename from corpus/picture-edit/cold/notebook/pictures.one rename to corpus/picture-edit/inserted/cold/notebook/pictures.one diff --git a/corpus/picture-edit/cold/read/environment.json b/corpus/picture-edit/inserted/cold/read/environment.json similarity index 100% rename from corpus/picture-edit/cold/read/environment.json rename to corpus/picture-edit/inserted/cold/read/environment.json diff --git a/corpus/picture-edit/cold/read/hierarchy.xml b/corpus/picture-edit/inserted/cold/read/hierarchy.xml similarity index 100% rename from corpus/picture-edit/cold/read/hierarchy.xml rename to corpus/picture-edit/inserted/cold/read/hierarchy.xml diff --git a/corpus/picture-edit/cold/read/page-000.xml b/corpus/picture-edit/inserted/cold/read/page-000.xml similarity index 100% rename from corpus/picture-edit/cold/read/page-000.xml rename to corpus/picture-edit/inserted/cold/read/page-000.xml diff --git a/corpus/picture-edit/cold/read/payloads.json b/corpus/picture-edit/inserted/cold/read/payloads.json similarity index 100% rename from corpus/picture-edit/cold/read/payloads.json rename to corpus/picture-edit/inserted/cold/read/payloads.json diff --git a/corpus/picture-edit/cold/run.json b/corpus/picture-edit/inserted/cold/run.json similarity index 100% rename from corpus/picture-edit/cold/run.json rename to corpus/picture-edit/inserted/cold/run.json diff --git a/corpus/picture-edit/cold/scripts/cold.ps1 b/corpus/picture-edit/inserted/cold/scripts/cold.ps1 similarity index 100% rename from corpus/picture-edit/cold/scripts/cold.ps1 rename to corpus/picture-edit/inserted/cold/scripts/cold.ps1 diff --git a/corpus/picture-edit/cold/scripts/read.ps1 b/corpus/picture-edit/inserted/cold/scripts/read.ps1 similarity index 100% rename from corpus/picture-edit/cold/scripts/read.ps1 rename to corpus/picture-edit/inserted/cold/scripts/read.ps1 diff --git a/corpus/picture-edit/cold/source.json b/corpus/picture-edit/inserted/cold/source.json similarity index 100% rename from corpus/picture-edit/cold/source.json rename to corpus/picture-edit/inserted/cold/source.json diff --git a/corpus/picture-edit/cold/teardown.json b/corpus/picture-edit/inserted/cold/teardown.json similarity index 100% rename from corpus/picture-edit/cold/teardown.json rename to corpus/picture-edit/inserted/cold/teardown.json diff --git a/corpus/picture-edit/native-resize/before.xml b/corpus/picture-edit/native-resize/before.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f41ea80baf7882e9a64a22c1681073dc963fb1f --- /dev/null +++ b/corpus/picture-edit/native-resize/before.xml @@ -0,0 +1,4 @@ + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w +AAAAAElFTkSuQmCC + diff --git a/corpus/picture-edit/native-resize/commands.jsonl b/corpus/picture-edit/native-resize/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..74e3124d0078ba8d0441bca1bd2cdcf0a982ad48 --- /dev/null +++ b/corpus/picture-edit/native-resize/commands.jsonl @@ -0,0 +1,4 @@ +{"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\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-3BE178F1", "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-3BE178F1 -ExpectedPages 1 -UseCurrentCache", "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/picture-edit/native-resize/machine.json b/corpus/picture-edit/native-resize/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..2e39ea03be694091b4dc6f0da279a9afadc90614 --- /dev/null +++ b/corpus/picture-edit/native-resize/machine.json @@ -0,0 +1 @@ +{"name": "m6-3be178f1", "hostname": "ONE-M6-3BE178F1"} diff --git a/corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..86e88105642e35612d1f763f3b60ec98f580397f GIT binary patch literal 3764 zcmZ3e|KIuM`!b(-j|5(YE0x8s-QSbsU--|r#k3OSA3%l8K(@{xCzO79HC(<(QJX#|o)FmB3lm3lTc7v$kk1;|Ltw~AXkuTrBZmbGhA_(P+ z6fiWrNa0HP6f@1CBUJm?Nuf!#FxAp8fM#$P$JoEU;@aYL{Gre?jTo35%sg}&BCjaI z(7>RQ03?6_-FT2VNLkyKEPoLbSLfENDd#i)Rm*A!Ff`~@Fmy1mfOIi11OPF_3m_gy z2S_#mDk}k!1&f1ZKseF>=v*jU251(;7HF9TGSGXManoVZL)|d{3jq~^01wbH=ne!K zCg8v@Ap>d^*mR&6q@;tCb?D{^gA4-#jtM}0(DD)F0c>Wo;II>$`Ji|Lc?#rSP#F%g z2SkJ1xNKYflr11h5P-O~0ec#HW4Gt0ddUr^p1)szGVK!G0Mbo34FT-|ry-CPOyG0` zN|PYlx6Hc-Bw=9*awEt@=@$(RLU&n0;*XSgES0*mM{k)F3rHMW8frM`c;dIyT}F^R z1B0SS11P-!2_Ogxy_$YGc%4Vv&XVVo?AF~^HGqhNEC#VbT0mGsprN4$sK+C*>vUMI zqVLlEybAMIeS8R#bF^sanF5S{7A~MJR^5Px9v2{w3zj^92DGFw^dJW~P!j_~jV8k{ v8KALX&L}Y&0;3@S4FTk=0xSDK`Cb%=K^abS?uQJh(ot$O1V%$(1cU$pcv(CJ literal 0 HcmV?d00001 diff --git a/corpus/picture-edit/native-resize/notebook/pictures.one b/corpus/picture-edit/native-resize/notebook/pictures.one new file mode 100644 index 0000000000000000000000000000000000000000..93dd8f4a7aafdc3a8acb415f99f8186d4f6584b6 GIT binary patch literal 16784 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 X@(}2AulzX#9-ZLSfKvm%p$7g1&R=+^ literal 0 HcmV?d00001 diff --git a/corpus/picture-edit/native-resize/read/environment.json b/corpus/picture-edit/native-resize/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..86e4ad7fdad6944b936c59b65548ccb0d9fd272f --- /dev/null +++ b/corpus/picture-edit/native-resize/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-3BE178F1", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/picture-edit/native-resize/read/hierarchy.xml b/corpus/picture-edit/native-resize/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..4beee5e1266a09e9aa953eb88ce2a08b5b08832f --- /dev/null +++ b/corpus/picture-edit/native-resize/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/picture-edit/native-resize/read/page-000.xml b/corpus/picture-edit/native-resize/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ffa250df34a4a2024b61d80ad78e577a00fef0b --- /dev/null +++ b/corpus/picture-edit/native-resize/read/page-000.xml @@ -0,0 +1,4 @@ + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w +AAAAAElFTkSuQmCC + diff --git a/corpus/picture-edit/native-resize/read/payloads.json b/corpus/picture-edit/native-resize/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/picture-edit/native-resize/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/picture-edit/native-resize/run.json b/corpus/picture-edit/native-resize/run.json new file mode 100644 index 0000000000000000000000000000000000000000..52ab401ffc7c29caa752562d112bc1be65c5b0e6 --- /dev/null +++ b/corpus/picture-edit/native-resize/run.json @@ -0,0 +1,19 @@ +{ + "notebook": "/Users/clo/dev/one/corpus/picture-edit/candidate", + "expected_pages": 1, + "author": "tools/native/picture-edit.ps1", + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "author.ps1": "ac663d020a6049b4b688aed1be4ae2661a59fff25d83fe7c5eaabbf14d357f55", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/picture-edit/native-resize/scripts/author.ps1 b/corpus/picture-edit/native-resize/scripts/author.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..ca0597f1475c8e4cccbb45fb4ed95c28eded912d --- /dev/null +++ b/corpus/picture-edit/native-resize/scripts/author.ps1 @@ -0,0 +1,49 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $sectionId = '' + $app.OpenHierarchy('pictures.one', $notebookId, [ref]$sectionId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(30) + $page = $null + do { + $hierarchy = '' + $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) + if ($pages.Count -eq 1) { + $content = '' + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + if (@($page.SelectNodes('//*[local-name()="Image"]')).Count -eq 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + $images = @($page.SelectNodes('//*[local-name()="Image"]')) + if ($images.Count -ne 1) { throw 'Expected one picture on the page.' } + [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) + $image = $images[0] + $image.SetAttribute('alt', 'Resized by OneNote') + $existing = $image.SelectSingleNode('*[local-name()="Size"]') + if ($existing) { [void]$image.RemoveChild($existing) } + $size = $page.CreateElement('one', 'Size', $namespace) + $size.SetAttribute('width', '144') + $size.SetAttribute('height', '108') + $size.SetAttribute('isSetByUser', 'true') + [void]$image.PrependChild($size) + [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/corpus/picture-edit/native-resize/scripts/cold.ps1 b/corpus/picture-edit/native-resize/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/picture-edit/native-resize/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/picture-edit/native-resize/scripts/read.ps1 b/corpus/picture-edit/native-resize/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/picture-edit/native-resize/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/picture-edit/native-resize/source.json b/corpus/picture-edit/native-resize/source.json new file mode 100644 index 0000000000000000000000000000000000000000..0613aac0d637eacd23e38a986eb8d4aed828b657 --- /dev/null +++ b/corpus/picture-edit/native-resize/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a", + "mtime_ns": 1789243969388188834 + }, + { + "path": "pictures.one", + "bytes": 7064, + "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70", + "mtime_ns": 1789243969387957378 + } +] diff --git a/corpus/picture-edit/native-resize/teardown.json b/corpus/picture-edit/native-resize/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/picture-edit/native-resize/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/picture-edit/native-resize/update.xml b/corpus/picture-edit/native-resize/update.xml new file mode 100644 index 0000000000000000000000000000000000000000..db5fa9154539ae2d26e03c0b6846f2364198c7cd --- /dev/null +++ b/corpus/picture-edit/native-resize/update.xml @@ -0,0 +1,3 @@ +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w +AAAAAElFTkSuQmCC + \ No newline at end of file diff --git a/corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 b/corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..e26ea04d75bcdb38ca618ae744185e70d732a9f9 GIT binary patch literal 1612 zcmZ3e|KIuM`!b(-j|5(YE0x8|z1Yb3++do|d@-BnTQ*EFU_b-*cV`JmPyFh+^n&=r zw;)KmcMh!UUi+OdL%Z&RtpfsshddG8EV#JRKl)fEB_{fKYJK2dV}p zP{Gh=$#6WuakARnclN8_O@q0i&$ZpvGS9bxt|TF!iJKXb6np5NKdwm?Fy{ zvr~paBpXQY1JVM(;IMIM4_?sz$!(2UmGOi0eOE!bNCt?D87dj_7)ls27*c^`CPOlV z9z#A*Bo!!!$lXBI4O~Eik>Q&Rg94k@t><+ z>*~G~P5OR(4NSH43x?yA{IG1n94S-&TvSol~fkOipBHp`W2AW2&fPQcz}*UcOb|x0SAT&1yHNNrUS)5 zW`O`mAG&$MAVDDDm;lrVEhhtjd~9a3;II>7I;#1gcmjC}aJ2m-pU9fL|l)Z~kb z*%&dwBVY`8#i&tZJV?AXhv%vrc8$R!xOl`PZv8#eFpiChtL{cKHD7i2t9N$2*HwSL z?x~6!JbCu97166J`sEz*T5vQlWM;qOnO7rL`AzC{cGT8ymj{o7bY29cudK zrOMUE+EBhR3&rmd;o3zI7|w!R5OYJ8wY^#B7ExN^Uz5=CrNmZEbU^`HL#~X@mPXe| zgXoNQKu9L`#Ic^X^(|=aesEk&(ZTf-cPlm}`53aI_S7Eh&~ah-r?V0OfO`WEF(RfFJ@Z95R)m>iXg0z&_kr* z|1op`R63^J4-(Q8*A(ssm6iP3&DVzi6= z?7)^EBH!MS?y@wuKhkI$`OY({{)%xRA2`H{7;>#D<-_K51|fapgOhua#x?lrx&vd8 z#(0n({$=?Fd%E3?c;Cn^f0|lp)3$>O&y^VI<>dsOj!3jUF5VqGVr2iAwMDyU z{Bx#!E;92*u-V1~;v0EuqmN?(jUe8|E7;{qaj#89J*Tvq>HQNdBCM5Io?!LV4jhLO z))tIae)Ga6r8>`NvhP<3%h8$GsP!Yf=fF5Bz*DiXNXXC#{(~|&Df3n{b+Vc@gBAKH zxbKz?hd&FOwf{nVtbf6+axQvEZQyBrb7+UNVAI@~bAbc=^i*M8n;EA&NXVLeVE?$ipQ~z=|aYm+*mtd-gG`p zi(Ir*(53QD?UMB9HB)vR>{i=pW$gS{hGPDZ&l9I$|EBpy-Fq~T!dznhu?F?6_=R|l ze%W?j4IO4CZEEc>dnf7YEjki(ZZv~E^k;P$pU z?-8}ox=+FK_D!##Z7dUo)C!m0b?fG?M1{Gqq;+G#FQn@dr4_Y|g(P|Og(NoLf_`9s z&^auIgH$V~qC7E+ar@H9w}mHv^P=`c-=7d~?K1HluT^WuH0O0DVhteI_au>NoByS% z#i9O7?3*^ptKHyTy%(9HJ;62!J@wXU-#czI+|KLr`(b|XI(>&qV!hY*^mn4}sAU2Mn;PYqKF(F8d;85V3uFg-C%_A#l2{%*5%LAwnO%Cqqj%^pl2${eOle&G z3U3A}`vKCA&={XZA{YfxmVg&1#X&?wzEJShf+w!`zlg6v@V{cWYwUkb?@qv5lM6Rl z&D(b?d~R8}%hp@Xf5aWbZ{C$!;3gyL{N^301a}e-a8G?Njq7dj6a7@Zt5cZqw!+dxNpGgPKaPd@d8ulbWSSQ!+ zRG%BB?a!o*x@PY_moprrFkUA+Hw3lzem_?cuw?Nkv1i|^0`5YYhStxNiVcn3b}S6% zTBV);GHO61%3}R=wXgzSug6_oEF{?b2qO*X0lcq;9BT8g17cbWM`+9XsjRSHvqKm+ zEJwJ7R!l{r=qa`)@grCVQ=JLXn^>N4)Mr```oY?PRT)g}idhwmbI5jan+Svp3!`}Z z>Fv{q*>@VpL;UH}n-@|5hg}C}9)n=7=W%d|J>eJ!;y}VB;|Y^Mz0|>D;1bl{zi(gg zqicZg8qW-8O$Sxo{WYR}B_}nru^@IO9Hl09CT>hDj2XK{@NPuFPDCMeCZ?8RL*H5C z!ru|bMYf)U*#pDEEBJnL-IykaSVg)(KfqCFn{S^=@ray*TY!I1@^#wzFwHM&=1}BA z<8x=)U>Z-JVN5C+;*W?iNtt6Z$BfUKph-&^HYQb*nVggPBAOEzEk7S5*bBV30__;g z3YBujoGh=hoWQ={EAGWuvzi|+Kx`|wQ${Qc@LIes?4(Yax-R?nVTfJja?2~l7n5fX z`5{0#)qk{)(}xgeD6CvhU-#HD;XCEF>uav~`lL^X_%90O{-ygogT@^H{?md>B~2VZ zuZH+_g_UYxS?+=*uZ{fYsAZm<_sdiC<1ru28Zc|%$!MS>oR()>6$JC;DUQ%3QyoLE6@Lv^gk-dmGheUaV=y+rIytfV|!%2UzV zk{HdT>2HRhE5TyIIw^#9hO-Ph>fd7Pvkk|4bmpWC@i*dd)JGJ3e{7Aif*ljKPJnyF zLKS1&V;XA-ac$&+uPsvKmi8ULlxcs6N}VF$8`o9SFRF90WAX#SZ8DRZCBhr*%;PN} z+>!(z3rJ0EAqtS?gb#{F0g0&%MFFz7a8UXJrnVOlDoJJyuV+VoPr|>U7%Wr5-P-a3 zlQ2Nj1{xtEbqToESU*&c$7QiH8$c49x<-9uY9ZRz_ws_wzCL1qdO(dItodNpfLQ}i zLIYxU9?yIo)5X(?YXhhkvADJi(@?BCjE!U$SUb8-AGIn5=Ws;_MSF#(qBXs+|E&?O zUPFRC!#7kQ*n6=TkH+@?NyWM4GvZ{OPfm?`vuWG(4nyBPLh9u0>3%t^yiuIHQ~o=R zbN_eqL<)H(;@-#62-Y9&6AszxH^sl75f(}_#O)2o!2c>A9 zI?VoGj01IN*iqO02$uhuj#;!J$&+Ar@jWXDc2{hp-q`N`R-6s|O-quk7<4?otUA(K zgqUgg*1pK|CV&;vWUD9-0f+`Pw=I}MY>$Q{6Gi@GVN$b(G={5oi(53^{J6}L* zy8VCxv{`|NLEnPuwgvEan8FE9n;w4)a68|ZSAi(bhJLWVQ8`Fqgw-MNR@mMTyqy$o z_XBUK!qE)84caxI%o_MzYCsHUlaU}hO*nua{wKfJpH9A*dLVidd;O2&)9Lme5$|B^ zu!ua~Ij1{CmnM7u8S3*PM{jNg-U%ryZ4*Kz^mvEPc+HBpNDbp1w9of}P8j9^Z)LG- z^6-6s)KSUsKMdo2|M3&>1|T(ggl2qJX3Ch<;?_By-G5qxrOEGG(XJEJ)9|j- zaHS1&Ucc+~iqgcB#VAcYSqG(?4e1^qY&7`gtk`oy-EjOI^LTVW?HwK^bL_?>xgIc!vx3o*g)35YS-|VJrsogqh zQRhy3h94Uw_dBuJ{|FUfb{rk3s)kPT`5hN$B`oku>RWR${?rRDfnVjWtf~rF>zdg& zbZ9TxG?eL<^tTVIlH2vT6QsCxY}V+o8E#>@4o6?~zpI@7`h^eH&kLb4sVmAd2F=Jm zU)W|`exBD(hs{A9*3CM3`gUS={@2mrR-?j{>)my#lQnL;`nfN2^NYCy>pxJZmI${#N<78ar5e0_`G=T&Sw%Lblvak`zLbVgVcx#rfx!3Q zu-i}GW@B8W1)~4A&BhqcH~IdvH7-}Wnml{r(WazMQ7T$aHk{r4JC>Q%OHMcO>pk-ye3QdfrsNJDb926!ZyB z8nr!&;qOQ>@ySg$7x_lc-Xn4U*`WlxK75#WUg>T_>eRbFgn1{(IZ|Hz`<0{QK1rZ| zX@e3)%C0bqjt}#ely4@p2L3=A5VMc;knzvE(_`U7;X5SSf6X6A`s-qpu$)kWrsR=MhXpSNJ9}j{iq|41yYc2Cb=^ONjY|mr_dix)dnEt> literal 0 HcmV?d00001 diff --git a/corpus/picture-edit/resized/cold/commands.jsonl b/corpus/picture-edit/resized/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..b129b8c37fa51742fcc27d106ba6f1d66c561bec --- /dev/null +++ b/corpus/picture-edit/resized/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-49185CAA -ExpectedPages 1", "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/picture-edit/resized/cold/machine.json b/corpus/picture-edit/resized/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..00e839ceff043e0ae7dea19da900eb1137d7c256 --- /dev/null +++ b/corpus/picture-edit/resized/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-49185caa", "hostname": "ONE-M6-49185CAA"} diff --git a/corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5a459c9ca3f178e3040db2cfcfdc84b5c2df8958 GIT binary patch literal 3780 zcmZ3e|KIuM`!b(-j|5(YE0x8|z1Yb3++do|d@-BnTQ*EFU_b-*cV`JmPyFh+^n&=r zw;)KmcMh!33Z*OdL%Z&RtpfsshddG8EV#JRKl)fEB_{08$`xKma6i zgbzd_08_ZjySTsW^|`jYTIPB9?O2*Iug)n)8KDC^Ym4SWei+8C zVpMcA1crPFG_WvCk!6tCDZ?O=4W#!0X#t?8Y#iEy7qowJTVqyb{2+bbRYstg3=kJH zR5Ii-lrUs4qyouIhGYgkhJ2t%Do_rVBN-TgsvEd~1S7*Y83q-QUI;ky<@?u-Rbfj6 zp?r}7hKA-l&M%7pT=iO4_oZmk_v33|s-<5rG`wd|d3D)?*=>WlXkxTTLj_C@W*#~X zkyjL9XkbuD01`leZahdFr0hjRU)~xkU6;P}uUSvF_=w&Ed251(;5op;3GSGXManoVZL)|d{3jq~^01wbH z=ne!KCg8v@p#W+X*mR&6xWI#!dFbW|gA4-#jtM}0&~h>W$j4?j3l2N6nGcF5kf%WI z1(oF>dq6bEjmx&xPdNgT1OcF1f#To*00kNoINm{N38Z?AVY0mwS0aZFmjfTKz2#kOb0028N BNVNa} literal 0 HcmV?d00001 diff --git a/corpus/picture-edit/resized/cold/notebook/synthetic.one b/corpus/picture-edit/resized/cold/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..891840859470c11d04c2c1f6d5cb52c2d314b440 GIT binary patch literal 19040 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 literal 0 HcmV?d00001 diff --git a/corpus/picture-edit/resized/cold/read/environment.json b/corpus/picture-edit/resized/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..e9d185b2369bb479300d211d73afa7d1b9e2abd5 --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-49185CAA", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/picture-edit/resized/cold/read/hierarchy.xml b/corpus/picture-edit/resized/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..9e177b446447ecf78e96cae03078203a9ce860eb --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/picture-edit/resized/cold/read/page-000.xml b/corpus/picture-edit/resized/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a01b58e0bdf3bfbb936aa060b91c9d68f2e6959 --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/page-000.xml @@ -0,0 +1,8 @@ + +Fictitious: café, 東京, مرحبا]]>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9J1uoA +AAAASUVORK5CYII= diff --git a/corpus/picture-edit/resized/cold/read/payloads.json b/corpus/picture-edit/resized/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/picture-edit/resized/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/picture-edit/resized/cold/run.json b/corpus/picture-edit/resized/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..05ca5a00ecf0c4d8938a7f46adbeb3a09b9aa8b2 --- /dev/null +++ b/corpus/picture-edit/resized/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/resized", + "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/picture-edit/resized/cold/scripts/cold.ps1 b/corpus/picture-edit/resized/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/picture-edit/resized/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/picture-edit/resized/cold/scripts/read.ps1 b/corpus/picture-edit/resized/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/picture-edit/resized/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/picture-edit/resized/cold/source.json b/corpus/picture-edit/resized/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..3cb2a274f69d8527485a3001b80cc1fe0c60a876 --- /dev/null +++ b/corpus/picture-edit/resized/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1612, + "sha256": "32c3fa727c56069e1c5e885e237b95dad32762b4c2ef269a2b0e566fa534382c", + "mtime_ns": 1789247968519334299 + }, + { + "path": "synthetic.one", + "bytes": 19040, + "sha256": "6cedb887b23e772353a266850d6bbf4f0a0e8ff6d0e6f513b2a1a8af8b2a37d8", + "mtime_ns": 1789247968518969882 + } +] diff --git a/corpus/picture-edit/resized/cold/teardown.json b/corpus/picture-edit/resized/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/picture-edit/resized/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/notebook/README.md b/crates/notebook/README.md index d2fb85623a5db93250338f7956b8880ced069b87..136c30cd4e9c4cff1989b0a3e532432d396e83d4 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, bullets, numbering, note tags, table rows and columns, pictures and attachments, paragraph structure, +paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, pictures (insertion, size, description) and attachments, 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 784e3a7dee3818756ef49d8d618e946c16045bd1..2952cbffa9748b6362fef6a86437ee71eb4c5398 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, bullets and numbering, note tags, table rows and columns, inserted pictures and attachments, 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, bullets and numbering, note tags, table rows and columns, inserted pictures and attachments, picture 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 | | `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/write.rs b/crates/onestore/src/page/write.rs index b7654c07f92eb4706936f8dd6b1da8cdf1907a7c..2cddf00e9329d72913ab52318a62b45054a7c370 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::{Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; +use super::{Image, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; use crate::{ Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets, RevisionIndex, Store, TextAttribute, TreeEdit, Value, @@ -386,8 +386,15 @@ impl Lowering<'_> { } (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {} (ParagraphContent::Image(a), ParagraphContent::Image(b)) => { - if a != b { - return Err(invalid("A stored picture cannot be edited")); + if a.id != b.id + || a.bytes != b.bytes + || a.size != b.size + || a.background != b.background + || (a.layout.x, a.layout.y) != (b.layout.x, b.layout.y) + { + return Err(invalid( + "A stored picture keeps its payload, intrinsic size, background state and position", + )); } } (ParagraphContent::Attachment(a), ParagraphContent::Attachment(b)) => { @@ -753,8 +760,11 @@ impl Lowering<'_> { continue; }; if let Some(previous) = old.paragraphs.get(paragraph_id) { - if previous.content != paragraph.content { - return Err(invalid("A stored picture cannot be edited")); + let ParagraphContent::Image(stored) = &previous.content else { + return Err(invalid("Paragraph content type cannot change")); + }; + if stored != image { + self.resize_image(stored, image)?; } continue; } @@ -824,6 +834,53 @@ impl Lowering<'_> { Ok(()) } + /// Writes a displayed size and description the way OneNote stores a resized picture: + /// the layout width and height with the user flag, leaving the intrinsic size alone. + fn resize_image(&mut self, stored: &Image, image: &Image) -> Result<(), Error> { + let mut values: Values = vec![( + 0x14001d7a, + crate::create::current_timestamps()? + .0 + .to_le_bytes() + .to_vec(), + )]; + let mut removed = Vec::new(); + if image.layout != stored.layout { + match (image.layout.max_width, image.layout.max_height) { + (Some(width), Some(height)) => { + if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) { + return Err(invalid("Picture size must be positive")); + } + values.push((0x14001c1b, (width / 36.0).to_le_bytes().to_vec())); + values.push((0x14001c1c, (height / 36.0).to_le_bytes().to_vec())); + let user_set = u32::from(image.layout.width_set_by_user == Some(true)); + values.push((0x08001cbd | (user_set << 31), Vec::new())); + } + (None, None) => removed.extend([0x14001c1b, 0x14001c1c, 0x08001cbd]), + _ => return Err(invalid("A picture size needs both dimensions")), + } + } + if image.alt != stored.alt { + match &image.alt { + Some(alt) => values.push((0x1c001e58, crate::create::string(alt))), + None => removed.push(0x1c001e58), + } + } + let (space, object) = (self.space, self.id(image.id)); + self.apply(|current| { + crate::write::write_revision(current, space, |raw| { + let mut picture = PropertyObject::from_object(&raw.objects[&object])?; + picture.remove(&removed)?; + let values: Vec<(u32, &[u8])> = values + .iter() + .map(|(id, bytes)| (*id, bytes.as_slice())) + .collect(); + picture.set(&values)?; + Ok(BTreeMap::from([(object, picture)])) + }) + }) + } + fn place_containers( &mut self, old: &View<'_>, diff --git a/crates/onestore/tests/page_images.rs b/crates/onestore/tests/page_images.rs index 44e7ea1a603e2741f8c0b45145c39f4bfb13a848..5d32d6225b666a61ce6f11ddd7e6453303b53b71 100644 --- a/crates/onestore/tests/page_images.rs +++ b/crates/onestore/tests/page_images.rs @@ -1,12 +1,14 @@ use onestore::{ ExGuid, PreparedEdit, RevisionIndex, Store, - document::{Document, Format}, + document::{Document, Format, Layout}, page::{Image, Page, PageObject, PageParagraph, ParagraphContent, TextObject, text::new_id}, }; use std::sync::Arc; const NATIVE: &[u8] = include_bytes!("../../../corpus/native/cold-05-05-image/notebook/synthetic.one"); +const NATIVE_RESIZE: &[u8] = + include_bytes!("../../../corpus/picture-edit/native-resize/notebook/pictures.one"); const AUTHOR: &str = "Picture author"; /// A one-pixel PNG. const PNG: &[u8] = &[ @@ -155,6 +157,85 @@ fn a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed() { assert_eq!(body_paragraphs(&mut stored.clone()).len(), 2); } +fn resize(page: &mut Page, layout: Layout, alt: Option<&str>) -> ExGuid { + let mut found = None; + for object in &mut page.objects { + if let PageObject::Outline(outline) = object { + for paragraph in &mut outline.paragraphs { + if let ParagraphContent::Image(image) = &mut paragraph.content { + image.layout = layout.clone(); + image.alt = alt.map(str::to_owned); + assert!(found.replace(image.id).is_none()); + } + } + } + } + found.unwrap() +} + +/// `ONESTORE_IMAGE_RESIZE_EXPORT` names a new directory receiving the candidate for a cold reopen. +#[test] +fn a_native_picture_is_resized_and_described_then_reset() { + let (space, native) = first_page(NATIVE); + let mut resized = native.clone(); + let layout = Layout { + max_width: Some(144.0), + width_set_by_user: Some(true), + max_height: Some(108.0), + ..Default::default() + }; + resize(&mut resized, layout, Some("Resized in Rust")); + let written = PreparedEdit::page(NATIVE, space, &resized, AUTHOR).unwrap(); + let stored = page_in(written.as_bytes(), space); + assert_eq!(stored, resized); + assert_eq!( + PreparedEdit::page(written.as_bytes(), space, &stored, AUTHOR) + .unwrap() + .as_bytes(), + written.as_bytes() + ); + if let Some(directory) = std::env::var_os("ONESTORE_IMAGE_RESIZE_EXPORT") { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir(&directory).unwrap(); + std::fs::write(directory.join("synthetic.one"), written.as_bytes()).unwrap(); + let written_store = Store::parse(written.as_bytes()).unwrap(); + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("synthetic.one", written_store.header.file_id)], + ) + .unwrap(), + ) + .unwrap(); + } + let mut reset = stored.clone(); + resize(&mut reset, Layout::default(), None); + let again = PreparedEdit::page(written.as_bytes(), space, &reset, AUTHOR).unwrap(); + assert_eq!(page_in(again.as_bytes(), space), native); +} + +#[test] +fn a_rust_picture_resized_natively_reads_back_with_its_size_and_description() { + let (space, page) = first_page(NATIVE_RESIZE); + let found = pictures(&page); + let [picture] = found.as_slice() else { + panic!("{page:?}"); + }; + assert_eq!(picture.bytes.as_deref(), Some(PNG)); + assert_eq!(picture.size, Some([0.75, 0.75])); + assert_eq!(picture.layout.max_width, Some(144.0)); + assert_eq!(picture.layout.max_height, Some(108.0)); + assert_eq!(picture.layout.width_set_by_user, Some(true)); + assert_eq!(picture.alt.as_deref(), Some("Resized by OneNote")); + let mut wider = page.clone(); + let mut layout = picture.layout.clone(); + layout.max_width = Some(200.0); + resize(&mut wider, layout, Some("Wider in Rust")); + let written = PreparedEdit::page(NATIVE_RESIZE, space, &wider, AUTHOR).unwrap(); + assert_eq!(page_in(written.as_bytes(), space), wider); +} + #[test] fn pictures_need_a_recognised_payload_and_stored_ones_stay_fixed() { let source = onestore::create_section("pictures.one", "Text", "Author").unwrap(); diff --git a/tools/native/picture-edit.ps1 b/tools/native/picture-edit.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..ca0597f1475c8e4cccbb45fb4ed95c28eded912d --- /dev/null +++ b/tools/native/picture-edit.ps1 @@ -0,0 +1,49 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $sectionId = '' + $app.OpenHierarchy('pictures.one', $notebookId, [ref]$sectionId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(30) + $page = $null + do { + $hierarchy = '' + $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) + if ($pages.Count -eq 1) { + $content = '' + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + if (@($page.SelectNodes('//*[local-name()="Image"]')).Count -eq 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + $images = @($page.SelectNodes('//*[local-name()="Image"]')) + if ($images.Count -ne 1) { throw 'Expected one picture on the page.' } + [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) + $image = $images[0] + $image.SetAttribute('alt', 'Resized by OneNote') + $existing = $image.SelectSingleNode('*[local-name()="Size"]') + if ($existing) { [void]$image.RemoveChild($existing) } + $size = $page.CreateElement('one', 'Size', $namespace) + $size.SetAttribute('width', '144') + $size.SetAttribute('height', '108') + $size.SetAttribute('isSetByUser', 'true') + [void]$image.PrependChild($size) + [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/tools/test_picture_edit.py b/tools/test_picture_edit.py index f2227113405fec3023555b78d436dd16fef85ede..3abb872b16f7e0240e121825c56f6901a52feda8 100644 --- a/tools/test_picture_edit.py +++ b/tools/test_picture_edit.py @@ -14,20 +14,44 @@ FIXTURE = ROOT / 'corpus/picture-edit' compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] +def native_page(notebook, cold): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(cold / 'read', read) + compare(notebook, read) + page, = (ET.parse(path).getroot() for path in sorted((cold / 'read').glob('page-*.xml'))) + return page + + +def texts(page): + return [re.sub(r'<[^>]*>', '', t.text or '') for t in page.findall('.//one:OE/one:T', ns)] + + class PictureEditTest(unittest.TestCase): def test_a_rust_inserted_picture_renders_natively_with_its_payload(self): - with TemporaryDirectory() as temporary: - read = Path(temporary) / 'read' - shutil.copytree(FIXTURE / 'cold/read', read) - compare(FIXTURE / 'candidate', read) - page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml'))) + page = native_page(FIXTURE / 'inserted/candidate', FIXTURE / 'inserted/cold') image, = page.iter('{%s}Image' % ns['one']) self.assertEqual(image.get('format'), 'png') payload = base64.b64decode(image.find('one:Data', ns).text) self.assertEqual(payload[:8], b'\x89PNG\r\n\x1a\n') self.assertEqual(len(payload), 69) - texts = [re.sub(r'<[^>]*>', '', t.text or '') for t in page.findall('.//one:OE/one:T', ns)] - self.assertEqual(texts, ['Before the picture', 'After the picture']) + self.assertIsNone(image.find('one:Size', ns)) + self.assertEqual(texts(page), ['Before the picture', 'After the picture']) + + def test_onenote_resizes_a_rust_inserted_picture_through_its_layout(self): + page = native_page(FIXTURE / 'native-resize/notebook', FIXTURE / 'native-resize') + image, = page.iter('{%s}Image' % ns['one']) + size = image.find('one:Size', ns) + self.assertEqual((size.get('width'), size.get('height'), size.get('isSetByUser')), ('144.0', '108.0', 'true')) + self.assertEqual(image.get('alt'), 'Resized by OneNote') + self.assertEqual(len(base64.b64decode(image.find('one:Data', ns).text)), 69) + + def test_a_rust_resized_native_picture_renders_with_its_size_and_description(self): + page = native_page(FIXTURE / 'resized/candidate', FIXTURE / 'resized/cold') + image, = page.iter('{%s}Image' % ns['one']) + size = image.find('one:Size', ns) + self.assertEqual((size.get('width'), size.get('height'), size.get('isSetByUser')), ('144.0', '108.0', 'true')) + self.assertEqual(image.get('alt'), 'Resized in Rust') if __name__ == '__main__': -- 2.54.0