debugger.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. /**
  2. * @licstart The following is the entire license notice for the
  3. * JavaScript code in this page
  4. *
  5. * Copyright 2022 Mozilla Foundation
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. *
  19. * @licend The above is the entire license notice for the
  20. * JavaScript code in this page
  21. */
  22. "use strict";
  23. Object.defineProperty(exports, "__esModule", {
  24. value: true
  25. });
  26. exports.PDFBug = void 0;
  27. let opMap;
  28. const FontInspector = function FontInspectorClosure() {
  29. let fonts;
  30. let active = false;
  31. const fontAttribute = "data-font-name";
  32. function removeSelection() {
  33. const divs = document.querySelectorAll(`span[${fontAttribute}]`);
  34. for (const div of divs) {
  35. div.className = "";
  36. }
  37. }
  38. function resetSelection() {
  39. const divs = document.querySelectorAll(`span[${fontAttribute}]`);
  40. for (const div of divs) {
  41. div.className = "debuggerHideText";
  42. }
  43. }
  44. function selectFont(fontName, show) {
  45. const divs = document.querySelectorAll(`span[${fontAttribute}=${fontName}]`);
  46. for (const div of divs) {
  47. div.className = show ? "debuggerShowText" : "debuggerHideText";
  48. }
  49. }
  50. function textLayerClick(e) {
  51. if (!e.target.dataset.fontName || e.target.tagName.toUpperCase() !== "SPAN") {
  52. return;
  53. }
  54. const fontName = e.target.dataset.fontName;
  55. const selects = document.getElementsByTagName("input");
  56. for (const select of selects) {
  57. if (select.dataset.fontName !== fontName) {
  58. continue;
  59. }
  60. select.checked = !select.checked;
  61. selectFont(fontName, select.checked);
  62. select.scrollIntoView();
  63. }
  64. }
  65. return {
  66. id: "FontInspector",
  67. name: "Font Inspector",
  68. panel: null,
  69. manager: null,
  70. init(pdfjsLib) {
  71. const panel = this.panel;
  72. const tmp = document.createElement("button");
  73. tmp.addEventListener("click", resetSelection);
  74. tmp.textContent = "Refresh";
  75. panel.append(tmp);
  76. fonts = document.createElement("div");
  77. panel.append(fonts);
  78. },
  79. cleanup() {
  80. fonts.textContent = "";
  81. },
  82. enabled: false,
  83. get active() {
  84. return active;
  85. },
  86. set active(value) {
  87. active = value;
  88. if (active) {
  89. document.body.addEventListener("click", textLayerClick, true);
  90. resetSelection();
  91. } else {
  92. document.body.removeEventListener("click", textLayerClick, true);
  93. removeSelection();
  94. }
  95. },
  96. fontAdded(fontObj, url) {
  97. function properties(obj, list) {
  98. const moreInfo = document.createElement("table");
  99. for (const entry of list) {
  100. const tr = document.createElement("tr");
  101. const td1 = document.createElement("td");
  102. td1.textContent = entry;
  103. tr.append(td1);
  104. const td2 = document.createElement("td");
  105. td2.textContent = obj[entry].toString();
  106. tr.append(td2);
  107. moreInfo.append(tr);
  108. }
  109. return moreInfo;
  110. }
  111. const moreInfo = properties(fontObj, ["name", "type"]);
  112. const fontName = fontObj.loadedName;
  113. const font = document.createElement("div");
  114. const name = document.createElement("span");
  115. name.textContent = fontName;
  116. const download = document.createElement("a");
  117. if (url) {
  118. url = /url\(['"]?([^)"']+)/.exec(url);
  119. download.href = url[1];
  120. } else if (fontObj.data) {
  121. download.href = URL.createObjectURL(new Blob([fontObj.data], {
  122. type: fontObj.mimetype
  123. }));
  124. }
  125. download.textContent = "Download";
  126. const logIt = document.createElement("a");
  127. logIt.href = "";
  128. logIt.textContent = "Log";
  129. logIt.addEventListener("click", function (event) {
  130. event.preventDefault();
  131. console.log(fontObj);
  132. });
  133. const select = document.createElement("input");
  134. select.setAttribute("type", "checkbox");
  135. select.dataset.fontName = fontName;
  136. select.addEventListener("click", function () {
  137. selectFont(fontName, select.checked);
  138. });
  139. font.append(select, name, " ", download, " ", logIt, moreInfo);
  140. fonts.append(font);
  141. setTimeout(() => {
  142. if (this.active) {
  143. resetSelection();
  144. }
  145. }, 2000);
  146. }
  147. };
  148. }();
  149. const StepperManager = function StepperManagerClosure() {
  150. let steppers = [];
  151. let stepperDiv = null;
  152. let stepperControls = null;
  153. let stepperChooser = null;
  154. let breakPoints = Object.create(null);
  155. return {
  156. id: "Stepper",
  157. name: "Stepper",
  158. panel: null,
  159. manager: null,
  160. init(pdfjsLib) {
  161. const self = this;
  162. stepperControls = document.createElement("div");
  163. stepperChooser = document.createElement("select");
  164. stepperChooser.addEventListener("change", function (event) {
  165. self.selectStepper(this.value);
  166. });
  167. stepperControls.append(stepperChooser);
  168. stepperDiv = document.createElement("div");
  169. this.panel.append(stepperControls, stepperDiv);
  170. if (sessionStorage.getItem("pdfjsBreakPoints")) {
  171. breakPoints = JSON.parse(sessionStorage.getItem("pdfjsBreakPoints"));
  172. }
  173. opMap = Object.create(null);
  174. for (const key in pdfjsLib.OPS) {
  175. opMap[pdfjsLib.OPS[key]] = key;
  176. }
  177. },
  178. cleanup() {
  179. stepperChooser.textContent = "";
  180. stepperDiv.textContent = "";
  181. steppers = [];
  182. },
  183. enabled: false,
  184. active: false,
  185. create(pageIndex) {
  186. const debug = document.createElement("div");
  187. debug.id = "stepper" + pageIndex;
  188. debug.hidden = true;
  189. debug.className = "stepper";
  190. stepperDiv.append(debug);
  191. const b = document.createElement("option");
  192. b.textContent = "Page " + (pageIndex + 1);
  193. b.value = pageIndex;
  194. stepperChooser.append(b);
  195. const initBreakPoints = breakPoints[pageIndex] || [];
  196. const stepper = new Stepper(debug, pageIndex, initBreakPoints);
  197. steppers.push(stepper);
  198. if (steppers.length === 1) {
  199. this.selectStepper(pageIndex, false);
  200. }
  201. return stepper;
  202. },
  203. selectStepper(pageIndex, selectPanel) {
  204. pageIndex |= 0;
  205. if (selectPanel) {
  206. this.manager.selectPanel(this);
  207. }
  208. for (const stepper of steppers) {
  209. stepper.panel.hidden = stepper.pageIndex !== pageIndex;
  210. }
  211. for (const option of stepperChooser.options) {
  212. option.selected = (option.value | 0) === pageIndex;
  213. }
  214. },
  215. saveBreakPoints(pageIndex, bps) {
  216. breakPoints[pageIndex] = bps;
  217. sessionStorage.setItem("pdfjsBreakPoints", JSON.stringify(breakPoints));
  218. }
  219. };
  220. }();
  221. const Stepper = function StepperClosure() {
  222. function c(tag, textContent) {
  223. const d = document.createElement(tag);
  224. if (textContent) {
  225. d.textContent = textContent;
  226. }
  227. return d;
  228. }
  229. function simplifyArgs(args) {
  230. if (typeof args === "string") {
  231. const MAX_STRING_LENGTH = 75;
  232. return args.length <= MAX_STRING_LENGTH ? args : args.substring(0, MAX_STRING_LENGTH) + "...";
  233. }
  234. if (typeof args !== "object" || args === null) {
  235. return args;
  236. }
  237. if ("length" in args) {
  238. const MAX_ITEMS = 10,
  239. simpleArgs = [];
  240. let i, ii;
  241. for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) {
  242. simpleArgs.push(simplifyArgs(args[i]));
  243. }
  244. if (i < args.length) {
  245. simpleArgs.push("...");
  246. }
  247. return simpleArgs;
  248. }
  249. const simpleObj = {};
  250. for (const key in args) {
  251. simpleObj[key] = simplifyArgs(args[key]);
  252. }
  253. return simpleObj;
  254. }
  255. class Stepper {
  256. constructor(panel, pageIndex, initialBreakPoints) {
  257. this.panel = panel;
  258. this.breakPoint = 0;
  259. this.nextBreakPoint = null;
  260. this.pageIndex = pageIndex;
  261. this.breakPoints = initialBreakPoints;
  262. this.currentIdx = -1;
  263. this.operatorListIdx = 0;
  264. this.indentLevel = 0;
  265. }
  266. init(operatorList) {
  267. const panel = this.panel;
  268. const content = c("div", "c=continue, s=step");
  269. const table = c("table");
  270. content.append(table);
  271. table.cellSpacing = 0;
  272. const headerRow = c("tr");
  273. table.append(headerRow);
  274. headerRow.append(c("th", "Break"), c("th", "Idx"), c("th", "fn"), c("th", "args"));
  275. panel.append(content);
  276. this.table = table;
  277. this.updateOperatorList(operatorList);
  278. }
  279. updateOperatorList(operatorList) {
  280. const self = this;
  281. function cboxOnClick() {
  282. const x = +this.dataset.idx;
  283. if (this.checked) {
  284. self.breakPoints.push(x);
  285. } else {
  286. self.breakPoints.splice(self.breakPoints.indexOf(x), 1);
  287. }
  288. StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints);
  289. }
  290. const MAX_OPERATORS_COUNT = 15000;
  291. if (this.operatorListIdx > MAX_OPERATORS_COUNT) {
  292. return;
  293. }
  294. const chunk = document.createDocumentFragment();
  295. const operatorsToDisplay = Math.min(MAX_OPERATORS_COUNT, operatorList.fnArray.length);
  296. for (let i = this.operatorListIdx; i < operatorsToDisplay; i++) {
  297. const line = c("tr");
  298. line.className = "line";
  299. line.dataset.idx = i;
  300. chunk.append(line);
  301. const checked = this.breakPoints.includes(i);
  302. const args = operatorList.argsArray[i] || [];
  303. const breakCell = c("td");
  304. const cbox = c("input");
  305. cbox.type = "checkbox";
  306. cbox.className = "points";
  307. cbox.checked = checked;
  308. cbox.dataset.idx = i;
  309. cbox.onclick = cboxOnClick;
  310. breakCell.append(cbox);
  311. line.append(breakCell, c("td", i.toString()));
  312. const fn = opMap[operatorList.fnArray[i]];
  313. let decArgs = args;
  314. if (fn === "showText") {
  315. const glyphs = args[0];
  316. const charCodeRow = c("tr");
  317. const fontCharRow = c("tr");
  318. const unicodeRow = c("tr");
  319. for (const glyph of glyphs) {
  320. if (typeof glyph === "object" && glyph !== null) {
  321. charCodeRow.append(c("td", glyph.originalCharCode));
  322. fontCharRow.append(c("td", glyph.fontChar));
  323. unicodeRow.append(c("td", glyph.unicode));
  324. } else {
  325. const advanceEl = c("td", glyph);
  326. advanceEl.classList.add("advance");
  327. charCodeRow.append(advanceEl);
  328. fontCharRow.append(c("td"));
  329. unicodeRow.append(c("td"));
  330. }
  331. }
  332. decArgs = c("td");
  333. const table = c("table");
  334. table.classList.add("showText");
  335. decArgs.append(table);
  336. table.append(charCodeRow, fontCharRow, unicodeRow);
  337. } else if (fn === "restore") {
  338. this.indentLevel--;
  339. }
  340. line.append(c("td", " ".repeat(this.indentLevel * 2) + fn));
  341. if (fn === "save") {
  342. this.indentLevel++;
  343. }
  344. if (decArgs instanceof HTMLElement) {
  345. line.append(decArgs);
  346. } else {
  347. line.append(c("td", JSON.stringify(simplifyArgs(decArgs))));
  348. }
  349. }
  350. if (operatorsToDisplay < operatorList.fnArray.length) {
  351. const lastCell = c("td", "...");
  352. lastCell.colspan = 4;
  353. chunk.append(lastCell);
  354. }
  355. this.operatorListIdx = operatorList.fnArray.length;
  356. this.table.append(chunk);
  357. }
  358. getNextBreakPoint() {
  359. this.breakPoints.sort(function (a, b) {
  360. return a - b;
  361. });
  362. for (const breakPoint of this.breakPoints) {
  363. if (breakPoint > this.currentIdx) {
  364. return breakPoint;
  365. }
  366. }
  367. return null;
  368. }
  369. breakIt(idx, callback) {
  370. StepperManager.selectStepper(this.pageIndex, true);
  371. this.currentIdx = idx;
  372. const listener = evt => {
  373. switch (evt.keyCode) {
  374. case 83:
  375. document.removeEventListener("keydown", listener);
  376. this.nextBreakPoint = this.currentIdx + 1;
  377. this.goTo(-1);
  378. callback();
  379. break;
  380. case 67:
  381. document.removeEventListener("keydown", listener);
  382. this.nextBreakPoint = this.getNextBreakPoint();
  383. this.goTo(-1);
  384. callback();
  385. break;
  386. }
  387. };
  388. document.addEventListener("keydown", listener);
  389. this.goTo(idx);
  390. }
  391. goTo(idx) {
  392. const allRows = this.panel.getElementsByClassName("line");
  393. for (const row of allRows) {
  394. if ((row.dataset.idx | 0) === idx) {
  395. row.style.backgroundColor = "rgb(251,250,207)";
  396. row.scrollIntoView();
  397. } else {
  398. row.style.backgroundColor = null;
  399. }
  400. }
  401. }
  402. }
  403. return Stepper;
  404. }();
  405. const Stats = function Stats() {
  406. let stats = [];
  407. function clear(node) {
  408. node.textContent = "";
  409. }
  410. function getStatIndex(pageNumber) {
  411. for (const [i, stat] of stats.entries()) {
  412. if (stat.pageNumber === pageNumber) {
  413. return i;
  414. }
  415. }
  416. return false;
  417. }
  418. return {
  419. id: "Stats",
  420. name: "Stats",
  421. panel: null,
  422. manager: null,
  423. init(pdfjsLib) {},
  424. enabled: false,
  425. active: false,
  426. add(pageNumber, stat) {
  427. if (!stat) {
  428. return;
  429. }
  430. const statsIndex = getStatIndex(pageNumber);
  431. if (statsIndex !== false) {
  432. stats[statsIndex].div.remove();
  433. stats.splice(statsIndex, 1);
  434. }
  435. const wrapper = document.createElement("div");
  436. wrapper.className = "stats";
  437. const title = document.createElement("div");
  438. title.className = "title";
  439. title.textContent = "Page: " + pageNumber;
  440. const statsDiv = document.createElement("div");
  441. statsDiv.textContent = stat.toString();
  442. wrapper.append(title, statsDiv);
  443. stats.push({
  444. pageNumber,
  445. div: wrapper
  446. });
  447. stats.sort(function (a, b) {
  448. return a.pageNumber - b.pageNumber;
  449. });
  450. clear(this.panel);
  451. for (const entry of stats) {
  452. this.panel.append(entry.div);
  453. }
  454. },
  455. cleanup() {
  456. stats = [];
  457. clear(this.panel);
  458. }
  459. };
  460. }();
  461. const PDFBug = function PDFBugClosure() {
  462. const panelWidth = 300;
  463. const buttons = [];
  464. let activePanel = null;
  465. return {
  466. tools: [FontInspector, StepperManager, Stats],
  467. enable(ids) {
  468. const all = ids.length === 1 && ids[0] === "all";
  469. const tools = this.tools;
  470. for (const tool of tools) {
  471. if (all || ids.includes(tool.id)) {
  472. tool.enabled = true;
  473. }
  474. }
  475. if (!all) {
  476. tools.sort(function (a, b) {
  477. let indexA = ids.indexOf(a.id);
  478. indexA = indexA < 0 ? tools.length : indexA;
  479. let indexB = ids.indexOf(b.id);
  480. indexB = indexB < 0 ? tools.length : indexB;
  481. return indexA - indexB;
  482. });
  483. }
  484. },
  485. init(pdfjsLib, container, ids) {
  486. this.loadCSS();
  487. this.enable(ids);
  488. const ui = document.createElement("div");
  489. ui.id = "PDFBug";
  490. const controls = document.createElement("div");
  491. controls.setAttribute("class", "controls");
  492. ui.append(controls);
  493. const panels = document.createElement("div");
  494. panels.setAttribute("class", "panels");
  495. ui.append(panels);
  496. container.append(ui);
  497. container.style.right = panelWidth + "px";
  498. for (const tool of this.tools) {
  499. const panel = document.createElement("div");
  500. const panelButton = document.createElement("button");
  501. panelButton.textContent = tool.name;
  502. panelButton.addEventListener("click", event => {
  503. event.preventDefault();
  504. this.selectPanel(tool);
  505. });
  506. controls.append(panelButton);
  507. panels.append(panel);
  508. tool.panel = panel;
  509. tool.manager = this;
  510. if (tool.enabled) {
  511. tool.init(pdfjsLib);
  512. } else {
  513. panel.textContent = `${tool.name} is disabled. To enable add "${tool.id}" to ` + "the pdfBug parameter and refresh (separate multiple by commas).";
  514. }
  515. buttons.push(panelButton);
  516. }
  517. this.selectPanel(0);
  518. },
  519. loadCSS() {
  520. const {
  521. url
  522. } = import.meta;
  523. const link = document.createElement("link");
  524. link.rel = "stylesheet";
  525. link.href = url.replace(/.js$/, ".css");
  526. document.head.append(link);
  527. },
  528. cleanup() {
  529. for (const tool of this.tools) {
  530. if (tool.enabled) {
  531. tool.cleanup();
  532. }
  533. }
  534. },
  535. selectPanel(index) {
  536. if (typeof index !== "number") {
  537. index = this.tools.indexOf(index);
  538. }
  539. if (index === activePanel) {
  540. return;
  541. }
  542. activePanel = index;
  543. for (const [j, tool] of this.tools.entries()) {
  544. const isActive = j === index;
  545. buttons[j].classList.toggle("active", isActive);
  546. tool.active = isActive;
  547. tool.panel.hidden = !isActive;
  548. }
  549. }
  550. };
  551. }();
  552. exports.PDFBug = PDFBug;
  553. globalThis.FontInspector = FontInspector;
  554. globalThis.StepperManager = StepperManager;
  555. globalThis.Stats = Stats;