dfgdfg
This commit is contained in:
@@ -1,263 +1,114 @@
|
||||
// PDF.js viewer using pdfjs-dist 6.1.200 legacy build.
|
||||
// Depends on global pdfjsLib (loaded in index.html).
|
||||
// Native Chromium PDF viewer using a single persistent iframe.
|
||||
// The iframe is created once in index.html and reused for all documents.
|
||||
|
||||
class PdfViewer {
|
||||
constructor(container) {
|
||||
class NativePdfViewer {
|
||||
constructor(frame, container) {
|
||||
this.frame = frame;
|
||||
this.container = container;
|
||||
this.generation = 0;
|
||||
this.loadingTask = null;
|
||||
this.doc = null;
|
||||
this.renderTasks = new Set();
|
||||
this.currentKey = null;
|
||||
this.lastUrl = null;
|
||||
this.lastSize = null;
|
||||
this.loadTimer = null;
|
||||
this._onLoad = null;
|
||||
this._createDOM();
|
||||
}
|
||||
|
||||
_createDOM() {
|
||||
// The pages container is always visible (never hidden).
|
||||
// Loading and error states are overlays on top.
|
||||
this.container.innerHTML = `
|
||||
<div class="pdf-v-pages"></div>
|
||||
<div class="pdf-v-overlay pdf-v-loading" hidden>Loading PDF\u2026</div>
|
||||
<div class="pdf-v-overlay pdf-v-error" hidden>
|
||||
<div class="pdf-v-error-msg"></div>
|
||||
<button class="pdf-v-retry">Retry</button>
|
||||
</div>
|
||||
`;
|
||||
this.pagesDiv = this.container.querySelector(".pdf-v-pages");
|
||||
this.loadingDiv = this.container.querySelector(".pdf-v-loading");
|
||||
this.errorDiv = this.container.querySelector(".pdf-v-error");
|
||||
this.errorMsg = this.container.querySelector(".pdf-v-error-msg");
|
||||
this.retryBtn = this.container.querySelector(".pdf-v-retry");
|
||||
this.loadingDiv = document.createElement("div");
|
||||
this.loadingDiv.className = "native-pdf-loading";
|
||||
this.loadingDiv.textContent = "Loading PDF\u2026";
|
||||
this.loadingDiv.hidden = true;
|
||||
this.container.appendChild(this.loadingDiv);
|
||||
|
||||
this.errorDiv = document.createElement("div");
|
||||
this.errorDiv.className = "native-pdf-error";
|
||||
this.errorDiv.hidden = true;
|
||||
|
||||
this.errorMsg = document.createElement("div");
|
||||
this.errorMsg.className = "native-pdf-error-msg";
|
||||
this.errorDiv.appendChild(this.errorMsg);
|
||||
|
||||
this.retryBtn = document.createElement("button");
|
||||
this.retryBtn.className = "native-pdf-retry";
|
||||
this.retryBtn.textContent = "Retry";
|
||||
this.retryBtn.addEventListener("click", () => {
|
||||
if (this.lastUrl != null) {
|
||||
this._setState("loading");
|
||||
this._loadInternal(this.lastUrl, this.lastSize);
|
||||
if (this.lastUrl && this.currentKey) {
|
||||
this._navigate(this.lastUrl, this.currentKey, true);
|
||||
}
|
||||
});
|
||||
this.errorDiv.appendChild(this.retryBtn);
|
||||
this.container.appendChild(this.errorDiv);
|
||||
|
||||
// Log iframe sizing once
|
||||
console.log(
|
||||
"[BizMatch QC] iframe size:",
|
||||
this.frame.clientWidth + "x" + this.frame.clientHeight,
|
||||
"container:",
|
||||
this.container.clientWidth + "x" + this.container.clientHeight,
|
||||
);
|
||||
}
|
||||
|
||||
_setState(state) {
|
||||
_showState(state) {
|
||||
this.loadingDiv.hidden = state !== "loading";
|
||||
this.errorDiv.hidden = state !== "error";
|
||||
this._viewerState = state;
|
||||
}
|
||||
|
||||
async load(url, byteSize) {
|
||||
this.lastUrl = url;
|
||||
this.lastSize = byteSize;
|
||||
this._setState("loading");
|
||||
await this._loadInternal(url, byteSize);
|
||||
load(url, _byteSize, key) {
|
||||
this._navigate(url, key, false);
|
||||
}
|
||||
|
||||
async _loadInternal(url, _byteSize) {
|
||||
_navigate(url, key, force) {
|
||||
// Deduplicate only AFTER the key is accepted
|
||||
if (!force && key && key === this.currentKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const generation = ++this.generation;
|
||||
this.currentKey = key;
|
||||
this.lastUrl = url;
|
||||
|
||||
this._showState("loading");
|
||||
console.log(`[BizMatch QC] Native PDF src assigned: ${key}`);
|
||||
console.log(
|
||||
`[BizMatch QC] PDF viewer load started: generation ${generation}`,
|
||||
`[BizMatch QC] Native PDF iframe size: ${this.frame.clientWidth}x${this.frame.clientHeight}`,
|
||||
);
|
||||
|
||||
await this._disposeCurrentDocument();
|
||||
if (generation !== this.generation) return;
|
||||
|
||||
this.pagesDiv.innerHTML = "";
|
||||
|
||||
const loadStart = performance.now();
|
||||
try {
|
||||
const task = pdfjsLib.getDocument({
|
||||
url,
|
||||
rangeChunkSize: 65536,
|
||||
disableAutoFetch: false,
|
||||
});
|
||||
this.loadingTask = task;
|
||||
|
||||
const doc = await task.promise;
|
||||
if (generation !== this.generation) {
|
||||
doc.destroy();
|
||||
return;
|
||||
// Register per-navigation load handler
|
||||
if (this._onLoad) {
|
||||
this.frame.removeEventListener("load", this._onLoad);
|
||||
}
|
||||
this._onLoad = () => {
|
||||
if (generation !== this.generation) return;
|
||||
if (this.loadTimer) {
|
||||
clearTimeout(this.loadTimer);
|
||||
this.loadTimer = null;
|
||||
}
|
||||
this.doc = doc;
|
||||
this._showState("loaded");
|
||||
console.log(`[BizMatch QC] Native PDF load event: ${key}`);
|
||||
};
|
||||
this.frame.addEventListener("load", this._onLoad, { once: true });
|
||||
|
||||
const numPages = doc.numPages;
|
||||
const loadMs = Math.round(performance.now() - loadStart);
|
||||
console.log(
|
||||
`[BizMatch QC] PDF document loaded: ${numPages} pages in ${loadMs}ms`,
|
||||
);
|
||||
|
||||
const viewerWidth = this._getViewportWidth();
|
||||
console.log(`[BizMatch QC] PDF usable render width: ${viewerWidth}px`);
|
||||
|
||||
// Render page 1 -- MUST succeed before we show ready state
|
||||
await this._renderPage(doc, 1, viewerWidth, generation);
|
||||
if (generation !== this.generation) return;
|
||||
this._setState("ready");
|
||||
console.log(
|
||||
`[BizMatch QC] PDF page 1 rendered in ${
|
||||
Math.round(performance.now() - loadStart)
|
||||
}ms`,
|
||||
);
|
||||
console.log(
|
||||
`[BizMatch QC] First page visible after ${
|
||||
Math.round(performance.now() - loadStart)
|
||||
}ms`,
|
||||
);
|
||||
|
||||
for (let p = 2; p <= numPages; p++) {
|
||||
if (generation !== this.generation) return;
|
||||
const pStart = performance.now();
|
||||
await this._renderPage(doc, p, viewerWidth, generation);
|
||||
if (generation !== this.generation) return;
|
||||
console.log(
|
||||
`[BizMatch QC] PDF page ${p} rendered in ${
|
||||
Math.round(performance.now() - pStart)
|
||||
}ms`,
|
||||
);
|
||||
await new Promise((r) => requestAnimationFrame(r));
|
||||
// Safety timeout
|
||||
if (this.loadTimer) clearTimeout(this.loadTimer);
|
||||
this.loadTimer = setTimeout(() => {
|
||||
if (generation === this.generation) {
|
||||
console.warn(`[BizMatch QC] Native PDF load timeout: ${key}`);
|
||||
this._showState("error");
|
||||
this.errorMsg.textContent =
|
||||
"PDF load timed out. The file may be very large.";
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
const totalMs = Math.round(performance.now() - loadStart);
|
||||
console.log(
|
||||
`[BizMatch QC] PDF viewer load completed: generation ${generation}`,
|
||||
);
|
||||
console.log(
|
||||
`[BizMatch QC] PDF fully rendered: ${numPages} pages in ${totalMs}ms`,
|
||||
);
|
||||
} catch (err) {
|
||||
if (generation !== this.generation) return;
|
||||
if (isCancellationError(err)) {
|
||||
console.log(`[BizMatch QC] PDF generation ${generation} cancelled`);
|
||||
return;
|
||||
}
|
||||
const msg = errorMsg(err);
|
||||
console.error(`[BizMatch QC] PDF load failed:`, msg, err?.stack);
|
||||
this.errorMsg.textContent = `Unable to open PDF: ${msg}`;
|
||||
this._setState("error");
|
||||
}
|
||||
}
|
||||
|
||||
_getViewportWidth() {
|
||||
const style = globalThis.getComputedStyle
|
||||
? getComputedStyle(this.pagesDiv)
|
||||
: null;
|
||||
let padding = 24; // default guess
|
||||
if (style) {
|
||||
padding = (parseFloat(style.paddingLeft) || 0) +
|
||||
(parseFloat(style.paddingRight) || 0);
|
||||
}
|
||||
const raw = Math.round(this.pagesDiv.clientWidth || 600);
|
||||
return Math.max(raw - padding, 200);
|
||||
}
|
||||
|
||||
async _disposeCurrentDocument() {
|
||||
for (const rt of this.renderTasks) {
|
||||
try {
|
||||
rt.cancel();
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
this.renderTasks.clear();
|
||||
|
||||
if (this.loadingTask) {
|
||||
try {
|
||||
await this.loadingTask.destroy();
|
||||
} catch { /* ok */ }
|
||||
this.loadingTask = null;
|
||||
}
|
||||
|
||||
if (this.doc) {
|
||||
try {
|
||||
await this.doc.destroy();
|
||||
} catch { /* ok */ }
|
||||
this.doc = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _renderPage(doc, pageNum, viewerWidth, generation) {
|
||||
const pageDiv = document.createElement("div");
|
||||
pageDiv.className = "pdf-v-page";
|
||||
pageDiv.dataset.page = String(pageNum);
|
||||
pageDiv.dataset.renderState = "idle";
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
pageDiv.appendChild(canvas);
|
||||
this.pagesDiv.appendChild(pageDiv);
|
||||
|
||||
pageDiv.dataset.renderState = "rendering";
|
||||
|
||||
try {
|
||||
const page = await doc.getPage(pageNum);
|
||||
if (generation !== this.generation) return;
|
||||
|
||||
const vp1 = page.getViewport({ scale: 1 });
|
||||
const fitScale = viewerWidth / vp1.width;
|
||||
const viewport = page.getViewport({ scale: fitScale });
|
||||
|
||||
// Use DPR 1 for initial diagnosis; restore DPR scaling after content renders
|
||||
const dpr = 1;
|
||||
canvas.width = Math.floor(viewport.width * dpr);
|
||||
canvas.height = Math.floor(viewport.height * dpr);
|
||||
canvas.style.width = `${Math.floor(viewport.width)}px`;
|
||||
canvas.style.height = `${Math.floor(viewport.height)}px`;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("2D canvas context is unavailable");
|
||||
|
||||
const renderTask = page.render({
|
||||
canvas,
|
||||
viewport,
|
||||
transform: undefined,
|
||||
});
|
||||
this.renderTasks.add(renderTask);
|
||||
|
||||
await renderTask.promise;
|
||||
this.renderTasks.delete(renderTask);
|
||||
|
||||
if (generation !== this.generation) return;
|
||||
pageDiv.dataset.renderState = "rendered";
|
||||
} catch (err) {
|
||||
if (generation !== this.generation) return;
|
||||
if (isCancellationError(err)) throw err;
|
||||
|
||||
console.error(
|
||||
`[BizMatch QC] Page ${pageNum} render failed`,
|
||||
err,
|
||||
err?.stack,
|
||||
);
|
||||
|
||||
if (pageNum === 1) {
|
||||
throw new Error(
|
||||
`Failed to render page 1: ${errorMsg(err)}`,
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
|
||||
// Inline error for later pages
|
||||
pageDiv.textContent = `Error rendering page ${pageNum}`;
|
||||
pageDiv.style.padding = "20px";
|
||||
pageDiv.style.color = "#f88";
|
||||
pageDiv.style.textAlign = "center";
|
||||
pageDiv.dataset.renderState = "error";
|
||||
}
|
||||
this.frame.src = url;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.loadTimer) clearTimeout(this.loadTimer);
|
||||
if (this._onLoad) this.frame.removeEventListener("load", this._onLoad);
|
||||
this.generation = 0;
|
||||
this._disposeCurrentDocument().catch(() => {});
|
||||
this.pagesDiv.innerHTML = "";
|
||||
this._setState("loading");
|
||||
this.frame.src = "";
|
||||
this._showState("loading");
|
||||
}
|
||||
}
|
||||
|
||||
function isCancellationError(err) {
|
||||
if (!(err instanceof Error)) return false;
|
||||
const msg = err.message || "";
|
||||
return msg.includes("cancelled") ||
|
||||
msg.includes("cancelled") ||
|
||||
msg.includes("destroyed") ||
|
||||
msg.includes("Worker was destroyed");
|
||||
}
|
||||
|
||||
function errorMsg(err) {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
|
||||
globalThis.PdfViewer = PdfViewer;
|
||||
globalThis.NativePdfViewer = NativePdfViewer;
|
||||
|
||||
Reference in New Issue
Block a user