mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 23:04:02 +02:00
Currently it's only possible to trigger page-render debugging through the page number, but when looking at the /Pages tree it's often not immediately obvious what the page number actually is. However, the /Ref of the page is directly available and it's thus handy to be able to use that one instead to enable page-render debugging.
273 lines
8.1 KiB
JavaScript
273 lines
8.1 KiB
JavaScript
/* Copyright 2026 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { getDocument, GlobalWorkerOptions, PasswordResponses } from "pdfjs-lib";
|
|
import { PageView } from "./page_view.js";
|
|
import { TreeView } from "./tree_view.js";
|
|
|
|
GlobalWorkerOptions.workerSrc =
|
|
typeof PDFJSDev === "undefined"
|
|
? "../../src/pdf.worker.js"
|
|
: "../build/pdf.worker.mjs";
|
|
|
|
// Parses "num" into { page: num }, or "numR"/"numRgen" into { ref: {num,gen} }.
|
|
// Returns null for invalid input.
|
|
function parseGoToInput(str) {
|
|
const match = str.trim().match(/^(\d+)(R(\d+)?)?$/i);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
if (!match[2]) {
|
|
return { page: parseInt(match[1], 10) };
|
|
}
|
|
return {
|
|
ref: {
|
|
num: parseInt(match[1], 10),
|
|
gen: match[3] !== undefined ? parseInt(match[3], 10) : 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Parses "num", "numR" or "numRgen" into { num, gen }, or returns null.
|
|
// Used for URL hash param parsing where a bare number means a ref, not a page.
|
|
function parseRefInput(str) {
|
|
const match = str.trim().match(/^(\d+)(?:R(\d+)?)?$/i);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
return {
|
|
num: parseInt(match[1], 10),
|
|
gen: match[2] !== undefined ? parseInt(match[2], 10) : 0,
|
|
};
|
|
}
|
|
|
|
let pdfDoc = null;
|
|
|
|
// Page number currently displayed in the tree (null when showing a
|
|
// ref/trailer).
|
|
let currentPage = null;
|
|
|
|
// Count of in-flight getRawData calls; drives the body "loading" cursor.
|
|
let loadingCount = 0;
|
|
function markLoading(delta) {
|
|
loadingCount += delta;
|
|
document.body.classList.toggle("loading", loadingCount > 0);
|
|
}
|
|
|
|
// Cache frequently accessed elements.
|
|
const treeButton = document.getElementById("tree-button");
|
|
const debugButton = document.getElementById("debug-button");
|
|
const debugViewEl = document.getElementById("debug-view");
|
|
const treeEl = document.getElementById("tree");
|
|
const statusEl = document.getElementById("status");
|
|
const gotoInput = document.getElementById("goto-input");
|
|
const pdfInfoEl = document.getElementById("pdf-info");
|
|
|
|
const pageView = new PageView({ onMarkLoading: markLoading });
|
|
|
|
const treeView = new TreeView(treeEl, { onMarkLoading: markLoading });
|
|
|
|
async function loadTree(data, rootLabel = null) {
|
|
currentPage = typeof data.page === "number" ? data.page : null;
|
|
debugButton.disabled = currentPage === null;
|
|
pageView.reset();
|
|
debugViewEl.hidden = true;
|
|
treeEl.hidden = false;
|
|
await treeView.load(data, rootLabel, pdfDoc);
|
|
}
|
|
|
|
async function openDocument(source, name) {
|
|
statusEl.textContent = `Loading ${name}…`;
|
|
pdfInfoEl.textContent = "";
|
|
treeView.clearCache();
|
|
|
|
if (pdfDoc) {
|
|
pageView.reset();
|
|
await pdfDoc.destroy();
|
|
pdfDoc = null;
|
|
}
|
|
|
|
const loadingTask = getDocument({
|
|
...source,
|
|
cMapUrl:
|
|
typeof PDFJSDev === "undefined" ? "../external/bcmaps/" : "../web/cmaps/",
|
|
iccUrl:
|
|
typeof PDFJSDev === "undefined" ? "../external/iccs/" : "../web/iccs/",
|
|
standardFontDataUrl:
|
|
typeof PDFJSDev === "undefined"
|
|
? "../external/standard_fonts/"
|
|
: "../web/standard_fonts/",
|
|
wasmUrl: "../web/wasm/",
|
|
useWorkerFetch: true,
|
|
pdfBug: true,
|
|
fontExtraProperties: true,
|
|
CanvasFactory: pageView.DebugCanvasFactory,
|
|
});
|
|
loadingTask.onPassword = (updateCallback, reason) => {
|
|
const dialog = document.getElementById("password-dialog");
|
|
const title = document.getElementById("password-dialog-title");
|
|
const input = document.getElementById("password-input");
|
|
const cancelButton = document.getElementById("password-cancel");
|
|
|
|
title.textContent =
|
|
reason === PasswordResponses.INCORRECT_PASSWORD
|
|
? "Incorrect password. Please try again:"
|
|
: "This PDF is password-protected. Please enter the password:";
|
|
input.value = "";
|
|
dialog.showModal();
|
|
|
|
const cleanup = () => {
|
|
dialog.removeEventListener("close", onSubmit);
|
|
cancelButton.removeEventListener("click", onCancel);
|
|
};
|
|
const onSubmit = () => {
|
|
cleanup();
|
|
updateCallback(input.value);
|
|
};
|
|
const onCancel = () => {
|
|
cleanup();
|
|
dialog.close();
|
|
updateCallback(new Error("Password prompt cancelled."));
|
|
};
|
|
|
|
dialog.addEventListener("close", onSubmit, { once: true });
|
|
cancelButton.addEventListener("click", onCancel, { once: true });
|
|
};
|
|
pdfDoc = await loadingTask.promise;
|
|
const plural = pdfDoc.numPages !== 1 ? "s" : "";
|
|
pdfInfoEl.textContent = `${name} — ${pdfDoc.numPages} page${plural}`;
|
|
statusEl.textContent = "";
|
|
gotoInput.disabled = false;
|
|
gotoInput.value = "";
|
|
}
|
|
|
|
function showError(err) {
|
|
statusEl.textContent = `Error: ${err.message}`;
|
|
treeView.showError(err.message);
|
|
}
|
|
|
|
document.getElementById("file-input").value = "";
|
|
|
|
document
|
|
.getElementById("file-input")
|
|
.addEventListener("change", async ({ target }) => {
|
|
const file = target.files[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
try {
|
|
await openDocument({ data: await file.arrayBuffer() }, file.name);
|
|
await loadTree({ ref: null }, "Trailer");
|
|
} catch (err) {
|
|
showError(err);
|
|
}
|
|
});
|
|
|
|
(async () => {
|
|
const searchParams = new URLSearchParams(location.search);
|
|
const hashParams = new URLSearchParams(location.hash.slice(1));
|
|
const fileUrl = searchParams.get("file");
|
|
if (!fileUrl) {
|
|
return;
|
|
}
|
|
try {
|
|
await openDocument({ url: fileUrl }, fileUrl.split("/").pop());
|
|
const refStr = hashParams.get("ref");
|
|
const pageStr = hashParams.get("page");
|
|
if (refStr) {
|
|
const ref = parseRefInput(refStr);
|
|
if (ref) {
|
|
gotoInput.value = refStr;
|
|
await loadTree({ ref });
|
|
return;
|
|
}
|
|
}
|
|
if (pageStr) {
|
|
const page = parseInt(pageStr, 10);
|
|
if (Number.isInteger(page) && page >= 1 && page <= pdfDoc.numPages) {
|
|
gotoInput.value = pageStr;
|
|
await loadTree({ page });
|
|
return;
|
|
}
|
|
}
|
|
await loadTree({ ref: null }, "Trailer");
|
|
} catch (err) {
|
|
showError(err);
|
|
}
|
|
})();
|
|
|
|
gotoInput.addEventListener("keydown", async ({ key, target }) => {
|
|
if (key !== "Enter" || !pdfDoc) {
|
|
return;
|
|
}
|
|
if (target.value.trim() === "") {
|
|
target.removeAttribute("aria-invalid");
|
|
await loadTree({ ref: null }, "Trailer");
|
|
return;
|
|
}
|
|
const result = parseGoToInput(target.value);
|
|
if (!result) {
|
|
target.setAttribute("aria-invalid", "true");
|
|
return;
|
|
}
|
|
if (
|
|
result.page !== undefined &&
|
|
(result.page < 1 || result.page > pdfDoc.numPages)
|
|
) {
|
|
target.setAttribute("aria-invalid", "true");
|
|
return;
|
|
}
|
|
target.removeAttribute("aria-invalid");
|
|
|
|
// Allow debugging via references, as well as page numbers.
|
|
if (result.page === undefined) {
|
|
try {
|
|
result.page =
|
|
pdfDoc.cachedPageNumber(result.ref) ??
|
|
(await pdfDoc.getPageIndex(result.ref)) + 1;
|
|
} catch {}
|
|
}
|
|
// If we're in debug view and navigating to a page, stay in debug view
|
|
// without switching to the tree at all.
|
|
if (!debugViewEl.hidden && result.page !== undefined) {
|
|
currentPage = result.page;
|
|
pageView.reset();
|
|
await pageView.show(pdfDoc, currentPage);
|
|
} else {
|
|
await (result.page !== undefined
|
|
? loadTree({ page: result.page })
|
|
: loadTree({ ref: result.ref }));
|
|
}
|
|
});
|
|
|
|
gotoInput.addEventListener("input", ({ target }) => {
|
|
if (target.value.trim() === "") {
|
|
target.removeAttribute("aria-invalid");
|
|
}
|
|
});
|
|
|
|
debugButton.addEventListener("click", async () => {
|
|
treeEl.hidden = true;
|
|
debugViewEl.hidden = false;
|
|
// Only render if not already loaded for this page; re-entering from the
|
|
// tree button keeps the existing debug state (op-list, canvas, breakpoints).
|
|
await pageView.show(pdfDoc, currentPage);
|
|
});
|
|
|
|
treeButton.addEventListener("click", () => {
|
|
debugViewEl.hidden = true;
|
|
treeEl.hidden = false;
|
|
});
|