Fix rect annotations: host-side overlay instead of in-iframe SVG

The in-iframe SVG overlay was invisible on PDFs for two reasons:

- pdf.js applies transform: scale(1/devicePixelRatio) to the iframe's
  <html>; 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).
This commit is contained in:
2026-08-17 07:45:52 -04:00
parent aba68d8723
commit 1c0ebf331f
+43 -35
View File
@@ -773,48 +773,55 @@ export class FixedLayout extends HTMLElement {
return null; return null;
} }
// Full-bleed SVG in the page iframe with a 0-100 viewBox and // Host-side overlay: a div inside the frame's wrapper element, children
// preserveAspectRatio:none: fraction-space rects render at any scale, // positioned in percentages of the element box (which always equals the
// so iframe CSS-zoom (comics) and PDF re-renders need no re-anchoring. // visible page area). This is immune to iframe-internal transforms
#ensureOverlay(doc) { // (pdf.js scales <html> by 1/devicePixelRatio, which would shrink an
if (!doc?.body) return null; // in-document overlay), comic iframe CSS-scaling, PDF hi-res re-renders,
let svg = doc.getElementById("foliate-rect-overlay"); // and the host transform-based pan/zoom — no re-anchoring anywhere.
if (!svg) { #ensureOverlay(frame) {
const NS = "http://www.w3.org/2000/svg"; if (!frame?.element) return null;
svg = doc.createElementNS(NS, "svg"); let overlay = frame.element.querySelector(
svg.id = "foliate-rect-overlay"; ":scope > .foliate-rect-overlay",
svg.setAttribute("viewBox", "0 0 100 100"); );
svg.setAttribute("preserveAspectRatio", "none"); if (!overlay) {
svg.style.cssText = `position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;`; overlay = frame.element.ownerDocument.createElement("div");
doc.body.appendChild(svg); 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) { #renderAnnotationsIntoFrame(frame) {
const svg = this.#ensureOverlay(doc); if (!frame || frame.blank || frame.index == null) return;
if (!svg) return; const overlay = this.#ensureOverlay(frame);
const NS = "http://www.w3.org/2000/svg"; if (!overlay) return;
svg.replaceChildren(); const doc = frame.element.ownerDocument;
overlay.replaceChildren();
for (const a of this.#rectAnnotations.values()) { 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) { for (const [x, y, w, h] of a.rects) {
const rect = doc.createElementNS(NS, "rect"); const rect = doc.createElement("div");
rect.setAttribute("x", String(x * 100)); Object.assign(rect.style, {
rect.setAttribute("y", String(y * 100)); position: "absolute",
rect.setAttribute("width", String(w * 100)); left: `${x * 100}%`,
rect.setAttribute("height", String(h * 100)); top: `${y * 100}%`,
rect.setAttribute("fill", a.color || "#ffd54f"); width: `${w * 100}%`,
rect.setAttribute("fill-opacity", "0.35"); height: `${h * 100}%`,
svg.appendChild(rect); backgroundColor: a.color || "#ffd54f",
opacity: "0.35",
borderRadius: "2px",
});
overlay.appendChild(rect);
} }
} }
} }
#renderAnnotationsForIndex(index) { #renderAnnotationsForIndex(index) {
const frame = this.#frameForIndex(index); this.#renderAnnotationsIntoFrame(this.#frameForIndex(index));
const doc = frame?.iframe?.contentDocument;
if (doc) this.#renderAnnotationsInto(doc, index);
} }
addRectAnnotation({ key, index, rects, color }) { addRectAnnotation({ key, index, rects, color }) {
@@ -865,9 +872,10 @@ export class FixedLayout extends HTMLElement {
iframe, iframe,
index, index,
}); });
// Re-render persisted annotations into the fresh document // Re-render persisted annotations into the fresh frame (frames
// (frames are recreated on every spread change). // are recreated on every spread change; the spread fields aren't
this.#renderAnnotationsInto(doc, index); // assigned yet, so pass the frame under construction directly).
this.#renderAnnotationsIntoFrame({ element, iframe, index });
resolve({ resolve({
element, element,