This commit is a first step towards #6419, and it can also help with first compute which ops can affect what is visible in that part of the page. This commit adds logic to track operations with their respective bounding boxes. Only operations that actually cause something to be rendered have a bounding box and dependencies. Consider the following example: ``` 0. setFillRGBColor 1. beginText 2. showText "Hello" 3. endText 4. constructPath [...] -> eoFill ``` here we have three rendering operations: the showText op (2) and the path (4). (2) depends on (0), (1) and (3), while (4) only depends on (0). Both (2) and (4) have a bounding box. This tracking happens when first rendering a PDF: we then use the recorded information to optimize future partial renderings of a PDF, so that we can skip operations that do not affected the PDF area on the canvas. All this logic only runs when the new `enableOptimizedPartialRendering` preference, disabled by default, is enabled. The bounding boxes and dependencies are also shown in the pdfBug stepper. When hovering over a step now: - it highlights the steps that they depend on - it highlights on the PDF itself the bounding box
770 lines
22 KiB
JavaScript
770 lines
22 KiB
JavaScript
/* Copyright 2012 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.
|
|
*/
|
|
|
|
const { OPS } = globalThis.pdfjsLib || (await import("pdfjs-lib"));
|
|
|
|
const opMap = Object.create(null);
|
|
for (const key in OPS) {
|
|
opMap[OPS[key]] = key;
|
|
}
|
|
|
|
const FontInspector = (function FontInspectorClosure() {
|
|
let fonts;
|
|
let active = false;
|
|
const fontAttribute = "data-font-name";
|
|
function removeSelection() {
|
|
const divs = document.querySelectorAll(`span[${fontAttribute}]`);
|
|
for (const div of divs) {
|
|
div.className = "";
|
|
}
|
|
}
|
|
function resetSelection() {
|
|
const divs = document.querySelectorAll(`span[${fontAttribute}]`);
|
|
for (const div of divs) {
|
|
div.className = "debuggerHideText";
|
|
}
|
|
}
|
|
function selectFont(fontName, show) {
|
|
const divs = document.querySelectorAll(
|
|
`span[${fontAttribute}=${fontName}]`
|
|
);
|
|
for (const div of divs) {
|
|
div.className = show ? "debuggerShowText" : "debuggerHideText";
|
|
}
|
|
}
|
|
function textLayerClick(e) {
|
|
if (
|
|
!e.target.dataset.fontName ||
|
|
e.target.tagName.toUpperCase() !== "SPAN"
|
|
) {
|
|
return;
|
|
}
|
|
const fontName = e.target.dataset.fontName;
|
|
const selects = document.getElementsByTagName("input");
|
|
for (const select of selects) {
|
|
if (select.dataset.fontName !== fontName) {
|
|
continue;
|
|
}
|
|
select.checked = !select.checked;
|
|
selectFont(fontName, select.checked);
|
|
select.scrollIntoView();
|
|
}
|
|
}
|
|
return {
|
|
// Properties/functions needed by PDFBug.
|
|
id: "FontInspector",
|
|
name: "Font Inspector",
|
|
panel: null,
|
|
manager: null,
|
|
init() {
|
|
const panel = this.panel;
|
|
const tmp = document.createElement("button");
|
|
tmp.addEventListener("click", resetSelection);
|
|
tmp.textContent = "Refresh";
|
|
panel.append(tmp);
|
|
|
|
fonts = document.createElement("div");
|
|
panel.append(fonts);
|
|
},
|
|
cleanup() {
|
|
fonts.textContent = "";
|
|
},
|
|
enabled: false,
|
|
get active() {
|
|
return active;
|
|
},
|
|
set active(value) {
|
|
active = value;
|
|
if (active) {
|
|
document.body.addEventListener("click", textLayerClick, true);
|
|
resetSelection();
|
|
} else {
|
|
document.body.removeEventListener("click", textLayerClick, true);
|
|
removeSelection();
|
|
}
|
|
},
|
|
// FontInspector specific functions.
|
|
fontAdded(fontObj, url) {
|
|
function properties(obj, list) {
|
|
const moreInfo = document.createElement("table");
|
|
for (const entry of list) {
|
|
const tr = document.createElement("tr");
|
|
const td1 = document.createElement("td");
|
|
td1.textContent = entry;
|
|
tr.append(td1);
|
|
const td2 = document.createElement("td");
|
|
td2.textContent = obj[entry].toString();
|
|
tr.append(td2);
|
|
moreInfo.append(tr);
|
|
}
|
|
return moreInfo;
|
|
}
|
|
|
|
const moreInfo = fontObj.css
|
|
? properties(fontObj, ["baseFontName"])
|
|
: properties(fontObj, ["name", "type"]);
|
|
|
|
const fontName = fontObj.loadedName;
|
|
const font = document.createElement("div");
|
|
const name = document.createElement("span");
|
|
name.textContent = fontName;
|
|
let download;
|
|
if (!fontObj.css) {
|
|
download = document.createElement("a");
|
|
if (url) {
|
|
url = /url\(['"]?([^)"']+)/.exec(url);
|
|
download.href = url[1];
|
|
} else if (fontObj.data) {
|
|
download.href = URL.createObjectURL(
|
|
new Blob([fontObj.data], { type: fontObj.mimetype })
|
|
);
|
|
}
|
|
download.textContent = "Download";
|
|
}
|
|
|
|
const logIt = document.createElement("a");
|
|
logIt.href = "";
|
|
logIt.textContent = "Log";
|
|
logIt.addEventListener("click", function (event) {
|
|
event.preventDefault();
|
|
console.log(fontObj);
|
|
});
|
|
const select = document.createElement("input");
|
|
select.setAttribute("type", "checkbox");
|
|
select.dataset.fontName = fontName;
|
|
select.addEventListener("click", function () {
|
|
selectFont(fontName, select.checked);
|
|
});
|
|
if (download) {
|
|
font.append(select, name, " ", download, " ", logIt, moreInfo);
|
|
} else {
|
|
font.append(select, name, " ", logIt, moreInfo);
|
|
}
|
|
fonts.append(font);
|
|
// Somewhat of a hack, should probably add a hook for when the text layer
|
|
// is done rendering.
|
|
setTimeout(() => {
|
|
if (this.active) {
|
|
resetSelection();
|
|
}
|
|
}, 2000);
|
|
},
|
|
};
|
|
})();
|
|
|
|
// Manages all the page steppers.
|
|
const StepperManager = (function StepperManagerClosure() {
|
|
let steppers = [];
|
|
let stepperDiv = null;
|
|
let stepperControls = null;
|
|
let stepperChooser = null;
|
|
let breakPoints = Object.create(null);
|
|
return {
|
|
// Properties/functions needed by PDFBug.
|
|
id: "Stepper",
|
|
name: "Stepper",
|
|
panel: null,
|
|
manager: null,
|
|
init() {
|
|
const self = this;
|
|
stepperControls = document.createElement("div");
|
|
stepperChooser = document.createElement("select");
|
|
stepperChooser.addEventListener("change", function (event) {
|
|
self.selectStepper(this.value);
|
|
});
|
|
stepperControls.append(stepperChooser);
|
|
stepperDiv = document.createElement("div");
|
|
this.panel.append(stepperControls, stepperDiv);
|
|
if (sessionStorage.getItem("pdfjsBreakPoints")) {
|
|
breakPoints = JSON.parse(sessionStorage.getItem("pdfjsBreakPoints"));
|
|
}
|
|
},
|
|
cleanup() {
|
|
stepperChooser.textContent = "";
|
|
stepperDiv.textContent = "";
|
|
steppers = [];
|
|
},
|
|
enabled: false,
|
|
active: false,
|
|
// Stepper specific functions.
|
|
create(pageIndex) {
|
|
const pageContainer = document.querySelector(
|
|
`#viewer div[data-page-number="${pageIndex + 1}"]`
|
|
);
|
|
|
|
const debug = document.createElement("div");
|
|
debug.id = "stepper" + pageIndex;
|
|
debug.hidden = true;
|
|
debug.className = "stepper";
|
|
stepperDiv.append(debug);
|
|
const b = document.createElement("option");
|
|
b.textContent = "Page " + (pageIndex + 1);
|
|
b.value = pageIndex;
|
|
stepperChooser.append(b);
|
|
const initBreakPoints = breakPoints[pageIndex] || [];
|
|
const stepper = new Stepper(
|
|
debug,
|
|
pageIndex,
|
|
initBreakPoints,
|
|
pageContainer
|
|
);
|
|
steppers.push(stepper);
|
|
if (steppers.length === 1) {
|
|
this.selectStepper(pageIndex, false);
|
|
}
|
|
return stepper;
|
|
},
|
|
selectStepper(pageIndex, selectPanel) {
|
|
pageIndex |= 0;
|
|
if (selectPanel) {
|
|
this.manager.selectPanel(this);
|
|
}
|
|
for (const stepper of steppers) {
|
|
stepper.panel.hidden = stepper.pageIndex !== pageIndex;
|
|
}
|
|
for (const option of stepperChooser.options) {
|
|
option.selected = (option.value | 0) === pageIndex;
|
|
}
|
|
},
|
|
saveBreakPoints(pageIndex, bps) {
|
|
breakPoints[pageIndex] = bps;
|
|
sessionStorage.setItem("pdfjsBreakPoints", JSON.stringify(breakPoints));
|
|
},
|
|
};
|
|
})();
|
|
|
|
// The stepper for each page's operatorList.
|
|
class Stepper {
|
|
// Shorter way to create element and optionally set textContent.
|
|
#c(tag, textContent) {
|
|
const d = document.createElement(tag);
|
|
if (textContent) {
|
|
d.textContent = textContent;
|
|
}
|
|
return d;
|
|
}
|
|
|
|
#simplifyArgs(args) {
|
|
if (typeof args === "string") {
|
|
const MAX_STRING_LENGTH = 75;
|
|
return args.length <= MAX_STRING_LENGTH
|
|
? args
|
|
: args.substring(0, MAX_STRING_LENGTH) + "...";
|
|
}
|
|
if (typeof args !== "object" || args === null) {
|
|
return args;
|
|
}
|
|
if ("length" in args) {
|
|
// array
|
|
const MAX_ITEMS = 10,
|
|
simpleArgs = [];
|
|
let i, ii;
|
|
for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) {
|
|
simpleArgs.push(this.#simplifyArgs(args[i]));
|
|
}
|
|
if (i < args.length) {
|
|
simpleArgs.push("...");
|
|
}
|
|
return simpleArgs;
|
|
}
|
|
const simpleObj = {};
|
|
for (const key in args) {
|
|
simpleObj[key] = this.#simplifyArgs(args[key]);
|
|
}
|
|
return simpleObj;
|
|
}
|
|
|
|
constructor(panel, pageIndex, initialBreakPoints, pageContainer) {
|
|
this.panel = panel;
|
|
this.breakPoint = 0;
|
|
this.nextBreakPoint = null;
|
|
this.pageIndex = pageIndex;
|
|
this.breakPoints = initialBreakPoints;
|
|
this.currentIdx = -1;
|
|
this.operatorListIdx = 0;
|
|
this.indentLevel = 0;
|
|
this.operatorGroups = null;
|
|
this.pageContainer = pageContainer;
|
|
}
|
|
|
|
init(operatorList) {
|
|
const panel = this.panel;
|
|
const content = this.#c("div", "c=continue, s=step");
|
|
|
|
const showBoxesToggle = this.#c("label", "Show bounding boxes");
|
|
const showBoxesCheckbox = this.#c("input");
|
|
showBoxesCheckbox.type = "checkbox";
|
|
showBoxesToggle.prepend(showBoxesCheckbox);
|
|
content.append(this.#c("br"), showBoxesToggle);
|
|
|
|
const table = this.#c("table");
|
|
content.append(table);
|
|
table.cellSpacing = 0;
|
|
const headerRow = this.#c("tr");
|
|
table.append(headerRow);
|
|
headerRow.append(
|
|
this.#c("th", "Break"),
|
|
this.#c("th", "Idx"),
|
|
this.#c("th", "fn"),
|
|
this.#c("th", "args")
|
|
);
|
|
panel.append(content);
|
|
this.table = table;
|
|
this.updateOperatorList(operatorList);
|
|
|
|
const hoverStyle = this.#c("style");
|
|
this.hoverStyle = hoverStyle;
|
|
content.prepend(hoverStyle);
|
|
table.addEventListener("mouseover", this.#handleStepHover.bind(this));
|
|
table.addEventListener("mouseleave", e => {
|
|
hoverStyle.innerText = "";
|
|
});
|
|
|
|
showBoxesCheckbox.addEventListener("change", () => {
|
|
if (showBoxesCheckbox.checked) {
|
|
this.pageContainer.classList.add("showDebugBoxes");
|
|
} else {
|
|
this.pageContainer.classList.remove("showDebugBoxes");
|
|
}
|
|
});
|
|
}
|
|
|
|
updateOperatorList(operatorList) {
|
|
const self = this;
|
|
|
|
function cboxOnClick() {
|
|
const x = +this.dataset.idx;
|
|
if (this.checked) {
|
|
self.breakPoints.push(x);
|
|
} else {
|
|
self.breakPoints.splice(self.breakPoints.indexOf(x), 1);
|
|
}
|
|
StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints);
|
|
}
|
|
|
|
const MAX_OPERATORS_COUNT = 15000;
|
|
if (this.operatorListIdx > MAX_OPERATORS_COUNT) {
|
|
return;
|
|
}
|
|
|
|
const chunk = document.createDocumentFragment();
|
|
const operatorsToDisplay = Math.min(
|
|
MAX_OPERATORS_COUNT,
|
|
operatorList.fnArray.length
|
|
);
|
|
for (let i = this.operatorListIdx; i < operatorsToDisplay; i++) {
|
|
const line = this.#c("tr");
|
|
line.className = "line";
|
|
line.dataset.idx = i;
|
|
chunk.append(line);
|
|
const checked = this.breakPoints.includes(i);
|
|
const args = operatorList.argsArray[i] || [];
|
|
|
|
const breakCell = this.#c("td");
|
|
const cbox = this.#c("input");
|
|
cbox.type = "checkbox";
|
|
cbox.className = "points";
|
|
cbox.checked = checked;
|
|
cbox.dataset.idx = i;
|
|
cbox.onclick = cboxOnClick;
|
|
|
|
breakCell.append(cbox);
|
|
line.append(breakCell, this.#c("td", i.toString()));
|
|
const fn = opMap[operatorList.fnArray[i]];
|
|
let decArgs = args;
|
|
if (fn === "showText") {
|
|
const glyphs = args[0];
|
|
const charCodeRow = this.#c("tr");
|
|
const fontCharRow = this.#c("tr");
|
|
const unicodeRow = this.#c("tr");
|
|
for (const glyph of glyphs) {
|
|
if (typeof glyph === "object" && glyph !== null) {
|
|
charCodeRow.append(this.#c("td", glyph.originalCharCode));
|
|
fontCharRow.append(this.#c("td", glyph.fontChar));
|
|
unicodeRow.append(this.#c("td", glyph.unicode));
|
|
} else {
|
|
// null or number
|
|
const advanceEl = this.#c("td", glyph);
|
|
advanceEl.classList.add("advance");
|
|
charCodeRow.append(advanceEl);
|
|
fontCharRow.append(this.#c("td"));
|
|
unicodeRow.append(this.#c("td"));
|
|
}
|
|
}
|
|
decArgs = this.#c("td");
|
|
const table = this.#c("table");
|
|
table.classList.add("showText");
|
|
decArgs.append(table);
|
|
table.append(charCodeRow, fontCharRow, unicodeRow);
|
|
} else if (fn === "constructPath") {
|
|
const [op, [path], minMax] = args;
|
|
decArgs = this.#c("td");
|
|
decArgs.append(JSON.stringify(this.#simplifyArgs(path)));
|
|
decArgs.append(this.#c("br"));
|
|
decArgs.append(`minMax: ${JSON.stringify(this.#simplifyArgs(minMax))}`);
|
|
decArgs.append(this.#c("br"));
|
|
decArgs.append(`→ ${opMap[op]}`);
|
|
} else if (fn === "restore" && this.indentLevel > 0) {
|
|
this.indentLevel--;
|
|
}
|
|
line.append(this.#c("td", " ".repeat(this.indentLevel * 2) + fn));
|
|
if (fn === "save") {
|
|
this.indentLevel++;
|
|
}
|
|
|
|
if (decArgs instanceof HTMLElement) {
|
|
line.append(decArgs);
|
|
} else {
|
|
line.append(this.#c("td", JSON.stringify(this.#simplifyArgs(decArgs))));
|
|
}
|
|
}
|
|
if (operatorsToDisplay < operatorList.fnArray.length) {
|
|
const lastCell = this.#c("td", "...");
|
|
lastCell.colspan = 4;
|
|
chunk.append(lastCell);
|
|
}
|
|
this.operatorListIdx = operatorList.fnArray.length;
|
|
this.table.append(chunk);
|
|
}
|
|
|
|
setOperatorGroups(groups) {
|
|
let boxesContainer = this.pageContainer.querySelector(".pdfBugGroupsLayer");
|
|
if (!boxesContainer) {
|
|
boxesContainer = this.#c("div");
|
|
boxesContainer.classList.add("pdfBugGroupsLayer");
|
|
this.pageContainer.append(boxesContainer);
|
|
|
|
boxesContainer.addEventListener(
|
|
"click",
|
|
this.#handleDebugBoxClick.bind(this)
|
|
);
|
|
boxesContainer.addEventListener(
|
|
"mouseover",
|
|
this.#handleDebugBoxHover.bind(this)
|
|
);
|
|
}
|
|
boxesContainer.innerHTML = "";
|
|
|
|
groups = groups.toSorted((a, b) => {
|
|
const diffs = [
|
|
a.minX - b.minX,
|
|
a.minY - b.minY,
|
|
b.maxX - a.maxX,
|
|
b.maxY - a.maxY,
|
|
];
|
|
for (const diff of diffs) {
|
|
if (Math.abs(diff) > 0.01) {
|
|
return Math.sign(diff);
|
|
}
|
|
}
|
|
for (const diff of diffs) {
|
|
if (Math.abs(diff) > 0.0001) {
|
|
return Math.sign(diff);
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
this.operatorGroups = groups;
|
|
|
|
for (let i = 0; i < groups.length; i++) {
|
|
const el = this.#c("div");
|
|
el.style.left = `${groups[i].minX * 100}%`;
|
|
el.style.top = `${groups[i].minY * 100}%`;
|
|
el.style.width = `${(groups[i].maxX - groups[i].minX) * 100}%`;
|
|
el.style.height = `${(groups[i].maxY - groups[i].minY) * 100}%`;
|
|
el.dataset.groupIdx = i;
|
|
boxesContainer.append(el);
|
|
}
|
|
}
|
|
|
|
#handleStepHover(e) {
|
|
const tr = e.target.closest("tr");
|
|
if (!tr || tr.dataset.idx === undefined) {
|
|
return;
|
|
}
|
|
|
|
const index = +tr.dataset.idx;
|
|
|
|
const closestGroupIndex =
|
|
this.operatorGroups?.findIndex(({ idx }) => idx === index) ?? -1;
|
|
if (closestGroupIndex === -1) {
|
|
this.hoverStyle.innerText = "";
|
|
return;
|
|
}
|
|
|
|
this.#highlightStepsGroup(closestGroupIndex);
|
|
}
|
|
|
|
#handleDebugBoxHover(e) {
|
|
if (e.target.dataset.groupIdx === undefined) {
|
|
return;
|
|
}
|
|
|
|
const groupIdx = Number(e.target.dataset.groupIdx);
|
|
this.#highlightStepsGroup(groupIdx);
|
|
}
|
|
|
|
#handleDebugBoxClick(e) {
|
|
if (e.target.dataset.groupIdx === undefined) {
|
|
return;
|
|
}
|
|
|
|
const groupIdx = Number(e.target.dataset.groupIdx);
|
|
const group = this.operatorGroups[groupIdx];
|
|
|
|
this.table.childNodes[group.idx].scrollIntoView();
|
|
}
|
|
|
|
#highlightStepsGroup(groupIndex) {
|
|
const group = this.operatorGroups[groupIndex];
|
|
|
|
this.hoverStyle.innerText = `#${this.panel.id} tr[data-idx="${group.idx}"] { background-color: rgba(0, 0, 0, 0.1); }`;
|
|
|
|
if (group.dependencies.length > 0) {
|
|
const selector = group.dependencies
|
|
.map(idx => `#${this.panel.id} tr[data-idx="${idx}"]`)
|
|
.join(", ");
|
|
this.hoverStyle.innerText += `${selector} { background-color: rgba(0, 255, 255, 0.1); }`;
|
|
}
|
|
|
|
this.hoverStyle.innerText += `
|
|
#viewer [data-page-number="${this.pageIndex + 1}"] .pdfBugGroupsLayer :nth-child(${groupIndex + 1}) {
|
|
background-color: var(--hover-background-color);
|
|
outline-style: var(--hover-outline-style);
|
|
}
|
|
`;
|
|
}
|
|
|
|
getNextBreakPoint() {
|
|
this.breakPoints.sort((a, b) => a - b);
|
|
for (const breakPoint of this.breakPoints) {
|
|
if (breakPoint > this.currentIdx) {
|
|
return breakPoint;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
breakIt(idx, callback) {
|
|
StepperManager.selectStepper(this.pageIndex, true);
|
|
this.currentIdx = idx;
|
|
|
|
const listener = evt => {
|
|
switch (evt.keyCode) {
|
|
case 83: // step
|
|
document.removeEventListener("keydown", listener);
|
|
this.nextBreakPoint = this.currentIdx + 1;
|
|
this.goTo(-1);
|
|
callback();
|
|
break;
|
|
case 67: // continue
|
|
document.removeEventListener("keydown", listener);
|
|
this.nextBreakPoint = this.getNextBreakPoint();
|
|
this.goTo(-1);
|
|
callback();
|
|
break;
|
|
}
|
|
};
|
|
document.addEventListener("keydown", listener);
|
|
this.goTo(idx);
|
|
}
|
|
|
|
goTo(idx) {
|
|
const allRows = this.panel.getElementsByClassName("line");
|
|
for (const row of allRows) {
|
|
if ((row.dataset.idx | 0) === idx) {
|
|
row.style.backgroundColor = "rgb(251,250,207)";
|
|
row.scrollIntoView();
|
|
} else {
|
|
row.style.backgroundColor = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const Stats = (function Stats() {
|
|
let stats = [];
|
|
function clear(node) {
|
|
node.textContent = ""; // Remove any `node` contents from the DOM.
|
|
}
|
|
function getStatIndex(pageNumber) {
|
|
for (const [i, stat] of stats.entries()) {
|
|
if (stat.pageNumber === pageNumber) {
|
|
return i;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
return {
|
|
// Properties/functions needed by PDFBug.
|
|
id: "Stats",
|
|
name: "Stats",
|
|
panel: null,
|
|
manager: null,
|
|
init() {},
|
|
enabled: false,
|
|
active: false,
|
|
// Stats specific functions.
|
|
add(pageNumber, stat) {
|
|
if (!stat) {
|
|
return;
|
|
}
|
|
const statsIndex = getStatIndex(pageNumber);
|
|
if (statsIndex !== false) {
|
|
stats[statsIndex].div.remove();
|
|
stats.splice(statsIndex, 1);
|
|
}
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "stats";
|
|
const title = document.createElement("div");
|
|
title.className = "title";
|
|
title.textContent = "Page: " + pageNumber;
|
|
const statsDiv = document.createElement("div");
|
|
statsDiv.textContent = stat.toString();
|
|
wrapper.append(title, statsDiv);
|
|
stats.push({ pageNumber, div: wrapper });
|
|
stats.sort((a, b) => a.pageNumber - b.pageNumber);
|
|
clear(this.panel);
|
|
for (const entry of stats) {
|
|
this.panel.append(entry.div);
|
|
}
|
|
},
|
|
cleanup() {
|
|
stats = [];
|
|
clear(this.panel);
|
|
},
|
|
};
|
|
})();
|
|
|
|
// Manages all the debugging tools.
|
|
class PDFBug {
|
|
static #buttons = [];
|
|
|
|
static #activePanel = null;
|
|
|
|
static tools = [FontInspector, StepperManager, Stats];
|
|
|
|
static enable(ids) {
|
|
const all = ids.length === 1 && ids[0] === "all";
|
|
const tools = this.tools;
|
|
for (const tool of tools) {
|
|
if (all || ids.includes(tool.id)) {
|
|
tool.enabled = true;
|
|
}
|
|
}
|
|
if (!all) {
|
|
// Sort the tools by the order they are enabled.
|
|
tools.sort(function (a, b) {
|
|
let indexA = ids.indexOf(a.id);
|
|
indexA = indexA < 0 ? tools.length : indexA;
|
|
let indexB = ids.indexOf(b.id);
|
|
indexB = indexB < 0 ? tools.length : indexB;
|
|
return indexA - indexB;
|
|
});
|
|
}
|
|
}
|
|
|
|
static init(container, ids) {
|
|
this.loadCSS();
|
|
this.enable(ids);
|
|
/*
|
|
* Basic Layout:
|
|
* PDFBug
|
|
* Controls
|
|
* Panels
|
|
* Panel
|
|
* Panel
|
|
* ...
|
|
*/
|
|
const ui = document.createElement("div");
|
|
ui.id = "PDFBug";
|
|
|
|
const controls = document.createElement("div");
|
|
controls.setAttribute("class", "controls");
|
|
ui.append(controls);
|
|
|
|
const panels = document.createElement("div");
|
|
panels.setAttribute("class", "panels");
|
|
ui.append(panels);
|
|
|
|
container.append(ui);
|
|
container.style.right = "var(--panel-width)";
|
|
|
|
// Initialize all the debugging tools.
|
|
for (const tool of this.tools) {
|
|
const panel = document.createElement("div");
|
|
const panelButton = document.createElement("button");
|
|
panelButton.textContent = tool.name;
|
|
panelButton.addEventListener("click", event => {
|
|
event.preventDefault();
|
|
this.selectPanel(tool);
|
|
});
|
|
controls.append(panelButton);
|
|
panels.append(panel);
|
|
tool.panel = panel;
|
|
tool.manager = this;
|
|
if (tool.enabled) {
|
|
tool.init();
|
|
} else {
|
|
panel.textContent =
|
|
`${tool.name} is disabled. To enable add "${tool.id}" to ` +
|
|
"the pdfBug parameter and refresh (separate multiple by commas).";
|
|
}
|
|
this.#buttons.push(panelButton);
|
|
}
|
|
this.selectPanel(0);
|
|
}
|
|
|
|
static loadCSS() {
|
|
const { url } = import.meta;
|
|
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = url.replace(/\.mjs$/, ".css");
|
|
|
|
document.head.append(link);
|
|
}
|
|
|
|
static cleanup() {
|
|
for (const tool of this.tools) {
|
|
if (tool.enabled) {
|
|
tool.cleanup();
|
|
}
|
|
}
|
|
}
|
|
|
|
static selectPanel(index) {
|
|
if (typeof index !== "number") {
|
|
index = this.tools.indexOf(index);
|
|
}
|
|
if (index === this.#activePanel) {
|
|
return;
|
|
}
|
|
this.#activePanel = index;
|
|
for (const [j, tool] of this.tools.entries()) {
|
|
const isActive = j === index;
|
|
this.#buttons[j].classList.toggle("active", isActive);
|
|
tool.active = isActive;
|
|
tool.panel.hidden = !isActive;
|
|
}
|
|
}
|
|
}
|
|
|
|
globalThis.FontInspector = FontInspector;
|
|
globalThis.StepperManager = StepperManager;
|
|
globalThis.Stats = Stats;
|
|
|
|
export { PDFBug };
|