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/candidate/Open Notebook.onetoc2
deleted file mode 100644
index 79f8f955db3b58cd2ae471c00ae2b66c95f81f62..0000000000000000000000000000000000000000
Binary files a/corpus/picture-edit/candidate/Open Notebook.onetoc2 and /dev/null differ
diff --git a/corpus/picture-edit/candidate/pictures.one b/corpus/picture-edit/candidate/pictures.one
deleted file mode 100644
index af4169c68f7c76ead1f32f25685131438a9fc7a1..0000000000000000000000000000000000000000
Binary files a/corpus/picture-edit/candidate/pictures.one and /dev/null differ
diff --git a/corpus/picture-edit/cold/commands.jsonl b/corpus/picture-edit/cold/commands.jsonl
deleted file mode 100644
index f077978e124b796017ac989959a7b5e48d27684c..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/commands.jsonl
+++ /dev/null
@@ -1,3 +0,0 @@
-{"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-A5A8705B -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/cold/machine.json b/corpus/picture-edit/cold/machine.json
deleted file mode 100644
index 80a47e2a0dd156babc8d6e6c895da3a3144fd28a..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/machine.json
+++ /dev/null
@@ -1 +0,0 @@
-{"name": "m6-a5a8705b", "hostname": "ONE-M6-A5A8705B"}
diff --git a/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2
deleted file mode 100644
index 836282818b6fe532b91bf32edf78fba4fe160b7a..0000000000000000000000000000000000000000
Binary files a/corpus/picture-edit/cold/notebook/Open Notebook.onetoc2 and /dev/null differ
diff --git a/corpus/picture-edit/cold/notebook/pictures.one b/corpus/picture-edit/cold/notebook/pictures.one
deleted file mode 100644
index 0dd87dcf4b41be4ca1b5b4b921d9462358aae3b1..0000000000000000000000000000000000000000
Binary files a/corpus/picture-edit/cold/notebook/pictures.one and /dev/null differ
diff --git a/corpus/picture-edit/cold/read/environment.json b/corpus/picture-edit/cold/read/environment.json
deleted file mode 100644
index f1032fa5ff0c8dddcee05b60f81366ec2671b1f9..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/read/environment.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "powershell": "5.1.14409.1005",
- "schema": "xs2010",
- "hostname": "ONE-M6-A5A8705B",
- "cold": true,
- "onenote": "14.0.4763.1000"
-}
diff --git a/corpus/picture-edit/cold/read/hierarchy.xml b/corpus/picture-edit/cold/read/hierarchy.xml
deleted file mode 100644
index dce86fad6744777127a6c19a6146cadb358f799f..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/read/hierarchy.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/corpus/picture-edit/cold/read/page-000.xml b/corpus/picture-edit/cold/read/page-000.xml
deleted file mode 100644
index aa8840aa5f2fb3e14fe2cb7223dc8638d73f1a92..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/read/page-000.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w
-AAAAAElFTkSuQmCC
-
diff --git a/corpus/picture-edit/cold/read/payloads.json b/corpus/picture-edit/cold/read/payloads.json
deleted file mode 100644
index 1ce91a413d6fff4d58765187b76756f0e97ae843..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/read/payloads.json
+++ /dev/null
@@ -1,3 +0,0 @@
-[
-
-]
\ No newline at end of file
diff --git a/corpus/picture-edit/cold/run.json b/corpus/picture-edit/cold/run.json
deleted file mode 100644
index 6e98c47fb0e8f3f15888fbc77851de46e6db6301..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/run.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "notebook": "/private/tmp/m4/pictures",
- "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/cold/scripts/cold.ps1 b/corpus/picture-edit/cold/scripts/cold.ps1
deleted file mode 100644
index a0b99e7aeb86f4dc10032537800501a52ad65b4f..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/scripts/cold.ps1
+++ /dev/null
@@ -1,27 +0,0 @@
-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/cold/scripts/read.ps1 b/corpus/picture-edit/cold/scripts/read.ps1
deleted file mode 100644
index 476094ab1e0c47077988a6461db2080cf208642b..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/scripts/read.ps1
+++ /dev/null
@@ -1,142 +0,0 @@
-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/cold/source.json b/corpus/picture-edit/cold/source.json
deleted file mode 100644
index 006d1122e0e6b26381b4b3c32834b6beb5866625..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/source.json
+++ /dev/null
@@ -1,14 +0,0 @@
-[
- {
- "path": "Open Notebook.onetoc2",
- "bytes": 1604,
- "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a",
- "mtime_ns": 1789243635388246438
- },
- {
- "path": "pictures.one",
- "bytes": 7064,
- "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70",
- "mtime_ns": 1789243635387879438
- }
-]
diff --git a/corpus/picture-edit/cold/teardown.json b/corpus/picture-edit/cold/teardown.json
deleted file mode 100644
index 05a47793de40f322e745c4c4183727e3590ecf70..0000000000000000000000000000000000000000
--- a/corpus/picture-edit/cold/teardown.json
+++ /dev/null
@@ -1 +0,0 @@
-{"absent": true}
diff --git a/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 b/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..79f8f955db3b58cd2ae471c00ae2b66c95f81f62
Binary files /dev/null and b/corpus/picture-edit/inserted/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/picture-edit/inserted/candidate/pictures.one b/corpus/picture-edit/inserted/candidate/pictures.one
new file mode 100644
index 0000000000000000000000000000000000000000..af4169c68f7c76ead1f32f25685131438a9fc7a1
Binary files /dev/null and b/corpus/picture-edit/inserted/candidate/pictures.one differ
diff --git a/corpus/picture-edit/inserted/cold/commands.jsonl b/corpus/picture-edit/inserted/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..f077978e124b796017ac989959a7b5e48d27684c
--- /dev/null
+++ b/corpus/picture-edit/inserted/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-A5A8705B -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/inserted/cold/machine.json b/corpus/picture-edit/inserted/cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..80a47e2a0dd156babc8d6e6c895da3a3144fd28a
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-a5a8705b", "hostname": "ONE-M6-A5A8705B"}
diff --git a/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 b/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..836282818b6fe532b91bf32edf78fba4fe160b7a
Binary files /dev/null and b/corpus/picture-edit/inserted/cold/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/picture-edit/inserted/cold/notebook/pictures.one b/corpus/picture-edit/inserted/cold/notebook/pictures.one
new file mode 100644
index 0000000000000000000000000000000000000000..0dd87dcf4b41be4ca1b5b4b921d9462358aae3b1
Binary files /dev/null and b/corpus/picture-edit/inserted/cold/notebook/pictures.one differ
diff --git a/corpus/picture-edit/inserted/cold/read/environment.json b/corpus/picture-edit/inserted/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..f1032fa5ff0c8dddcee05b60f81366ec2671b1f9
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-A5A8705B",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/picture-edit/inserted/cold/read/hierarchy.xml b/corpus/picture-edit/inserted/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dce86fad6744777127a6c19a6146cadb358f799f
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/picture-edit/inserted/cold/read/page-000.xml b/corpus/picture-edit/inserted/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..aa8840aa5f2fb3e14fe2cb7223dc8638d73f1a92
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/read/page-000.xml
@@ -0,0 +1,4 @@
+
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w
+AAAAAElFTkSuQmCC
+
diff --git a/corpus/picture-edit/inserted/cold/read/payloads.json b/corpus/picture-edit/inserted/cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/picture-edit/inserted/cold/run.json b/corpus/picture-edit/inserted/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..6e98c47fb0e8f3f15888fbc77851de46e6db6301
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/m4/pictures",
+ "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/inserted/cold/scripts/cold.ps1 b/corpus/picture-edit/inserted/cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/picture-edit/inserted/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/inserted/cold/scripts/read.ps1 b/corpus/picture-edit/inserted/cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/picture-edit/inserted/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/inserted/cold/source.json b/corpus/picture-edit/inserted/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..006d1122e0e6b26381b4b3c32834b6beb5866625
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 1604,
+ "sha256": "0eddd691b52d26bac222cf7e52add3c9909446647b452fb193f03d49e273426a",
+ "mtime_ns": 1789243635388246438
+ },
+ {
+ "path": "pictures.one",
+ "bytes": 7064,
+ "sha256": "3076880e02cf74eb85400852c133060ab8ed50383c6b9df8a07bde8389891a70",
+ "mtime_ns": 1789243635387879438
+ }
+]
diff --git a/corpus/picture-edit/inserted/cold/teardown.json b/corpus/picture-edit/inserted/cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/picture-edit/inserted/cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
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
Binary files /dev/null and b/corpus/picture-edit/native-resize/notebook/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/picture-edit/native-resize/notebook/pictures.one differ
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
Binary files /dev/null and b/corpus/picture-edit/resized/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/picture-edit/resized/candidate/synthetic.one b/corpus/picture-edit/resized/candidate/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..b97fd387030660493eadd435f7e110486261d358
Binary files /dev/null and b/corpus/picture-edit/resized/candidate/synthetic.one differ
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
Binary files /dev/null and b/corpus/picture-edit/resized/cold/notebook/Open Notebook.onetoc2 differ
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
Binary files /dev/null and b/corpus/picture-edit/resized/cold/notebook/synthetic.one differ
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__':