From 36f101f5df85155b6a96701911be41e4fc5c893b Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 5 Oct 2026 10:30:17 +0200 Subject: [PATCH] fix(html): isolate annotation snapshots and option updates --- CHANGELOG.md | 3 ++ .../internal/html/frontend/pdf-annotation.js | 29 +++++++++++++------ test/browser/annotation/tests.html | 19 ++++++++++-- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c65a8f171..c220b4f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- PDF annotation snapshots no longer expose mutable pending state. Pointer + options are copied, and unknown option keys leave the settings unchanged. + - PDF ink annotations retain their final pointer-up position and ignore competing pointers until the active stroke ends. diff --git a/src/odr/internal/html/frontend/pdf-annotation.js b/src/odr/internal/html/frontend/pdf-annotation.js index 177a37cad..c1413bd05 100644 --- a/src/odr/internal/html/frontend/pdf-annotation.js +++ b/src/odr/internal/html/frontend/pdf-annotation.js @@ -10,6 +10,18 @@ var pending = []; var nextId = 1; + function copyArrays(value) { + return Array.isArray(value) ? value.map(copyArrays) : value; + } + + function snapshot(value) { + var copy = {}; + Object.keys(value).forEach(function (key) { + copy[key] = copyArrays(value[key]); + }); + return copy; + } + /// Gesture policy, the viewer's to set. `inkPointerTypes` null takes any. var options = { markOnSelection: @@ -659,29 +671,28 @@ }, /// Merged into what is set; an unknown key throws. setOptions: function (value) { - Object.keys(value || {}).forEach(function (key) { + var keys = Object.keys(value || {}); + keys.forEach(function (key) { if (!Object.prototype.hasOwnProperty.call(options, key)) { throw new Error("odr.annotation: unknown option " + key); } - options[key] = value[key]; + }); + keys.forEach(function (key) { + options[key] = copyArrays(value[key]); }); applyOptions(); }, getOptions: function () { - var copy = {}; - Object.keys(options).forEach(function (key) { - copy[key] = options[key]; - }); - return copy; + return snapshot(options); }, /// Marks the selection with the armed tool, and answers whether anything /// was added. The selection is left standing. mark: function () { return markSelection(tool, color, true); }, - /// What is pending, newest last. Geometry is in page-box points. + /// Snapshot of pending annotations, newest last, in page-box points. list: function () { - return pending.slice(); + return pending.map(snapshot); }, remove: function (id) { pending = pending.filter(function (a) { diff --git a/test/browser/annotation/tests.html b/test/browser/annotation/tests.html index 83e26232b..f82df2597 100644 --- a/test/browser/annotation/tests.html +++ b/test/browser/annotation/tests.html @@ -132,6 +132,12 @@ const run1 = document.querySelector('[data-odr-page="0"] .sr'); const first = payload().annotations[0]; + const savedHighlight = api.getAnnotations(); + const highlightSnapshot = api.list()[0]; + highlightSnapshot.color[0] = 0; + highlightSnapshot.boxes[0][0] = -100; + check("listed highlights do not expose pending geometry or color", + api.getAnnotations() === savedHighlight); check("it names its page", first.page === 0, first.page); check("it is a highlight", first.type === "highlight", first.type); check("it has one quad of eight numbers", first.quads.length === 1 && @@ -258,6 +264,11 @@ check("another pointer does not split the stroke", api.list().length === 1); const ink = payload().annotations[0]; + const savedInk = api.getAnnotations(); + const inkSnapshot = api.list()[0]; + inkSnapshot.width = 100; + inkSnapshot.strokes[0].push(0, 0); + check("listed strokes do not expose pending state", api.getAnnotations() === savedInk); check("a drag makes an ink annotation", ink && ink.type === "ink", ink); check("its width carries", ink && ink.width === 3, ink && ink.width); check( @@ -416,15 +427,19 @@ let threw = false; try { - api.setOptions({ nonesuch: 1 }); + api.setOptions({ touchAction: "auto", nonesuch: 1 }); } catch (e) { threw = true; } check("an unknown option throws rather than being ignored", threw); + check("a refused update leaves options unchanged", api.getOptions().touchAction === "none"); // --- pointer types the host does not want are not drawn with ---------- api.clear(); - api.setOptions({ inkPointerTypes: ["pen"] }); + const pointerTypes = ["pen"]; + api.setOptions({ inkPointerTypes: pointerTypes }); + pointerTypes.push("touch"); + api.getOptions().inkPointerTypes.push("touch"); const withType = (type, x, y) => Object.assign(at(x, y), { pointerType: type, pointerId: 7 }); document.dispatchEvent(new PointerEvent("pointerdown", withType("touch", 100, 200)));