7a89f4a789
Because of a capitalization error, the MIME type wasn't actually being set correctly. However, please note that downloading of font files still worked correctly which is probably why this has gone unnoticed since 2014.
622 lines
18 KiB
JavaScript
622 lines
18 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.
|
|
*/
|
|
|
|
let opMap;
|
|
|
|
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(pdfjsLib) {
|
|
const panel = this.panel;
|
|
const tmp = document.createElement("button");
|
|
tmp.addEventListener("click", resetSelection);
|
|
tmp.textContent = "Refresh";
|
|
panel.appendChild(tmp);
|
|
|
|
fonts = document.createElement("div");
|
|
panel.appendChild(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.appendChild(td1);
|
|
const td2 = document.createElement("td");
|
|
td2.textContent = obj[entry].toString();
|
|
tr.appendChild(td2);
|
|
moreInfo.appendChild(tr);
|
|
}
|
|
return moreInfo;
|
|
}
|
|
const moreInfo = properties(fontObj, ["name", "type"]);
|
|
const fontName = fontObj.loadedName;
|
|
const font = document.createElement("div");
|
|
const name = document.createElement("span");
|
|
name.textContent = fontName;
|
|
const 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);
|
|
});
|
|
font.appendChild(select);
|
|
font.appendChild(name);
|
|
font.appendChild(document.createTextNode(" "));
|
|
font.appendChild(download);
|
|
font.appendChild(document.createTextNode(" "));
|
|
font.appendChild(logIt);
|
|
font.appendChild(moreInfo);
|
|
fonts.appendChild(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(pdfjsLib) {
|
|
const self = this;
|
|
stepperControls = document.createElement("div");
|
|
stepperChooser = document.createElement("select");
|
|
stepperChooser.addEventListener("change", function (event) {
|
|
self.selectStepper(this.value);
|
|
});
|
|
stepperControls.appendChild(stepperChooser);
|
|
stepperDiv = document.createElement("div");
|
|
this.panel.appendChild(stepperControls);
|
|
this.panel.appendChild(stepperDiv);
|
|
if (sessionStorage.getItem("pdfjsBreakPoints")) {
|
|
breakPoints = JSON.parse(sessionStorage.getItem("pdfjsBreakPoints"));
|
|
}
|
|
|
|
opMap = Object.create(null);
|
|
for (const key in pdfjsLib.OPS) {
|
|
opMap[pdfjsLib.OPS[key]] = key;
|
|
}
|
|
},
|
|
cleanup() {
|
|
stepperChooser.textContent = "";
|
|
stepperDiv.textContent = "";
|
|
steppers = [];
|
|
},
|
|
enabled: false,
|
|
active: false,
|
|
// Stepper specific functions.
|
|
create(pageIndex) {
|
|
const debug = document.createElement("div");
|
|
debug.id = "stepper" + pageIndex;
|
|
debug.hidden = true;
|
|
debug.className = "stepper";
|
|
stepperDiv.appendChild(debug);
|
|
const b = document.createElement("option");
|
|
b.textContent = "Page " + (pageIndex + 1);
|
|
b.value = pageIndex;
|
|
stepperChooser.appendChild(b);
|
|
const initBreakPoints = breakPoints[pageIndex] || [];
|
|
const stepper = new Stepper(debug, pageIndex, initBreakPoints);
|
|
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.
|
|
const Stepper = (function StepperClosure() {
|
|
// Shorter way to create element and optionally set textContent.
|
|
function c(tag, textContent) {
|
|
const d = document.createElement(tag);
|
|
if (textContent) {
|
|
d.textContent = textContent;
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function 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(simplifyArgs(args[i]));
|
|
}
|
|
if (i < args.length) {
|
|
simpleArgs.push("...");
|
|
}
|
|
return simpleArgs;
|
|
}
|
|
const simpleObj = {};
|
|
for (const key in args) {
|
|
simpleObj[key] = simplifyArgs(args[key]);
|
|
}
|
|
return simpleObj;
|
|
}
|
|
|
|
// eslint-disable-next-line no-shadow
|
|
class Stepper {
|
|
constructor(panel, pageIndex, initialBreakPoints) {
|
|
this.panel = panel;
|
|
this.breakPoint = 0;
|
|
this.nextBreakPoint = null;
|
|
this.pageIndex = pageIndex;
|
|
this.breakPoints = initialBreakPoints;
|
|
this.currentIdx = -1;
|
|
this.operatorListIdx = 0;
|
|
this.indentLevel = 0;
|
|
}
|
|
|
|
init(operatorList) {
|
|
const panel = this.panel;
|
|
const content = c("div", "c=continue, s=step");
|
|
const table = c("table");
|
|
content.appendChild(table);
|
|
table.cellSpacing = 0;
|
|
const headerRow = c("tr");
|
|
table.appendChild(headerRow);
|
|
headerRow.appendChild(c("th", "Break"));
|
|
headerRow.appendChild(c("th", "Idx"));
|
|
headerRow.appendChild(c("th", "fn"));
|
|
headerRow.appendChild(c("th", "args"));
|
|
panel.appendChild(content);
|
|
this.table = table;
|
|
this.updateOperatorList(operatorList);
|
|
}
|
|
|
|
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 = c("tr");
|
|
line.className = "line";
|
|
line.dataset.idx = i;
|
|
chunk.appendChild(line);
|
|
const checked = this.breakPoints.includes(i);
|
|
const args = operatorList.argsArray[i] || [];
|
|
|
|
const breakCell = c("td");
|
|
const cbox = c("input");
|
|
cbox.type = "checkbox";
|
|
cbox.className = "points";
|
|
cbox.checked = checked;
|
|
cbox.dataset.idx = i;
|
|
cbox.onclick = cboxOnClick;
|
|
|
|
breakCell.appendChild(cbox);
|
|
line.appendChild(breakCell);
|
|
line.appendChild(c("td", i.toString()));
|
|
const fn = opMap[operatorList.fnArray[i]];
|
|
let decArgs = args;
|
|
if (fn === "showText") {
|
|
const glyphs = args[0];
|
|
const charCodeRow = c("tr");
|
|
const fontCharRow = c("tr");
|
|
const unicodeRow = c("tr");
|
|
for (const glyph of glyphs) {
|
|
if (typeof glyph === "object" && glyph !== null) {
|
|
charCodeRow.appendChild(c("td", glyph.originalCharCode));
|
|
fontCharRow.appendChild(c("td", glyph.fontChar));
|
|
unicodeRow.appendChild(c("td", glyph.unicode));
|
|
} else {
|
|
// null or number
|
|
const advanceEl = c("td", glyph);
|
|
advanceEl.classList.add("advance");
|
|
charCodeRow.appendChild(advanceEl);
|
|
fontCharRow.appendChild(c("td"));
|
|
unicodeRow.appendChild(c("td"));
|
|
}
|
|
}
|
|
decArgs = c("td");
|
|
const table = c("table");
|
|
table.classList.add("showText");
|
|
decArgs.appendChild(table);
|
|
table.appendChild(charCodeRow);
|
|
table.appendChild(fontCharRow);
|
|
table.appendChild(unicodeRow);
|
|
} else if (fn === "restore") {
|
|
this.indentLevel--;
|
|
}
|
|
line.appendChild(c("td", " ".repeat(this.indentLevel * 2) + fn));
|
|
if (fn === "save") {
|
|
this.indentLevel++;
|
|
}
|
|
|
|
if (decArgs instanceof HTMLElement) {
|
|
line.appendChild(decArgs);
|
|
} else {
|
|
line.appendChild(c("td", JSON.stringify(simplifyArgs(decArgs))));
|
|
}
|
|
}
|
|
if (operatorsToDisplay < operatorList.fnArray.length) {
|
|
const lastCell = c("td", "...");
|
|
lastCell.colspan = 4;
|
|
chunk.appendChild(lastCell);
|
|
}
|
|
this.operatorListIdx = operatorList.fnArray.length;
|
|
this.table.appendChild(chunk);
|
|
}
|
|
|
|
getNextBreakPoint() {
|
|
this.breakPoints.sort(function (a, b) {
|
|
return 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Stepper;
|
|
})();
|
|
|
|
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(pdfjsLib) {},
|
|
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.appendChild(title);
|
|
wrapper.appendChild(statsDiv);
|
|
stats.push({ pageNumber, div: wrapper });
|
|
stats.sort(function (a, b) {
|
|
return a.pageNumber - b.pageNumber;
|
|
});
|
|
clear(this.panel);
|
|
for (const entry of stats) {
|
|
this.panel.appendChild(entry.div);
|
|
}
|
|
},
|
|
cleanup() {
|
|
stats = [];
|
|
clear(this.panel);
|
|
},
|
|
};
|
|
})();
|
|
|
|
// Manages all the debugging tools.
|
|
const PDFBug = (function PDFBugClosure() {
|
|
const panelWidth = 300;
|
|
const buttons = [];
|
|
let activePanel = null;
|
|
|
|
return {
|
|
tools: [FontInspector, StepperManager, Stats],
|
|
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;
|
|
});
|
|
}
|
|
},
|
|
init(pdfjsLib, 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.appendChild(controls);
|
|
|
|
const panels = document.createElement("div");
|
|
panels.setAttribute("class", "panels");
|
|
ui.appendChild(panels);
|
|
|
|
container.appendChild(ui);
|
|
container.style.right = panelWidth + "px";
|
|
|
|
// 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.appendChild(panelButton);
|
|
panels.appendChild(panel);
|
|
tool.panel = panel;
|
|
tool.manager = this;
|
|
if (tool.enabled) {
|
|
tool.init(pdfjsLib);
|
|
} else {
|
|
panel.textContent =
|
|
`${tool.name} is disabled. To enable add "${tool.id}" to ` +
|
|
"the pdfBug parameter and refresh (separate multiple by commas).";
|
|
}
|
|
buttons.push(panelButton);
|
|
}
|
|
this.selectPanel(0);
|
|
},
|
|
loadCSS() {
|
|
const { url } = import.meta;
|
|
|
|
const link = document.createElement("link");
|
|
link.rel = "stylesheet";
|
|
link.href = url.replace(/.js$/, ".css");
|
|
|
|
document.head.appendChild(link);
|
|
},
|
|
cleanup() {
|
|
for (const tool of this.tools) {
|
|
if (tool.enabled) {
|
|
tool.cleanup();
|
|
}
|
|
}
|
|
},
|
|
selectPanel(index) {
|
|
if (typeof index !== "number") {
|
|
index = this.tools.indexOf(index);
|
|
}
|
|
if (index === activePanel) {
|
|
return;
|
|
}
|
|
activePanel = index;
|
|
for (const [j, tool] of this.tools.entries()) {
|
|
const isActive = j === index;
|
|
buttons[j].classList.toggle("active", isActive);
|
|
tool.active = isActive;
|
|
tool.panel.hidden = !isActive;
|
|
}
|
|
},
|
|
};
|
|
})();
|
|
|
|
globalThis.FontInspector = FontInspector;
|
|
globalThis.StepperManager = StepperManager;
|
|
globalThis.Stats = Stats;
|
|
|
|
export { PDFBug };
|