Add PDF state persistence

This commit is contained in:
2026-03-30 09:29:19 +02:00
parent d1bb79570d
commit 4613b8e5dd
15 changed files with 380 additions and 55 deletions

View File

@@ -32,15 +32,17 @@ export class PageManager {
* @param {Function} dispatchRender - (pageNum, scale, vpWidth, vpHeight, gen) => void
*/
constructor(wrapper, viewports, initialScale, dpr, dispatchRender) {
this._wrapper = wrapper;
this._viewports = viewports;
this._scale = initialScale;
this._dpr = dpr;
this._wrapper = wrapper;
this._viewports = viewports;
this._scale = initialScale;
this._dpr = dpr;
this._dispatchRender = dispatchRender;
this._renderGen = 0;
this._states = new Array(viewports.length).fill(State.PLACEHOLDER);
this._canvases = new Array(viewports.length).fill(null);
this._wrappers = [];
this._renderGen = 0;
this._inFlight = 0; // renders dispatched but not yet completed/cancelled
this._zooming = false; // true during Phase 1 CSS zoom (before debounced re-render)
this._states = new Array(viewports.length).fill(State.PLACEHOLDER);
this._canvases = new Array(viewports.length).fill(null);
this._wrappers = [];
this._buildPlaceholders();
}
@@ -49,6 +51,9 @@ export class PageManager {
get numPages() { return this._viewports.length; }
get renderGen() { return this._renderGen; }
/** Called by ZoomController to suppress canvas teardown during CSS zoom. */
setZooming(z) { this._zooming = z; }
_buildPlaceholders() {
for (let i = 0; i < this._viewports.length; i++) {
const vp = this._viewports[i];
@@ -87,34 +92,45 @@ export class PageManager {
}
}
// Clean up pages no longer in the buffer
// Clean up pages no longer in the buffer.
// Skip during active CSS zoom (Phase 1): IO may report stale intersection
// data while the layout is still settling, and prematurely removing a
// canvas causes a visible dark flash. onScaleChange (Phase 2) handles the
// authoritative cleanup once the debounce fires.
for (let i = 0; i < this._viewports.length; i++) {
const pageNum = i + 1;
if (!bufferSet.has(pageNum) && this._states[i] === State.RENDERED) {
if (!this._zooming && !bufferSet.has(pageNum) && this._states[i] === State.RENDERED) {
this._cleanup(i);
}
// Also cancel stale RENDERING pages outside the buffer
// Also cancel stale RENDERING pages outside the buffer.
// Don't remove the canvas — it's off-screen and harmless, and tearing it
// down immediately causes a dark flash when IntersectionObserver fires
// between Phase 1 (CSS zoom) and Phase 2 (debounced re-render).
if (!bufferSet.has(pageNum) && this._states[i] === State.RENDERING) {
this._inFlight--;
this._states[i] = State.PLACEHOLDER;
}
}
}
get allRendered() { return this._inFlight === 0; }
_startRender(i, gen) {
const vp = this._viewports[i];
const scale = clampedRenderScale(vp.width, vp.height, this._scale, this._dpr);
this._states[i] = State.RENDERING;
this._inFlight++;
this._dispatchRender(i + 1, scale, vp.width, vp.height, gen);
}
_cleanup(i) {
if (this._states[i] === State.RENDERING) this._inFlight--;
this._states[i] = State.PLACEHOLDER;
const canvas = this._canvases[i];
if (canvas) {
canvas.remove();
this._canvases[i] = null;
}
// Reset wrapper size (without canvas it still holds placeholder dimensions)
}
/**
@@ -134,6 +150,7 @@ export class PageManager {
bitmap.close();
return;
}
this._inFlight--;
const vp = this._viewports[i];
const wrap = this._wrappers[i];
@@ -160,8 +177,11 @@ export class PageManager {
bitmap.close();
}
// Remove CSS zoom and set explicit size (canvas is now at the right dimensions)
wrap.style.zoom = "1";
// Set explicit wrapper size. Do NOT touch wrap.style.zoom here —
// ZoomController may have applied a CSS zoom since the last onScaleChange
// (the user kept zooming while this render was in-flight). Resetting zoom
// to "1" would briefly show the page at the wrong visual scale until the
// next onScaleChange corrects it, causing the "zoomed far in/out" flash.
wrap.style.width = cssW + "px";
wrap.style.height = cssH + "px";
wrap.appendChild(canvas);
@@ -202,9 +222,10 @@ export class PageManager {
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
}
if (this._states[i] !== State.PLACEHOLDER) {
this._states[i] = State.PLACEHOLDER;
if (this._states[i] === State.RENDERING) {
this._inFlight--;
}
this._states[i] = State.PLACEHOLDER;
} else {
// Off-screen: clean up immediately — not visible, so no flash.
const canvas = this._canvases[i];