From 1c0ebf331fa976babdde32204ad1ec34ea8061d3 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 17 Aug 2026 07:45:52 -0400 Subject: [PATCH] Fix rect annotations: host-side overlay instead of in-iframe SVG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-iframe SVG overlay was invisible on PDFs for two reasons: - pdf.js applies transform: scale(1/devicePixelRatio) to the iframe's ; an overlay inside that document shrinks into the top-left corner on any dpr != 1 display. The transform is also applied only after the page render, so load-time-injected overlays were sized before the scale factor existed. - Fraction rect geometry itself was correct (selection rects and the denominator are both post-transform), but nothing inside the iframe can escape its html transform. Render annotations host-side instead: a div inside the frame's wrapper element with percentage-positioned children. The wrapper box always equals the visible page area for both formats — comics (iframe CSS-scaled inside it) and PDFs (re-rendered at true scale) — so the overlay is immune to the html transform, zoom re-renders, iframe scaling, and host pan/zoom, still with zero re-anchoring. The in-iframe click hit-test is unchanged (it compares fractions against the same post-transform denominator). --- fixed-layout.js | 78 +++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/fixed-layout.js b/fixed-layout.js index 1cd5257..29113a9 100644 --- a/fixed-layout.js +++ b/fixed-layout.js @@ -773,48 +773,55 @@ export class FixedLayout extends HTMLElement { return null; } - // Full-bleed SVG in the page iframe with a 0-100 viewBox and - // preserveAspectRatio:none: fraction-space rects render at any scale, - // so iframe CSS-zoom (comics) and PDF re-renders need no re-anchoring. - #ensureOverlay(doc) { - if (!doc?.body) return null; - let svg = doc.getElementById("foliate-rect-overlay"); - if (!svg) { - const NS = "http://www.w3.org/2000/svg"; - svg = doc.createElementNS(NS, "svg"); - svg.id = "foliate-rect-overlay"; - svg.setAttribute("viewBox", "0 0 100 100"); - svg.setAttribute("preserveAspectRatio", "none"); - svg.style.cssText = `position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;`; - doc.body.appendChild(svg); + // Host-side overlay: a div inside the frame's wrapper element, children + // positioned in percentages of the element box (which always equals the + // visible page area). This is immune to iframe-internal transforms + // (pdf.js scales by 1/devicePixelRatio, which would shrink an + // in-document overlay), comic iframe CSS-scaling, PDF hi-res re-renders, + // and the host transform-based pan/zoom — no re-anchoring anywhere. + #ensureOverlay(frame) { + if (!frame?.element) return null; + let overlay = frame.element.querySelector( + ":scope > .foliate-rect-overlay", + ); + if (!overlay) { + overlay = frame.element.ownerDocument.createElement("div"); + overlay.className = "foliate-rect-overlay"; + overlay.style.cssText = + "position:absolute;inset:0;pointer-events:none;"; + frame.element.style.position = "relative"; + frame.element.appendChild(overlay); } - return svg; + return overlay; } - #renderAnnotationsInto(doc, index) { - const svg = this.#ensureOverlay(doc); - if (!svg) return; - const NS = "http://www.w3.org/2000/svg"; - svg.replaceChildren(); + #renderAnnotationsIntoFrame(frame) { + if (!frame || frame.blank || frame.index == null) return; + const overlay = this.#ensureOverlay(frame); + if (!overlay) return; + const doc = frame.element.ownerDocument; + overlay.replaceChildren(); for (const a of this.#rectAnnotations.values()) { - if (a.index !== index) continue; + if (a.index !== frame.index) continue; for (const [x, y, w, h] of a.rects) { - const rect = doc.createElementNS(NS, "rect"); - rect.setAttribute("x", String(x * 100)); - rect.setAttribute("y", String(y * 100)); - rect.setAttribute("width", String(w * 100)); - rect.setAttribute("height", String(h * 100)); - rect.setAttribute("fill", a.color || "#ffd54f"); - rect.setAttribute("fill-opacity", "0.35"); - svg.appendChild(rect); + const rect = doc.createElement("div"); + Object.assign(rect.style, { + position: "absolute", + left: `${x * 100}%`, + top: `${y * 100}%`, + width: `${w * 100}%`, + height: `${h * 100}%`, + backgroundColor: a.color || "#ffd54f", + opacity: "0.35", + borderRadius: "2px", + }); + overlay.appendChild(rect); } } } #renderAnnotationsForIndex(index) { - const frame = this.#frameForIndex(index); - const doc = frame?.iframe?.contentDocument; - if (doc) this.#renderAnnotationsInto(doc, index); + this.#renderAnnotationsIntoFrame(this.#frameForIndex(index)); } addRectAnnotation({ key, index, rects, color }) { @@ -865,9 +872,10 @@ export class FixedLayout extends HTMLElement { iframe, index, }); - // Re-render persisted annotations into the fresh document - // (frames are recreated on every spread change). - this.#renderAnnotationsInto(doc, index); + // Re-render persisted annotations into the fresh frame (frames + // are recreated on every spread change; the spread fields aren't + // assigned yet, so pass the frame under construction directly). + this.#renderAnnotationsIntoFrame({ element, iframe, index }); resolve({ element,