debugger.js 18 KB

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