From 5385c89d0275cb487f5487cffb38f53ad5ab5c9d Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 20 Sep 2026 19:18:12 +0200 Subject: [PATCH] fix(html): ink lands where the finger went under a fit the rects leave out The ink tool mapped a pointer through a page's own rect. Every other input to that mapping is a rect, and a webkit view leaves the fit's zoom out of a rect while a pointer carries it, so the stroke landed as far out as the page is scaled - about twice, on a phone. A pointer now comes back through `odr.getViewportRect` before the page is picked and the point is mapped. Chromium reports both alike and was never affected, so the new browser check stands in a rect of its own to reach the case. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XASU9ZfSJ6QwsAJGE3DrN1 --- CHANGELOG.md | 3 ++ .../internal/html/frontend/pdf-annotation.js | 36 +++++++++++---- test/browser/annotation/tests.html | 44 +++++++++++++++++++ 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 338cbc597..e0ac9e2da 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 +- **Fix**: drawing on a pdf put the stroke about twice as far from the page's + corner as the finger went, in a webkit view that fits the page to the screen. + ## v7.2.1 - 2026-09-20 - **Fix**: marking text in a pdf view left the selection one handle, because a diff --git a/src/odr/internal/html/frontend/pdf-annotation.js b/src/odr/internal/html/frontend/pdf-annotation.js index 26d5f3e66..30a4bd71e 100644 --- a/src/odr/internal/html/frontend/pdf-annotation.js +++ b/src/odr/internal/html/frontend/pdf-annotation.js @@ -49,16 +49,26 @@ return null; } - /// A viewport point to page-box points (y-down, the unit the overlay draws + /// What an event's point is scaled by to reach the space the rects report, + /// which is the space `toBox` takes. A webkit view leaves the fit's zoom out + /// of a rect while an event carries it; a chromium one reports both alike. + /// Read once per event, not per sample: each call costs two layouts. + function layoutScale(page) { + if (!page || !odr.getViewportRect) { + return 1; + } + var drawn = odr.getViewportRect(page); + var raw = page.getBoundingClientRect(); + return drawn && drawn.width && raw.width ? raw.width / drawn.width : 1; + } + + /// A layout point to page-box points (y-down, the unit the overlay draws /// in). The page box is laid out in inches, so its own layout width in css /// pixels gives the scale a zoom transform is applied on top of. - function toBox(page, clientX, clientY) { + function toBox(page, x, y) { var rect = page.getBoundingClientRect(); var zoom = page.offsetWidth ? rect.width / page.offsetWidth : 1; - return [ - ((clientX - rect.left) / zoom) * 0.75, - ((clientY - rect.top) / zoom) * 0.75, - ]; + return [((x - rect.left) / zoom) * 0.75, ((y - rect.top) / zoom) * 0.75]; } /// Page-box points to pdf user space, through the page's own inverse. @@ -482,12 +492,15 @@ if (tool !== "ink" || event.button !== 0 || !inkTakes(event)) { return; } - var page = pageAt(event.clientX, event.clientY); + var scale = layoutScale(document.querySelector("[data-odr-page]")); + var x = event.clientX * scale; + var y = event.clientY * scale; + var page = pageAt(x, y); if (!page) { return; } event.preventDefault(); - var p = toBox(page, event.clientX, event.clientY); + var p = toBox(page, x, y); stroke = { id: nextId++, page: +page.getAttribute("data-odr-page"), @@ -518,8 +531,13 @@ samples = [event]; } var appended = false; + var scale = layoutScale(page); for (var i = 0; i < samples.length; ++i) { - var p = toBox(page, samples[i].clientX, samples[i].clientY); + var p = toBox( + page, + samples[i].clientX * scale, + samples[i].clientY * scale, + ); // drop the sub-point jitter a pointer emits while nearly still if ( Math.abs(p[0] - points[points.length - 2]) + diff --git a/test/browser/annotation/tests.html b/test/browser/annotation/tests.html index 06ca64e03..a9174702c 100644 --- a/test/browser/annotation/tests.html +++ b/test/browser/annotation/tests.html @@ -257,6 +257,50 @@ ink && ink.strokes[0].slice(0, 2) ); + // --- ink where the rects leave the fit out ---------------------------- + // Chromium reports the two alike and cannot show it, so the case is made + // by standing in a getViewportRect that halves the box. + api.clear(); + api.setTool("ink"); + const shrink = 0.5; + const drawnRect = (element) => { + const box = element.getBoundingClientRect(); + return { + left: box.left * shrink, + top: box.top * shrink, + right: box.right * shrink, + bottom: box.bottom * shrink, + width: box.width * shrink, + height: box.height * shrink, + }; + }; + const hadViewportRect = odr.getViewportRect; + odr.getViewportRect = drawnRect; + // the finger goes down on the drawn page, not the laid-out one + const drawn = drawnRect(page1); + const drawnZoom = drawn.width / page1.offsetWidth; + const onScreen = (bx, by) => ({ + clientX: drawn.left + (bx / 0.75) * drawnZoom, + clientY: drawn.top + (by / 0.75) * drawnZoom, + bubbles: true, + pointerId: 3, + button: 0, + }); + document.dispatchEvent(new PointerEvent("pointerdown", onScreen(100, 100))); + document.dispatchEvent(new PointerEvent("pointermove", onScreen(140, 140))); + document.dispatchEvent(new PointerEvent("pointerup", onScreen(140, 140))); + odr.getViewportRect = hadViewportRect; + + const fitted = payload().annotations[0]; + check( + "a fit the rects leave out still marks where the finger went", + fitted && + fitted.type === "ink" && + near(fitted.strokes[0][0], 100) && + near(fitted.strokes[0][1], 692), + fitted && fitted.strokes[0].slice(0, 2) + ); + // --- undo, remove, clear --------------------------------------------- const before = api.list().length; api.undo();