authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-12 14:31:17-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-12 17:00:32-07:00
logd0ec66f9b4d3023668fa156d8a0903b3b5b8a316
tree313733ce0e5280fa41d4d86a19594a1e4d031424
parentcb4842d4e6047a60f7c99dcbf033c4fe366e8b8f
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

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

68 files changed, 978 insertions(+), 245 deletions(-)

corpus/picture-edit/README.md+24-8
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1# Rust picture authoring1# Rust picture authoring
22
3`candidate/` is the page writer's output for3`inserted/candidate` is the page writer's output for
4`a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed` in4`a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed` in
5`crates/onestore/tests/page_images.rs`: on a section created in Rust, a5`crates/onestore/tests/page_images.rs`: on a section created in Rust, a
6paragraph between two text paragraphs becomes a picture, stored the way6paragraph 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...@@ -10,11 +10,27 @@ object, a picture-container file object declaring it by identity and
10extension, and a picture object with the displayed size that the paragraph10extension, and a picture object with the displayed size that the paragraph
11holds as content. OneNote's own picture objects also carry a DPAPI-protected11holds as content. OneNote's own picture objects also carry a DPAPI-protected
12blob (`0x1c001dfb`) that only the authoring Windows user can decrypt; the12blob (`0x1c001dfb`) that only the authoring Windows user can decrypt; the
13writer omits it.13writer omits it. `inserted/cold` is a fresh OneNote 2010 read: the image
14reads back as `format="png"` with the exact payload bytes, between the two
15text paragraphs.
1416
15`cold/` is a fresh OneNote 2010 read: the image reads back as `format="png"`17`native-resize` is OneNote 2010 editing that Rust-written picture
16with the exact payload bytes, between the two text paragraphs.18(`tools/native/picture-edit.ps1` on `inserted/candidate`): the picture gets
17`tools/test_picture_edit.py` checks this without a VM. Regenerate with19a 144 by 108 point user-set size and alternative text through the COM API
18`ONESTORE_IMAGE_EXPORT` set to a new absolute directory while running the20(`before.xml`, `update.xml`), and `read/` is the read after the edit. In
19test, then cold-open it with `tools/native_runner.py OUTPUT COLD21`notebook/`, OneNote stored the size as the picture object's layout width
20--expected-pages 1 --collect-notebook`.22and height with the user flag (`0x14001c1b`, `0x14001c1c`, `0x08001cbd`) and
23left the intrinsic size (`0x140034cd/ce`) alone; it also rewrote the picture
24container under a native identity with its DPAPI blob and hash.
25
26`resized/candidate` is the writer doing the same on the native picture of
27`cold-05-05-image` (`a_native_picture_is_resized_and_described_then_reset`):
28layout width, height, user flag and alternative text on the picture object.
29`resized/cold` is its cold read with the size and description.
30
31`tools/test_picture_edit.py` checks all three without a VM. Regenerate the
32candidates with `ONESTORE_IMAGE_EXPORT` and `ONESTORE_IMAGE_RESIZE_EXPORT`
33set to new absolute directories while running the tests, then cold-open
34them with `tools/native_runner.py OUTPUT COLD --expected-pages 1
35--collect-notebook`; regenerate `native-resize` with `--author
36tools/native/picture-edit.ps1` on `inserted/candidate`.
corpus/picture-edit/candidate/Open Notebook.onetoc2 deleted
Binary files a/corpus/picture-edit/candidate/Open Notebook.onetoc2 and /dev/null differ
corpus/picture-edit/candidate/pictures.one deleted
Binary files a/corpus/picture-edit/candidate/pictures.one and /dev/null differ
corpus/picture-edit/cold/commands.jsonl deleted-3
...@@ -1,3 +0,0 @@
1{"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}
2{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-A5A8705B -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null}
3{"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}
corpus/picture-edit/cold/machine.json deleted-1
...@@ -1 +0,0 @@
1{"name": "m6-a5a8705b", "hostname": "ONE-M6-A5A8705B"}
corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 deleted
Binary files a/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 and /dev/null differ
corpus/picture-edit/cold/notebook/pictures.one deleted
Binary files a/corpus/picture-edit/cold/notebook/pictures.one and /dev/null differ
corpus/picture-edit/cold/read/environment.json deleted-7
...@@ -1,7 +0,0 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-A5A8705B",
5 "cold": true,
6 "onenote": "14.0.4763.1000"
7}
corpus/picture-edit/cold/read/hierarchy.xml deleted-2
...@@ -1,2 +0,0 @@
1<?xml version="1.0"?>
2<one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{BB903F04-A992-47A1-B747-4D7C2D151005}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T20:08:56.000Z" color="#B49EDE"><one:Section name="pictures" ID="{54DE5097-F4BE-0BCC-0392-19FB4210AAAD}{1}{B0}" path="C:\one-tests\runs\capture\notebook\pictures.one" lastModifiedTime="2026-09-12T20:08:56.000Z" color="#8AA8E4"><one:Page ID="{D3A0AD9C-D12D-02B2-1683-4D32345076A5}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:08:56.000Z" pageLevel="1"/></one:Section></one:Notebook>
corpus/picture-edit/cold/read/page-000.xml deleted-4
...@@ -1,4 +0,0 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{D3A0AD9C-D12D-02B2-1683-4D32345076A5}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:08:56.000Z" pageLevel="1" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{54DE5097-F4BE-0BCC-0392-19FB4210AAAD}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="148.3100738525391" height="27.60543251037598"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{54DE5097-F4BE-0BCC-0392-19FB4210AAAD}{24}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Before the picture]]></one:T></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{496D54CF-B74C-091A-2D00-F268C53481D1}{0}{B0}" alignment="left"><one:Image format="png"><one:Data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w
3AAAAAElFTkSuQmCC
4</one:Data></one:Image></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{39F25FF2-3F08-07F1-06E1-F7F0075DE8F1}{0}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[After the picture]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/picture-edit/cold/read/payloads.json deleted-3
...@@ -1,3 +0,0 @@
1[
2
3]
\ No newline at end of file
corpus/picture-edit/cold/run.json deleted-18
...@@ -1,18 +0,0 @@
1{
2 "notebook": "/private/tmp/m4/pictures",
3 "expected_pages": 1,
4 "author": null,
5 "author_timeout_seconds": 600,
6 "inspect": false,
7 "collect_notebook": true,
8 "base": {
9 "file": "win7-office-base.qcow2",
10 "format": "qcow2",
11 "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
12 "virtual_size": 68719476736
13 },
14 "scripts": {
15 "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
16 "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
17 }
18}
corpus/picture-edit/cold/scripts/cold.ps1 deleted-27
...@@ -1,27 +0,0 @@
1param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
5if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
6 throw 'Choose a run directly below C:\one-tests\runs.'
7}
8if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
9$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
10if ($CloneHost) {
11 if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
12 throw 'The disposable clone hostname does not match this machine.'
13 }
14 New-Item "$key\Options\Paths" -Force | Out-Null
15 New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
16} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
17 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
18 throw 'Park the personal OneNote profile before resetting the test cache.'
19}
20$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
21$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
22if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
23New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
24if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
25if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
26New-Item "$key\OpenNotebooks" | Out-Null
27New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
corpus/picture-edit/cold/scripts/read.ps1 deleted-142
...@@ -1,142 +0,0 @@
1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $lastChange = ''
44 $stableSince = [DateTime]::UtcNow
45 $settled = $false
46 do {
47 $pages = @{}
48 foreach ($section in $sections) {
49 $hierarchy = ''
50 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
51 [xml]$xml = $hierarchy
52 foreach ($node in $xml.SelectNodes('//*[@path]')) {
53 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
54 throw 'OneNote opened a section outside the copied notebook.'
55 }
56 }
57 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
58 $id = $node.GetAttribute('ID')
59 $content = ''
60 $app.GetPageContent($id, [ref]$content, 1, 1)
61 $pages[$id] = $content
62 }
63 }
64 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
65 if ($signature -ne $previous) {
66 $lastChange = $previous
67 $previous = $signature
68 $stableSince = [DateTime]::UtcNow
69 }
70 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
71 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
72 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
73 Start-Sleep -Milliseconds 250
74 } while ([DateTime]::UtcNow -lt $deadline)
75 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
76 [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
77 $index = 0
78 foreach ($id in @($pages.Keys | Sort-Object)) {
79 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
80 $index++
81 }
82 $hierarchy = ''
83 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
84 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
85 throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
86 }
87 $index = 0
88 $payloads = @()
89 foreach ($id in @($pages.Keys | Sort-Object)) {
90 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
91 if ($Pdf) {
92 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
93 $app.NavigateTo($id, '', $false)
94 $app.Publish($id, $pdfPath, 3, '')
95 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
96 }
97 [xml]$page = $pages[$id]
98 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
99 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
100 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
101 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
102 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
103 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
104 sha256 = $hash; bytes = $bytes.Length }
105 }
106 $index++
107 }
108 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
109 $all = ''
110 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
111 [xml]$finalTree = $all
112 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
113 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
114 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
115 }
116 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
117 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
118} catch {
119 $failure = $_
120 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
121 throw
122} finally {
123 try {
124 try {
125 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
126 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
127 $app.CloseNotebook($notebookId, $false)
128 }
129 } catch {
130 if ($null -eq $failure) { throw }
131 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
132 }
133 } finally {
134 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
135 }
136 $app = $null
137 [GC]::Collect()
138 [GC]::WaitForPendingFinalizers()
139 if (-not $UseCurrentCache -and -not $CloneHost) {
140 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
141 }
142}
corpus/picture-edit/cold/source.json deleted-14
...@@ -1,14 +0,0 @@
1[
2 {
3 "path": "Open Notebook.onetoc2",
4 "bytes": 1604,
5 "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a",
6 "mtime_ns": 1789243635388246438
7 },
8 {
9 "path": "pictures.one",
10 "bytes": 7064,
11 "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70",
12 "mtime_ns": 1789243635387879438
13 }
14]
corpus/picture-edit/cold/teardown.json deleted-1
...@@ -1 +0,0 @@
1{"absent": true}
corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 differ
corpus/picture-edit/inserted/candidate/pictures.one created
Binary files /dev/null and b/corpus/picture-edit/inserted/candidate/pictures.one differ
corpus/picture-edit/inserted/cold/commands.jsonl created+3
...@@ -0,0 +1,3 @@
1{"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}
2{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-A5A8705B -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null}
3{"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}
corpus/picture-edit/inserted/cold/machine.json created+1
...@@ -0,0 +1 @@
1{"name": "m6-a5a8705b", "hostname": "ONE-M6-A5A8705B"}
corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 differ
corpus/picture-edit/inserted/cold/notebook/pictures.one created
Binary files /dev/null and b/corpus/picture-edit/inserted/cold/notebook/pictures.one differ
corpus/picture-edit/inserted/cold/read/environment.json created+7
...@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-A5A8705B",
5 "cold": true,
6 "onenote": "14.0.4763.1000"
7}
corpus/picture-edit/inserted/cold/read/hierarchy.xml created+2
...@@ -0,0 +1,2 @@
1<?xml version="1.0"?>
2<one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{BB903F04-A992-47A1-B747-4D7C2D151005}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T20:08:56.000Z" color="#B49EDE"><one:Section name="pictures" ID="{54DE5097-F4BE-0BCC-0392-19FB4210AAAD}{1}{B0}" path="C:\one-tests\runs\capture\notebook\pictures.one" lastModifiedTime="2026-09-12T20:08:56.000Z" color="#8AA8E4"><one:Page ID="{D3A0AD9C-D12D-02B2-1683-4D32345076A5}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:08:56.000Z" pageLevel="1"/></one:Section></one:Notebook>
corpus/picture-edit/inserted/cold/read/page-000.xml created+4
...@@ -0,0 +1,4 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{D3A0AD9C-D12D-02B2-1683-4D32345076A5}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:08:56.000Z" pageLevel="1" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{54DE5097-F4BE-0BCC-0392-19FB4210AAAD}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="148.3100738525391" height="27.60543251037598"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{54DE5097-F4BE-0BCC-0392-19FB4210AAAD}{24}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Before the picture]]></one:T></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{496D54CF-B74C-091A-2D00-F268C53481D1}{0}{B0}" alignment="left"><one:Image format="png"><one:Data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w
3AAAAAElFTkSuQmCC
4</one:Data></one:Image></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{39F25FF2-3F08-07F1-06E1-F7F0075DE8F1}{0}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[After the picture]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/picture-edit/inserted/cold/read/payloads.json created+3
...@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/picture-edit/inserted/cold/run.json created+18
...@@ -0,0 +1,18 @@
1{
2 "notebook": "/private/tmp/m4/pictures",
3 "expected_pages": 1,
4 "author": null,
5 "author_timeout_seconds": 600,
6 "inspect": false,
7 "collect_notebook": true,
8 "base": {
9 "file": "win7-office-base.qcow2",
10 "format": "qcow2",
11 "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
12 "virtual_size": 68719476736
13 },
14 "scripts": {
15 "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
16 "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
17 }
18}
corpus/picture-edit/inserted/cold/scripts/cold.ps1 created+27
...@@ -0,0 +1,27 @@
1param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
5if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
6 throw 'Choose a run directly below C:\one-tests\runs.'
7}
8if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
9$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
10if ($CloneHost) {
11 if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
12 throw 'The disposable clone hostname does not match this machine.'
13 }
14 New-Item "$key\Options\Paths" -Force | Out-Null
15 New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
16} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
17 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
18 throw 'Park the personal OneNote profile before resetting the test cache.'
19}
20$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
21$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
22if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
23New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
24if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
25if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
26New-Item "$key\OpenNotebooks" | Out-Null
27New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
corpus/picture-edit/inserted/cold/scripts/read.ps1 created+142
...@@ -0,0 +1,142 @@
1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $lastChange = ''
44 $stableSince = [DateTime]::UtcNow
45 $settled = $false
46 do {
47 $pages = @{}
48 foreach ($section in $sections) {
49 $hierarchy = ''
50 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
51 [xml]$xml = $hierarchy
52 foreach ($node in $xml.SelectNodes('//*[@path]')) {
53 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
54 throw 'OneNote opened a section outside the copied notebook.'
55 }
56 }
57 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
58 $id = $node.GetAttribute('ID')
59 $content = ''
60 $app.GetPageContent($id, [ref]$content, 1, 1)
61 $pages[$id] = $content
62 }
63 }
64 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
65 if ($signature -ne $previous) {
66 $lastChange = $previous
67 $previous = $signature
68 $stableSince = [DateTime]::UtcNow
69 }
70 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
71 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
72 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
73 Start-Sleep -Milliseconds 250
74 } while ([DateTime]::UtcNow -lt $deadline)
75 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
76 [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
77 $index = 0
78 foreach ($id in @($pages.Keys | Sort-Object)) {
79 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
80 $index++
81 }
82 $hierarchy = ''
83 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
84 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
85 throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
86 }
87 $index = 0
88 $payloads = @()
89 foreach ($id in @($pages.Keys | Sort-Object)) {
90 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
91 if ($Pdf) {
92 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
93 $app.NavigateTo($id, '', $false)
94 $app.Publish($id, $pdfPath, 3, '')
95 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
96 }
97 [xml]$page = $pages[$id]
98 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
99 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
100 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
101 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
102 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
103 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
104 sha256 = $hash; bytes = $bytes.Length }
105 }
106 $index++
107 }
108 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
109 $all = ''
110 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
111 [xml]$finalTree = $all
112 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
113 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
114 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
115 }
116 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
117 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
118} catch {
119 $failure = $_
120 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
121 throw
122} finally {
123 try {
124 try {
125 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
126 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
127 $app.CloseNotebook($notebookId, $false)
128 }
129 } catch {
130 if ($null -eq $failure) { throw }
131 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
132 }
133 } finally {
134 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
135 }
136 $app = $null
137 [GC]::Collect()
138 [GC]::WaitForPendingFinalizers()
139 if (-not $UseCurrentCache -and -not $CloneHost) {
140 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
141 }
142}
corpus/picture-edit/inserted/cold/source.json created+14
...@@ -0,0 +1,14 @@
1[
2 {
3 "path": "Open Notebook.onetoc2",
4 "bytes": 1604,
5 "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a",
6 "mtime_ns": 1789243635388246438
7 },
8 {
9 "path": "pictures.one",
10 "bytes": 7064,
11 "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70",
12 "mtime_ns": 1789243635387879438
13 }
14]
corpus/picture-edit/inserted/cold/teardown.json created+1
...@@ -0,0 +1 @@
1{"absent": true}
corpus/picture-edit/native-resize/before.xml created+4
...@@ -0,0 +1,4 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{793ED43C-73DF-0C13-3ECD-AF01DCB57EC2}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T21:15:06.000Z" pageLevel="1" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{FE402937-564C-056D-2BDC-FBC8AAF5A2CA}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="148.3100738525391" height="27.60543251037598"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{FE402937-564C-056D-2BDC-FBC8AAF5A2CA}{24}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Before the picture]]></one:T></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{E3F32D6F-15BE-07BB-054E-105B2DD189B6}{0}{B0}" alignment="left"><one:Image format="png"><one:Data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w
3AAAAAElFTkSuQmCC
4</one:Data></one:Image></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{936C2652-9DFA-0950-2EAF-15C3EFB8E096}{0}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[After the picture]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/picture-edit/native-resize/commands.jsonl created+4
...@@ -0,0 +1,4 @@
1{"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}
2{"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}
3{"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}
4{"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}
corpus/picture-edit/native-resize/machine.json created+1
...@@ -0,0 +1 @@
1{"name": "m6-3be178f1", "hostname": "ONE-M6-3BE178F1"}
corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 differ
corpus/picture-edit/native-resize/notebook/pictures.one created
Binary files /dev/null and b/corpus/picture-edit/native-resize/notebook/pictures.one differ
corpus/picture-edit/native-resize/read/environment.json created+7
...@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-3BE178F1",
5 "cold": false,
6 "onenote": "14.0.4763.1000"
7}
corpus/picture-edit/native-resize/read/hierarchy.xml created+2
...@@ -0,0 +1,2 @@
1<?xml version="1.0"?>
2<one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{2DCFFDE2-71DF-4568-9FB9-9B223E2A8DEC}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T21:16:38.000Z" color="#B49EDE"><one:Section name="pictures" ID="{886B6EBB-7FAD-0D44-0372-05E7CC43DFC7}{1}{B0}" path="C:\one-tests\runs\capture\notebook\pictures.one" lastModifiedTime="2026-09-12T21:16:38.000Z" color="#8AA8E4"><one:Page ID="{0F1593B0-5A3E-043A-1663-512EBA0303CF}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T21:16:38.000Z" pageLevel="1"/></one:Section></one:Notebook>
corpus/picture-edit/native-resize/read/page-000.xml created+4
...@@ -0,0 +1,4 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{0F1593B0-5A3E-043A-1663-512EBA0303CF}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T21:16:38.000Z" pageLevel="1" lang="en-US"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-12T21:15:09.000Z" objectID="{886B6EBB-7FAD-0D44-0372-05E7CC43DFC7}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="171.0" height="134.8554229736328"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T21:15:09.000Z" lastModifiedTime="2026-09-12T21:15:09.000Z" objectID="{886B6EBB-7FAD-0D44-0372-05E7CC43DFC7}{24}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Before the picture]]></one:T></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T21:15:09.000Z" objectID="{95D86AE3-3C5F-0F92-2DE0-EE744B67F4BB}{0}{B0}" alignment="left"><one:Image format="png" alt="Resized by OneNote" originalPageNumber="0"><one:Size width="144.0" height="108.0" isSetByUser="true"/><one:Data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w
3AAAAAElFTkSuQmCC
4</one:Data></one:Image></one:OE><one:OE creationTime="2026-09-12T21:15:09.000Z" lastModifiedTime="2026-09-12T21:15:09.000Z" objectID="{E54761DE-B41B-0179-0601-EBEC890E9D9B}{0}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[After the picture]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/picture-edit/native-resize/read/payloads.json created+3
...@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/picture-edit/native-resize/run.json created+19
...@@ -0,0 +1,19 @@
1{
2 "notebook": "/Users/clo/dev/one/corpus/picture-edit/candidate",
3 "expected_pages": 1,
4 "author": "tools/native/picture-edit.ps1",
5 "author_timeout_seconds": 600,
6 "inspect": false,
7 "collect_notebook": true,
8 "base": {
9 "file": "win7-office-base.qcow2",
10 "format": "qcow2",
11 "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
12 "virtual_size": 68719476736
13 },
14 "scripts": {
15 "author.ps1": "ac663d020a6049b4b688aed1be4ae2661a59fff25d83fe7c5eaabbf14d357f55",
16 "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
17 "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
18 }
19}
corpus/picture-edit/native-resize/scripts/author.ps1 created+49
...@@ -0,0 +1,49 @@
1param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost)
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost
5$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote'
6$app = New-Object -ComObject OneNote.Application
7$notebookId = ''
8try {
9 $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0)
10 $sectionId = ''
11 $app.OpenHierarchy('pictures.one', $notebookId, [ref]$sectionId, 0)
12 $deadline = [DateTime]::UtcNow.AddSeconds(30)
13 $page = $null
14 do {
15 $hierarchy = ''
16 $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1)
17 [xml]$tree = $hierarchy
18 $pages = @($tree.SelectNodes('//*[local-name()="Page"]'))
19 if ($pages.Count -eq 1) {
20 $content = ''
21 $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1)
22 [xml]$page = $content
23 if (@($page.SelectNodes('//*[local-name()="Image"]')).Count -eq 1) { break }
24 }
25 Start-Sleep -Milliseconds 250
26 } while ([DateTime]::UtcNow -lt $deadline)
27 $images = @($page.SelectNodes('//*[local-name()="Image"]'))
28 if ($images.Count -ne 1) { throw 'Expected one picture on the page.' }
29 [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8)
30 $image = $images[0]
31 $image.SetAttribute('alt', 'Resized by OneNote')
32 $existing = $image.SelectSingleNode('*[local-name()="Size"]')
33 if ($existing) { [void]$image.RemoveChild($existing) }
34 $size = $page.CreateElement('one', 'Size', $namespace)
35 $size.SetAttribute('width', '144')
36 $size.SetAttribute('height', '108')
37 $size.SetAttribute('isSetByUser', 'true')
38 [void]$image.PrependChild($size)
39 [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8)
40 $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false)
41 $app.SyncHierarchy($notebookId)
42 $app.CloseNotebook($notebookId, $false)
43 $notebookId = ''
44} finally {
45 if ($notebookId) { $app.CloseNotebook($notebookId, $false) }
46 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
47 [GC]::Collect()
48 [GC]::WaitForPendingFinalizers()
49}
corpus/picture-edit/native-resize/scripts/cold.ps1 created+27
...@@ -0,0 +1,27 @@
1param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
5if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
6 throw 'Choose a run directly below C:\one-tests\runs.'
7}
8if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
9$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
10if ($CloneHost) {
11 if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
12 throw 'The disposable clone hostname does not match this machine.'
13 }
14 New-Item "$key\Options\Paths" -Force | Out-Null
15 New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
16} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
17 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
18 throw 'Park the personal OneNote profile before resetting the test cache.'
19}
20$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
21$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
22if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
23New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
24if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
25if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
26New-Item "$key\OpenNotebooks" | Out-Null
27New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
corpus/picture-edit/native-resize/scripts/read.ps1 created+142
...@@ -0,0 +1,142 @@
1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $lastChange = ''
44 $stableSince = [DateTime]::UtcNow
45 $settled = $false
46 do {
47 $pages = @{}
48 foreach ($section in $sections) {
49 $hierarchy = ''
50 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
51 [xml]$xml = $hierarchy
52 foreach ($node in $xml.SelectNodes('//*[@path]')) {
53 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
54 throw 'OneNote opened a section outside the copied notebook.'
55 }
56 }
57 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
58 $id = $node.GetAttribute('ID')
59 $content = ''
60 $app.GetPageContent($id, [ref]$content, 1, 1)
61 $pages[$id] = $content
62 }
63 }
64 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
65 if ($signature -ne $previous) {
66 $lastChange = $previous
67 $previous = $signature
68 $stableSince = [DateTime]::UtcNow
69 }
70 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
71 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
72 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
73 Start-Sleep -Milliseconds 250
74 } while ([DateTime]::UtcNow -lt $deadline)
75 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
76 [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
77 $index = 0
78 foreach ($id in @($pages.Keys | Sort-Object)) {
79 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
80 $index++
81 }
82 $hierarchy = ''
83 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
84 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
85 throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
86 }
87 $index = 0
88 $payloads = @()
89 foreach ($id in @($pages.Keys | Sort-Object)) {
90 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
91 if ($Pdf) {
92 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
93 $app.NavigateTo($id, '', $false)
94 $app.Publish($id, $pdfPath, 3, '')
95 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
96 }
97 [xml]$page = $pages[$id]
98 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
99 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
100 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
101 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
102 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
103 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
104 sha256 = $hash; bytes = $bytes.Length }
105 }
106 $index++
107 }
108 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
109 $all = ''
110 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
111 [xml]$finalTree = $all
112 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
113 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
114 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
115 }
116 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
117 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
118} catch {
119 $failure = $_
120 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
121 throw
122} finally {
123 try {
124 try {
125 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
126 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
127 $app.CloseNotebook($notebookId, $false)
128 }
129 } catch {
130 if ($null -eq $failure) { throw }
131 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
132 }
133 } finally {
134 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
135 }
136 $app = $null
137 [GC]::Collect()
138 [GC]::WaitForPendingFinalizers()
139 if (-not $UseCurrentCache -and -not $CloneHost) {
140 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
141 }
142}
corpus/picture-edit/native-resize/source.json created+14
...@@ -0,0 +1,14 @@
1[
2 {
3 "path": "Open Notebook.onetoc2",
4 "bytes": 1604,
5 "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a",
6 "mtime_ns": 1789243969388188834
7 },
8 {
9 "path": "pictures.one",
10 "bytes": 7064,
11 "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70",
12 "mtime_ns": 1789243969387957378
13 }
14]
corpus/picture-edit/native-resize/teardown.json created+1
...@@ -0,0 +1 @@
1{"absent": true}
corpus/picture-edit/native-resize/update.xml created+3
...@@ -0,0 +1,3 @@
1<?xml version="1.0"?><one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{793ED43C-73DF-0C13-3ECD-AF01DCB57EC2}{1}{B0}" name="Before the picture" dateTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T21:15:06.000Z" pageLevel="1" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic /></one:PageSize><one:RuleLines visible="false" /></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{FE402937-564C-056D-2BDC-FBC8AAF5A2CA}{23}{B0}"><one:Position x="36.0" y="36.0" z="0" /><one:Size width="148.3100738525391" height="27.60543251037598" /><one:Indents><one:Indent level="0" indent="0.0" /></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{FE402937-564C-056D-2BDC-FBC8AAF5A2CA}{24}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Before the picture]]></one:T></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{E3F32D6F-15BE-07BB-054E-105B2DD189B6}{0}{B0}" alignment="left"><one:Image format="png" alt="Resized by OneNote"><one:Size width="144" height="108" isSetByUser="true" /><one:Data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w
2AAAAAElFTkSuQmCC
3</one:Data></one:Image></one:OE><one:OE author="Picture author" lastModifiedBy="Picture author" creationTime="2026-09-12T20:07:15.000Z" lastModifiedTime="2026-09-12T20:07:15.000Z" objectID="{936C2652-9DFA-0950-2EAF-15C3EFB8E096}{0}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[After the picture]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
\ No newline at end of file
corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 differ
corpus/picture-edit/resized/candidate/synthetic.one created
Binary files /dev/null and b/corpus/picture-edit/resized/candidate/synthetic.one differ
corpus/picture-edit/resized/cold/commands.jsonl created+3
...@@ -0,0 +1,3 @@
1{"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}
2{"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}
3{"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}
corpus/picture-edit/resized/cold/machine.json created+1
...@@ -0,0 +1 @@
1{"name": "m6-49185caa", "hostname": "ONE-M6-49185CAA"}
corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 differ
corpus/picture-edit/resized/cold/notebook/synthetic.one created
Binary files /dev/null and b/corpus/picture-edit/resized/cold/notebook/synthetic.one differ
corpus/picture-edit/resized/cold/read/environment.json created+7
...@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-49185CAA",
5 "cold": true,
6 "onenote": "14.0.4763.1000"
7}
corpus/picture-edit/resized/cold/read/hierarchy.xml created+2
...@@ -0,0 +1,2 @@
1<?xml version="1.0"?>
2<one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{D280F52D-D262-4B6A-A88B-95A566ED5B46}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T21:21:40.000Z" color="#B49EDE"><one:Section name="synthetic" ID="{FA31A0CC-36F2-0200-3C00-C8AE231D89D9}{1}{B0}" path="C:\one-tests\runs\capture\notebook\synthetic.one" lastModifiedTime="2026-09-12T21:21:40.000Z" color="#8AA8E4"><one:Page ID="{D678534A-BF39-4111-BB67-603FDF2595C6}{1}{B0}" name="Fictitious: café, 東京, مرحبا" dateTime="2026-09-05T05:06:54.000Z" lastModifiedTime="2026-09-12T21:21:40.000Z" pageLevel="1"/></one:Section></one:Notebook>
corpus/picture-edit/resized/cold/read/page-000.xml created+8
...@@ -0,0 +1,8 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{D678534A-BF39-4111-BB67-603FDF2595C6}{1}{B0}" name="Fictitious: café, 東京, مرحبا" dateTime="2026-09-05T05:06:54.000Z" lastModifiedTime="2026-09-12T21:21:40.000Z" pageLevel="1" lang="en-US"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="snow" authorInitials="S" lastModifiedBy="snow" lastModifiedByInitials="S" lastModifiedTime="2026-09-05T05:06:56.000Z" objectID="{B1B4FBF6-F44C-42A7-B495-ECE420B970A7}{10}{B0}"><one:Position x="36.0" y="14.40000057220459" z="0"/><one:Size width="127.5136947631836" height="14.30000114440918"/><one:OEChildren><one:OE creationTime="2026-09-05T05:06:55.000Z" lastModifiedTime="2026-09-05T05:06:56.000Z" objectID="{B1B4FBF6-F44C-42A7-B495-ECE420B970A7}{11}{B0}" alignment="left" quickStyleIndex="0" style="font-size:11.0pt;color:#1F4E79"><one:T><![CDATA[<span
3style='font-weight:bold;font-family:Calibri' lang=en-US>Fictitious: café, </span><span
4style='font-weight:bold;font-family:SimSun' lang=en-US>東京</span><span
5style='font-weight:bold;font-family:Calibri' lang=en-US>, </span><span
6style='font-weight:bold;font-family:Arial;direction:rtl;unicode-bidi:embed'
7lang=ar-SA>مرحبا</span>]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="snow" authorInitials="S" lastModifiedBy="snow" lastModifiedByInitials="S" lastModifiedTime="2026-09-05T05:06:57.000Z" objectID="{E70427E3-4719-48FA-AE23-6E6C84B594EE}{10}{B0}"><one:Position x="144.0" y="96.0" z="1"/><one:Size width="167.0449523925781" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-05T05:06:57.000Z" lastModifiedTime="2026-09-05T05:06:57.000Z" objectID="{E70427E3-4719-48FA-AE23-6E6C84B594EE}{11}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Fictitious positioned outline.]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="snow" authorInitials="S" lastModifiedBy="snow" lastModifiedByInitials="S" lastModifiedTime="2026-09-12T21:19:28.000Z" objectID="{DA2D4F6C-A183-4154-9853-C59868D8DC60}{10}{B0}"><one:Position x="144.0" y="192.0" z="2"/><one:Size width="144.0" height="108.0000152587891"/><one:OEChildren><one:OE creationTime="2026-09-05T05:06:58.000Z" lastModifiedTime="2026-09-12T21:19:28.000Z" objectID="{DA2D4F6C-A183-4154-9853-C59868D8DC60}{11}{B0}" alignment="left"><one:Image format="png" alt="Resized in Rust" originalPageNumber="0"><one:Size width="144.0" height="108.0" isSetByUser="true"/><one:Data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9J1uoA
8AAAASUVORK5CYII=</one:Data></one:Image></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/picture-edit/resized/cold/read/payloads.json created+3
...@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/picture-edit/resized/cold/run.json created+18
...@@ -0,0 +1,18 @@
1{
2 "notebook": "/private/tmp/m4/resized",
3 "expected_pages": 1,
4 "author": null,
5 "author_timeout_seconds": 600,
6 "inspect": false,
7 "collect_notebook": true,
8 "base": {
9 "file": "win7-office-base.qcow2",
10 "format": "qcow2",
11 "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
12 "virtual_size": 68719476736
13 },
14 "scripts": {
15 "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
16 "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
17 }
18}
corpus/picture-edit/resized/cold/scripts/cold.ps1 created+27
...@@ -0,0 +1,27 @@
1param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
5if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
6 throw 'Choose a run directly below C:\one-tests\runs.'
7}
8if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
9$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
10if ($CloneHost) {
11 if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
12 throw 'The disposable clone hostname does not match this machine.'
13 }
14 New-Item "$key\Options\Paths" -Force | Out-Null
15 New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
16} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
17 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
18 throw 'Park the personal OneNote profile before resetting the test cache.'
19}
20$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
21$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
22if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
23New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
24if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
25if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
26New-Item "$key\OpenNotebooks" | Out-Null
27New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
corpus/picture-edit/resized/cold/scripts/read.ps1 created+142
...@@ -0,0 +1,142 @@
1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $lastChange = ''
44 $stableSince = [DateTime]::UtcNow
45 $settled = $false
46 do {
47 $pages = @{}
48 foreach ($section in $sections) {
49 $hierarchy = ''
50 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
51 [xml]$xml = $hierarchy
52 foreach ($node in $xml.SelectNodes('//*[@path]')) {
53 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
54 throw 'OneNote opened a section outside the copied notebook.'
55 }
56 }
57 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
58 $id = $node.GetAttribute('ID')
59 $content = ''
60 $app.GetPageContent($id, [ref]$content, 1, 1)
61 $pages[$id] = $content
62 }
63 }
64 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
65 if ($signature -ne $previous) {
66 $lastChange = $previous
67 $previous = $signature
68 $stableSince = [DateTime]::UtcNow
69 }
70 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
71 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
72 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
73 Start-Sleep -Milliseconds 250
74 } while ([DateTime]::UtcNow -lt $deadline)
75 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
76 [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
77 $index = 0
78 foreach ($id in @($pages.Keys | Sort-Object)) {
79 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
80 $index++
81 }
82 $hierarchy = ''
83 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
84 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
85 throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
86 }
87 $index = 0
88 $payloads = @()
89 foreach ($id in @($pages.Keys | Sort-Object)) {
90 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
91 if ($Pdf) {
92 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
93 $app.NavigateTo($id, '', $false)
94 $app.Publish($id, $pdfPath, 3, '')
95 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
96 }
97 [xml]$page = $pages[$id]
98 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
99 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
100 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
101 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
102 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
103 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
104 sha256 = $hash; bytes = $bytes.Length }
105 }
106 $index++
107 }
108 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
109 $all = ''
110 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
111 [xml]$finalTree = $all
112 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
113 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
114 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
115 }
116 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
117 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
118} catch {
119 $failure = $_
120 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
121 throw
122} finally {
123 try {
124 try {
125 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
126 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
127 $app.CloseNotebook($notebookId, $false)
128 }
129 } catch {
130 if ($null -eq $failure) { throw }
131 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
132 }
133 } finally {
134 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
135 }
136 $app = $null
137 [GC]::Collect()
138 [GC]::WaitForPendingFinalizers()
139 if (-not $UseCurrentCache -and -not $CloneHost) {
140 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
141 }
142}
corpus/picture-edit/resized/cold/source.json created+14
...@@ -0,0 +1,14 @@
1[
2 {
3 "path": "Open Notebook.onetoc2",
4 "bytes": 1612,
5 "sha256": "32c3fa727c56069e1c5e885e237b95dad32762b4c2ef269a2b0e566fa534382c",
6 "mtime_ns": 1789247968519334299
7 },
8 {
9 "path": "synthetic.one",
10 "bytes": 19040,
11 "sha256": "6cedb887b23e772353a266850d6bbf4f0a0e8ff6d0e6f513b2a1a8af8b2a37d8",
12 "mtime_ns": 1789247968518969882
13 }
14]
corpus/picture-edit/resized/cold/teardown.json created+1
...@@ -0,0 +1 @@
1{"absent": true}
crates/notebook/README.md+1-1
...@@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot,...@@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot,
14writes the difference into the working image and queues one14writes the difference into the working image and queues one
15`Operation::Page(PageIntent { before, after, author })`; `before` is the page the15`Operation::Page(PageIntent { before, after, author })`; `before` is the page the
16edit started from and is the precondition reconciliation checks. Text, styles,16edit started from and is the precondition reconciliation checks. Text, styles,
17paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, pictures and attachments, paragraph structure,17paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, pictures (insertion, size, description) and attachments, paragraph structure,
18outline layout, insertions and deletions are all differences between `before` and18outline layout, insertions and deletions are all differences between `before` and
19`after`; the library never sees editor operations.19`after`; the library never sees editor operations.
2020
crates/onestore/README.md+1-1
...@@ -57,7 +57,7 @@ harness also accepts `--client-profile release`....@@ -57,7 +57,7 @@ harness also accepts `--client-profile release`.
57| `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules |57| `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules |
58| `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs |58| `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs |
59| `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content |59| `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content |
60| `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 |60| `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 |
61| `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically |61| `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically |
62| `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter |62| `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter |
63| `read_file` | Read a snapshot under whole-file exclusion |63| `read_file` | Read a snapshot under whole-file exclusion |
crates/onestore/src/page/write.rs+62-5
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1//! Publishes an edited page model by lowering the difference from the stored page onto1//! Publishes an edited page model by lowering the difference from the stored page onto
2//! the typed writers, then squashing their transactions into one revision per space.2//! the typed writers, then squashing their transactions into one revision per space.
33
4use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent, Table};4use super::{Image, Outline, Page, PageObject, PageParagraph, ParagraphContent, Table};
5use crate::{5use crate::{
6 Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets,6 Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets,
7 RevisionIndex, Store, TextAttribute, TreeEdit, Value,7 RevisionIndex, Store, TextAttribute, TreeEdit, Value,
...@@ -386,8 +386,15 @@ impl Lowering<'_> {...@@ -386,8 +386,15 @@ impl Lowering<'_> {
386 }386 }
387 (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {}387 (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {}
388 (ParagraphContent::Image(a), ParagraphContent::Image(b)) => {388 (ParagraphContent::Image(a), ParagraphContent::Image(b)) => {
389 if a != b {389 if a.id != b.id
390 return Err(invalid("A stored picture cannot be edited"));390 || a.bytes != b.bytes
391 || a.size != b.size
392 || a.background != b.background
393 || (a.layout.x, a.layout.y) != (b.layout.x, b.layout.y)
394 {
395 return Err(invalid(
396 "A stored picture keeps its payload, intrinsic size, background state and position",
397 ));
391 }398 }
392 }399 }
393 (ParagraphContent::Attachment(a), ParagraphContent::Attachment(b)) => {400 (ParagraphContent::Attachment(a), ParagraphContent::Attachment(b)) => {
...@@ -753,8 +760,11 @@ impl Lowering<'_> {...@@ -753,8 +760,11 @@ impl Lowering<'_> {
753 continue;760 continue;
754 };761 };
755 if let Some(previous) = old.paragraphs.get(paragraph_id) {762 if let Some(previous) = old.paragraphs.get(paragraph_id) {
756 if previous.content != paragraph.content {763 let ParagraphContent::Image(stored) = &previous.content else {
757 return Err(invalid("A stored picture cannot be edited"));764 return Err(invalid("Paragraph content type cannot change"));
765 };
766 if stored != image {
767 self.resize_image(stored, image)?;
758 }768 }
759 continue;769 continue;
760 }770 }
...@@ -824,6 +834,53 @@ impl Lowering<'_> {...@@ -824,6 +834,53 @@ impl Lowering<'_> {
824 Ok(())834 Ok(())
825 }835 }
826836
837 /// Writes a displayed size and description the way OneNote stores a resized picture:
838 /// the layout width and height with the user flag, leaving the intrinsic size alone.
839 fn resize_image(&mut self, stored: &Image, image: &Image) -> Result<(), Error> {
840 let mut values: Values = vec![(
841 0x14001d7a,
842 crate::create::current_timestamps()?
843 .0
844 .to_le_bytes()
845 .to_vec(),
846 )];
847 let mut removed = Vec::new();
848 if image.layout != stored.layout {
849 match (image.layout.max_width, image.layout.max_height) {
850 (Some(width), Some(height)) => {
851 if !(width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0) {
852 return Err(invalid("Picture size must be positive"));
853 }
854 values.push((0x14001c1b, (width / 36.0).to_le_bytes().to_vec()));
855 values.push((0x14001c1c, (height / 36.0).to_le_bytes().to_vec()));
856 let user_set = u32::from(image.layout.width_set_by_user == Some(true));
857 values.push((0x08001cbd | (user_set << 31), Vec::new()));
858 }
859 (None, None) => removed.extend([0x14001c1b, 0x14001c1c, 0x08001cbd]),
860 _ => return Err(invalid("A picture size needs both dimensions")),
861 }
862 }
863 if image.alt != stored.alt {
864 match &image.alt {
865 Some(alt) => values.push((0x1c001e58, crate::create::string(alt))),
866 None => removed.push(0x1c001e58),
867 }
868 }
869 let (space, object) = (self.space, self.id(image.id));
870 self.apply(|current| {
871 crate::write::write_revision(current, space, |raw| {
872 let mut picture = PropertyObject::from_object(&raw.objects[&object])?;
873 picture.remove(&removed)?;
874 let values: Vec<(u32, &[u8])> = values
875 .iter()
876 .map(|(id, bytes)| (*id, bytes.as_slice()))
877 .collect();
878 picture.set(&values)?;
879 Ok(BTreeMap::from([(object, picture)]))
880 })
881 })
882 }
883
827 fn place_containers(884 fn place_containers(
828 &mut self,885 &mut self,
829 old: &View<'_>,886 old: &View<'_>,
crates/onestore/tests/page_images.rs+82-1
...@@ -1,12 +1,14 @@...@@ -1,12 +1,14 @@
1use onestore::{1use onestore::{
2 ExGuid, PreparedEdit, RevisionIndex, Store,2 ExGuid, PreparedEdit, RevisionIndex, Store,
3 document::{Document, Format},3 document::{Document, Format, Layout},
4 page::{Image, Page, PageObject, PageParagraph, ParagraphContent, TextObject, text::new_id},4 page::{Image, Page, PageObject, PageParagraph, ParagraphContent, TextObject, text::new_id},
5};5};
6use std::sync::Arc;6use std::sync::Arc;
77
8const NATIVE: &[u8] =8const NATIVE: &[u8] =
9 include_bytes!("../../../corpus/native/cold-05-05-image/notebook/synthetic.one");9 include_bytes!("../../../corpus/native/cold-05-05-image/notebook/synthetic.one");
10const NATIVE_RESIZE: &[u8] =
11 include_bytes!("../../../corpus/picture-edit/native-resize/notebook/pictures.one");
10const AUTHOR: &str = "Picture author";12const AUTHOR: &str = "Picture author";
11/// A one-pixel PNG.13/// A one-pixel PNG.
12const PNG: &[u8] = &[14const PNG: &[u8] = &[
...@@ -155,6 +157,85 @@ fn a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed() {...@@ -155,6 +157,85 @@ fn a_picture_inserted_on_a_fresh_page_reads_back_and_can_be_removed() {
155 assert_eq!(body_paragraphs(&mut stored.clone()).len(), 2);157 assert_eq!(body_paragraphs(&mut stored.clone()).len(), 2);
156}158}
157159
160fn resize(page: &mut Page, layout: Layout, alt: Option<&str>) -> ExGuid {
161 let mut found = None;
162 for object in &mut page.objects {
163 if let PageObject::Outline(outline) = object {
164 for paragraph in &mut outline.paragraphs {
165 if let ParagraphContent::Image(image) = &mut paragraph.content {
166 image.layout = layout.clone();
167 image.alt = alt.map(str::to_owned);
168 assert!(found.replace(image.id).is_none());
169 }
170 }
171 }
172 }
173 found.unwrap()
174}
175
176/// `ONESTORE_IMAGE_RESIZE_EXPORT` names a new directory receiving the candidate for a cold reopen.
177#[test]
178fn a_native_picture_is_resized_and_described_then_reset() {
179 let (space, native) = first_page(NATIVE);
180 let mut resized = native.clone();
181 let layout = Layout {
182 max_width: Some(144.0),
183 width_set_by_user: Some(true),
184 max_height: Some(108.0),
185 ..Default::default()
186 };
187 resize(&mut resized, layout, Some("Resized in Rust"));
188 let written = PreparedEdit::page(NATIVE, space, &resized, AUTHOR).unwrap();
189 let stored = page_in(written.as_bytes(), space);
190 assert_eq!(stored, resized);
191 assert_eq!(
192 PreparedEdit::page(written.as_bytes(), space, &stored, AUTHOR)
193 .unwrap()
194 .as_bytes(),
195 written.as_bytes()
196 );
197 if let Some(directory) = std::env::var_os("ONESTORE_IMAGE_RESIZE_EXPORT") {
198 let directory = std::path::PathBuf::from(directory);
199 std::fs::create_dir(&directory).unwrap();
200 std::fs::write(directory.join("synthetic.one"), written.as_bytes()).unwrap();
201 let written_store = Store::parse(written.as_bytes()).unwrap();
202 std::fs::write(
203 directory.join("Open Notebook.onetoc2"),
204 onestore::create_table_of_contents(
205 "Open Notebook.onetoc2",
206 &[("synthetic.one", written_store.header.file_id)],
207 )
208 .unwrap(),
209 )
210 .unwrap();
211 }
212 let mut reset = stored.clone();
213 resize(&mut reset, Layout::default(), None);
214 let again = PreparedEdit::page(written.as_bytes(), space, &reset, AUTHOR).unwrap();
215 assert_eq!(page_in(again.as_bytes(), space), native);
216}
217
218#[test]
219fn a_rust_picture_resized_natively_reads_back_with_its_size_and_description() {
220 let (space, page) = first_page(NATIVE_RESIZE);
221 let found = pictures(&page);
222 let [picture] = found.as_slice() else {
223 panic!("{page:?}");
224 };
225 assert_eq!(picture.bytes.as_deref(), Some(PNG));
226 assert_eq!(picture.size, Some([0.75, 0.75]));
227 assert_eq!(picture.layout.max_width, Some(144.0));
228 assert_eq!(picture.layout.max_height, Some(108.0));
229 assert_eq!(picture.layout.width_set_by_user, Some(true));
230 assert_eq!(picture.alt.as_deref(), Some("Resized by OneNote"));
231 let mut wider = page.clone();
232 let mut layout = picture.layout.clone();
233 layout.max_width = Some(200.0);
234 resize(&mut wider, layout, Some("Wider in Rust"));
235 let written = PreparedEdit::page(NATIVE_RESIZE, space, &wider, AUTHOR).unwrap();
236 assert_eq!(page_in(written.as_bytes(), space), wider);
237}
238
158#[test]239#[test]
159fn pictures_need_a_recognised_payload_and_stored_ones_stay_fixed() {240fn pictures_need_a_recognised_payload_and_stored_ones_stay_fixed() {
160 let source = onestore::create_section("pictures.one", "Text", "Author").unwrap();241 let source = onestore::create_section("pictures.one", "Text", "Author").unwrap();
tools/native/picture-edit.ps1 created+49
...@@ -0,0 +1,49 @@
1param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost)
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost
5$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote'
6$app = New-Object -ComObject OneNote.Application
7$notebookId = ''
8try {
9 $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0)
10 $sectionId = ''
11 $app.OpenHierarchy('pictures.one', $notebookId, [ref]$sectionId, 0)
12 $deadline = [DateTime]::UtcNow.AddSeconds(30)
13 $page = $null
14 do {
15 $hierarchy = ''
16 $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1)
17 [xml]$tree = $hierarchy
18 $pages = @($tree.SelectNodes('//*[local-name()="Page"]'))
19 if ($pages.Count -eq 1) {
20 $content = ''
21 $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1)
22 [xml]$page = $content
23 if (@($page.SelectNodes('//*[local-name()="Image"]')).Count -eq 1) { break }
24 }
25 Start-Sleep -Milliseconds 250
26 } while ([DateTime]::UtcNow -lt $deadline)
27 $images = @($page.SelectNodes('//*[local-name()="Image"]'))
28 if ($images.Count -ne 1) { throw 'Expected one picture on the page.' }
29 [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8)
30 $image = $images[0]
31 $image.SetAttribute('alt', 'Resized by OneNote')
32 $existing = $image.SelectSingleNode('*[local-name()="Size"]')
33 if ($existing) { [void]$image.RemoveChild($existing) }
34 $size = $page.CreateElement('one', 'Size', $namespace)
35 $size.SetAttribute('width', '144')
36 $size.SetAttribute('height', '108')
37 $size.SetAttribute('isSetByUser', 'true')
38 [void]$image.PrependChild($size)
39 [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8)
40 $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false)
41 $app.SyncHierarchy($notebookId)
42 $app.CloseNotebook($notebookId, $false)
43 $notebookId = ''
44} finally {
45 if ($notebookId) { $app.CloseNotebook($notebookId, $false) }
46 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
47 [GC]::Collect()
48 [GC]::WaitForPendingFinalizers()
49}
tools/test_picture_edit.py+31-7
...@@ -14,20 +14,44 @@ FIXTURE = ROOT / 'corpus/picture-edit'...@@ -14,20 +14,44 @@ FIXTURE = ROOT / 'corpus/picture-edit'
14compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']14compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
1515
1616
17def native_page(notebook, cold):
18 with TemporaryDirectory() as temporary:
19 read = Path(temporary) / 'read'
20 shutil.copytree(cold / 'read', read)
21 compare(notebook, read)
22 page, = (ET.parse(path).getroot() for path in sorted((cold / 'read').glob('page-*.xml')))
23 return page
24
25
26def texts(page):
27 return [re.sub(r'<[^>]*>', '', t.text or '') for t in page.findall('.//one:OE/one:T', ns)]
28
29
17class PictureEditTest(unittest.TestCase):30class PictureEditTest(unittest.TestCase):
18 def test_a_rust_inserted_picture_renders_natively_with_its_payload(self):31 def test_a_rust_inserted_picture_renders_natively_with_its_payload(self):
19 with TemporaryDirectory() as temporary:32 page = native_page(FIXTURE / 'inserted/candidate', FIXTURE / 'inserted/cold')
20 read = Path(temporary) / 'read'
21 shutil.copytree(FIXTURE / 'cold/read', read)
22 compare(FIXTURE / 'candidate', read)
23 page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml')))
24 image, = page.iter('{%s}Image' % ns['one'])33 image, = page.iter('{%s}Image' % ns['one'])
25 self.assertEqual(image.get('format'), 'png')34 self.assertEqual(image.get('format'), 'png')
26 payload = base64.b64decode(image.find('one:Data', ns).text)35 payload = base64.b64decode(image.find('one:Data', ns).text)
27 self.assertEqual(payload[:8], b'\x89PNG\r\n\x1a\n')36 self.assertEqual(payload[:8], b'\x89PNG\r\n\x1a\n')
28 self.assertEqual(len(payload), 69)37 self.assertEqual(len(payload), 69)
29 texts = [re.sub(r'<[^>]*>', '', t.text or '') for t in page.findall('.//one:OE/one:T', ns)]38 self.assertIsNone(image.find('one:Size', ns))
30 self.assertEqual(texts, ['Before the picture', 'After the picture'])39 self.assertEqual(texts(page), ['Before the picture', 'After the picture'])
40
41 def test_onenote_resizes_a_rust_inserted_picture_through_its_layout(self):
42 page = native_page(FIXTURE / 'native-resize/notebook', FIXTURE / 'native-resize')
43 image, = page.iter('{%s}Image' % ns['one'])
44 size = image.find('one:Size', ns)
45 self.assertEqual((size.get('width'), size.get('height'), size.get('isSetByUser')), ('144.0', '108.0', 'true'))
46 self.assertEqual(image.get('alt'), 'Resized by OneNote')
47 self.assertEqual(len(base64.b64decode(image.find('one:Data', ns).text)), 69)
48
49 def test_a_rust_resized_native_picture_renders_with_its_size_and_description(self):
50 page = native_page(FIXTURE / 'resized/candidate', FIXTURE / 'resized/cold')
51 image, = page.iter('{%s}Image' % ns['one'])
52 size = image.find('one:Size', ns)
53 self.assertEqual((size.get('width'), size.get('height'), size.get('isSetByUser')), ('144.0', '108.0', 'true'))
54 self.assertEqual(image.get('alt'), 'Resized in Rust')
3155
3256
33if __name__ == '__main__':57if __name__ == '__main__':