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] }); } });