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