fix(web): PDF adjust text recompute — overlap-based glyph dedupe
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).
This commit is contained in:
1 parent
8acaeddda5
commit
dbeefb395c
1 file changed
+14
-8
@@ -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<string>();
|
||||
// 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] });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user