From dbeefb395cd869717223fa2518421c82fcc4c0d6 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sat, 10 Oct 2026 14:25:48 -0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20PDF=20adjust=20text=20recompute=20?= =?UTF-8?q?=E2=80=94=20overlap-based=20glyph=20dedupe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: extending a highlight doubled letters into the recomputed selection text ("aauuthenticcity off ... ruunss iinnto"). The exact- position dedupe key (toFixed(3)) only collapsed duplicate glyphs whose re-render copies landed inside the same rounding bucket — pdf.js re-render copies can sit SUB-PIXEL SHIFTED, so per character the jitter sometimes straddled a boundary and both copies survived, partially. Dedupe by overlap instead: a glyph is a duplicate when a kept glyph on the same line (y0 within half a glyph height) overlaps more than half the narrower glyph's width. Distinct glyphs — including tight pairs — never reach that; shifted duplicate copies always do. The snap tables share the source, so their boundary lists are clean too. The root cause remains the fork's pdf.js render() appending a fresh span set without clearing .textLayer (open bug, fork-cycle fix); this is the defensive layer that makes the adjust feature correct regardless. Repaired the affected highlight's stored text with the fixed recompute (verified: the same live duplicated DOM now yields the exact line text with zero doubling). --- web/src/reader/reader.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/web/src/reader/reader.ts b/web/src/reader/reader.ts index a64e2e1d..1a580954 100644 --- a/web/src/reader/reader.ts +++ b/web/src/reader/reader.ts @@ -1634,9 +1634,12 @@ document.addEventListener("alpine:init", () => { []; // pdf.js re-renders (zoom settle, scale updates) append a second // span set into the same .textLayer without clearing — char boxes - // come back duplicated at identical positions. Dedupe by box key so - // snap tables and recomputed text see each glyph once. - const seen = new Set(); + // come back duplicated, and the copies can sit SUB-PIXEL SHIFTED + // from the originals (a doubled-letter text recompute once slipped + // through an exact-position key). Dedupe by overlap instead: a + // candidate is a duplicate when a kept glyph on the same line + // overlaps more than half the narrower glyph. Distinct glyphs + // (including tight pairs like "il") never reach that. const range = doc.createRange(); const spans = doc.querySelectorAll(".textLayer span"); spans.forEach((span) => { @@ -1659,11 +1662,14 @@ document.addEventListener("alpine:init", () => { const y1 = (r.bottom - dr.top) / dr.height; const cy = (y0 + y1) / 2; if (cy < bandTop || cy > bandBottom) continue; - // 3 decimals ≈ sub-pixel tolerance: distinct glyphs are never - // this close, but re-render copies jitter less than this. - const key = `${x0.toFixed(3)}:${y0.toFixed(3)}`; - if (seen.has(key)) continue; - seen.add(key); + const w = x1 - x0; + const dup = out.some((k) => { + if (Math.abs(k.y0 - y0) > (k.y1 - k.y0) * 0.5) return false; + const kw = k.x1 - k.x0; + const overlap = Math.min(k.x1, x1) - Math.max(k.x0, x0); + return overlap > Math.min(kw, w) * 0.5; + }); + if (dup) continue; out.push({ x0, x1, y0, y1, ch: text[i] }); } });