michael@0: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ michael@0: /* Copyright 2012 Mozilla Foundation michael@0: * michael@0: * Licensed under the Apache License, Version 2.0 (the "License"); michael@0: * you may not use this file except in compliance with the License. michael@0: * You may obtain a copy of the License at michael@0: * michael@0: * http://www.apache.org/licenses/LICENSE-2.0 michael@0: * michael@0: * Unless required by applicable law or agreed to in writing, software michael@0: * distributed under the License is distributed on an "AS IS" BASIS, michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. michael@0: * See the License for the specific language governing permissions and michael@0: * limitations under the License. michael@0: */ michael@0: /* globals PDFJS */ michael@0: michael@0: 'use strict'; michael@0: michael@0: var FontInspector = (function FontInspectorClosure() { michael@0: var fonts; michael@0: var active = false; michael@0: var fontAttribute = 'data-font-name'; michael@0: function removeSelection() { michael@0: var divs = document.querySelectorAll('div[' + fontAttribute + ']'); michael@0: for (var i = 0, ii = divs.length; i < ii; ++i) { michael@0: var div = divs[i]; michael@0: div.className = ''; michael@0: } michael@0: } michael@0: function resetSelection() { michael@0: var divs = document.querySelectorAll('div[' + fontAttribute + ']'); michael@0: for (var i = 0, ii = divs.length; i < ii; ++i) { michael@0: var div = divs[i]; michael@0: div.className = 'debuggerHideText'; michael@0: } michael@0: } michael@0: function selectFont(fontName, show) { michael@0: var divs = document.querySelectorAll('div[' + fontAttribute + '=' + michael@0: fontName + ']'); michael@0: for (var i = 0, ii = divs.length; i < ii; ++i) { michael@0: var div = divs[i]; michael@0: div.className = show ? 'debuggerShowText' : 'debuggerHideText'; michael@0: } michael@0: } michael@0: function textLayerClick(e) { michael@0: if (!e.target.dataset.fontName || michael@0: e.target.tagName.toUpperCase() !== 'DIV') { michael@0: return; michael@0: } michael@0: var fontName = e.target.dataset.fontName; michael@0: var selects = document.getElementsByTagName('input'); michael@0: for (var i = 0; i < selects.length; ++i) { michael@0: var select = selects[i]; michael@0: if (select.dataset.fontName != fontName) { michael@0: continue; michael@0: } michael@0: select.checked = !select.checked; michael@0: selectFont(fontName, select.checked); michael@0: select.scrollIntoView(); michael@0: } michael@0: } michael@0: return { michael@0: // Properties/functions needed by PDFBug. michael@0: id: 'FontInspector', michael@0: name: 'Font Inspector', michael@0: panel: null, michael@0: manager: null, michael@0: init: function init() { michael@0: var panel = this.panel; michael@0: panel.setAttribute('style', 'padding: 5px;'); michael@0: var tmp = document.createElement('button'); michael@0: tmp.addEventListener('click', resetSelection); michael@0: tmp.textContent = 'Refresh'; michael@0: panel.appendChild(tmp); michael@0: michael@0: fonts = document.createElement('div'); michael@0: panel.appendChild(fonts); michael@0: }, michael@0: cleanup: function cleanup() { michael@0: fonts.textContent = ''; michael@0: }, michael@0: enabled: false, michael@0: get active() { michael@0: return active; michael@0: }, michael@0: set active(value) { michael@0: active = value; michael@0: if (active) { michael@0: document.body.addEventListener('click', textLayerClick, true); michael@0: resetSelection(); michael@0: } else { michael@0: document.body.removeEventListener('click', textLayerClick, true); michael@0: removeSelection(); michael@0: } michael@0: }, michael@0: // FontInspector specific functions. michael@0: fontAdded: function fontAdded(fontObj, url) { michael@0: function properties(obj, list) { michael@0: var moreInfo = document.createElement('table'); michael@0: for (var i = 0; i < list.length; i++) { michael@0: var tr = document.createElement('tr'); michael@0: var td1 = document.createElement('td'); michael@0: td1.textContent = list[i]; michael@0: tr.appendChild(td1); michael@0: var td2 = document.createElement('td'); michael@0: td2.textContent = obj[list[i]].toString(); michael@0: tr.appendChild(td2); michael@0: moreInfo.appendChild(tr); michael@0: } michael@0: return moreInfo; michael@0: } michael@0: var moreInfo = properties(fontObj, ['name', 'type']); michael@0: var m = /url\(['"]?([^\)"']+)/.exec(url); michael@0: var fontName = fontObj.loadedName; michael@0: var font = document.createElement('div'); michael@0: var name = document.createElement('span'); michael@0: name.textContent = fontName; michael@0: var download = document.createElement('a'); michael@0: download.href = m[1]; michael@0: download.textContent = 'Download'; michael@0: var logIt = document.createElement('a'); michael@0: logIt.href = ''; michael@0: logIt.textContent = 'Log'; michael@0: logIt.addEventListener('click', function(event) { michael@0: event.preventDefault(); michael@0: console.log(fontObj); michael@0: }); michael@0: var select = document.createElement('input'); michael@0: select.setAttribute('type', 'checkbox'); michael@0: select.dataset.fontName = fontName; michael@0: select.addEventListener('click', (function(select, fontName) { michael@0: return (function() { michael@0: selectFont(fontName, select.checked); michael@0: }); michael@0: })(select, fontName)); michael@0: font.appendChild(select); michael@0: font.appendChild(name); michael@0: font.appendChild(document.createTextNode(' ')); michael@0: font.appendChild(download); michael@0: font.appendChild(document.createTextNode(' ')); michael@0: font.appendChild(logIt); michael@0: font.appendChild(moreInfo); michael@0: fonts.appendChild(font); michael@0: // Somewhat of a hack, should probably add a hook for when the text layer michael@0: // is done rendering. michael@0: setTimeout(function() { michael@0: if (this.active) { michael@0: resetSelection(); michael@0: } michael@0: }.bind(this), 2000); michael@0: } michael@0: }; michael@0: })(); michael@0: michael@0: // Manages all the page steppers. michael@0: var StepperManager = (function StepperManagerClosure() { michael@0: var steppers = []; michael@0: var stepperDiv = null; michael@0: var stepperControls = null; michael@0: var stepperChooser = null; michael@0: var breakPoints = {}; michael@0: return { michael@0: // Properties/functions needed by PDFBug. michael@0: id: 'Stepper', michael@0: name: 'Stepper', michael@0: panel: null, michael@0: manager: null, michael@0: init: function init() { michael@0: var self = this; michael@0: this.panel.setAttribute('style', 'padding: 5px;'); michael@0: stepperControls = document.createElement('div'); michael@0: stepperChooser = document.createElement('select'); michael@0: stepperChooser.addEventListener('change', function(event) { michael@0: self.selectStepper(this.value); michael@0: }); michael@0: stepperControls.appendChild(stepperChooser); michael@0: stepperDiv = document.createElement('div'); michael@0: this.panel.appendChild(stepperControls); michael@0: this.panel.appendChild(stepperDiv); michael@0: if (sessionStorage.getItem('pdfjsBreakPoints')) { michael@0: breakPoints = JSON.parse(sessionStorage.getItem('pdfjsBreakPoints')); michael@0: } michael@0: }, michael@0: cleanup: function cleanup() { michael@0: stepperChooser.textContent = ''; michael@0: stepperDiv.textContent = ''; michael@0: steppers = []; michael@0: }, michael@0: enabled: false, michael@0: active: false, michael@0: // Stepper specific functions. michael@0: create: function create(pageIndex) { michael@0: var debug = document.createElement('div'); michael@0: debug.id = 'stepper' + pageIndex; michael@0: debug.setAttribute('hidden', true); michael@0: debug.className = 'stepper'; michael@0: stepperDiv.appendChild(debug); michael@0: var b = document.createElement('option'); michael@0: b.textContent = 'Page ' + (pageIndex + 1); michael@0: b.value = pageIndex; michael@0: stepperChooser.appendChild(b); michael@0: var initBreakPoints = breakPoints[pageIndex] || []; michael@0: var stepper = new Stepper(debug, pageIndex, initBreakPoints); michael@0: steppers.push(stepper); michael@0: if (steppers.length === 1) { michael@0: this.selectStepper(pageIndex, false); michael@0: } michael@0: return stepper; michael@0: }, michael@0: selectStepper: function selectStepper(pageIndex, selectPanel) { michael@0: var i; michael@0: if (selectPanel) { michael@0: this.manager.selectPanel(this); michael@0: } michael@0: for (i = 0; i < steppers.length; ++i) { michael@0: var stepper = steppers[i]; michael@0: if (stepper.pageIndex == pageIndex) { michael@0: stepper.panel.removeAttribute('hidden'); michael@0: } else { michael@0: stepper.panel.setAttribute('hidden', true); michael@0: } michael@0: } michael@0: var options = stepperChooser.options; michael@0: for (i = 0; i < options.length; ++i) { michael@0: var option = options[i]; michael@0: option.selected = option.value == pageIndex; michael@0: } michael@0: }, michael@0: saveBreakPoints: function saveBreakPoints(pageIndex, bps) { michael@0: breakPoints[pageIndex] = bps; michael@0: sessionStorage.setItem('pdfjsBreakPoints', JSON.stringify(breakPoints)); michael@0: } michael@0: }; michael@0: })(); michael@0: michael@0: // The stepper for each page's IRQueue. michael@0: var Stepper = (function StepperClosure() { michael@0: // Shorter way to create element and optionally set textContent. michael@0: function c(tag, textContent) { michael@0: var d = document.createElement(tag); michael@0: if (textContent) { michael@0: d.textContent = textContent; michael@0: } michael@0: return d; michael@0: } michael@0: michael@0: function glyphsToString(glyphs) { michael@0: var out = ''; michael@0: for (var i = 0; i < glyphs.length; i++) { michael@0: if (glyphs[i] === null) { michael@0: out += ' '; michael@0: } else { michael@0: out += glyphs[i].fontChar; michael@0: } michael@0: } michael@0: return out; michael@0: } michael@0: michael@0: var opMap = null; michael@0: michael@0: var glyphCommands = { michael@0: 'showText': 0, michael@0: 'showSpacedText': 0, michael@0: 'nextLineShowText': 0, michael@0: 'nextLineSetSpacingShowText': 2 michael@0: }; michael@0: michael@0: function simplifyArgs(args) { michael@0: if (typeof args === 'string') { michael@0: var MAX_STRING_LENGTH = 75; michael@0: return args.length <= MAX_STRING_LENGTH ? args : michael@0: args.substr(0, MAX_STRING_LENGTH) + '...'; michael@0: } michael@0: if (typeof args !== 'object' || args === null) { michael@0: return args; michael@0: } michael@0: if ('length' in args) { // array michael@0: var simpleArgs = [], i, ii; michael@0: var MAX_ITEMS = 10; michael@0: for (i = 0, ii = Math.min(MAX_ITEMS, args.length); i < ii; i++) { michael@0: simpleArgs.push(simplifyArgs(args[i])); michael@0: } michael@0: if (i < args.length) { michael@0: simpleArgs.push('...'); michael@0: } michael@0: return simpleArgs; michael@0: } michael@0: var simpleObj = {}; michael@0: for (var key in args) { michael@0: simpleObj[key] = simplifyArgs(args[key]); michael@0: } michael@0: return simpleObj; michael@0: } michael@0: michael@0: function Stepper(panel, pageIndex, initialBreakPoints) { michael@0: this.panel = panel; michael@0: this.breakPoint = 0; michael@0: this.nextBreakPoint = null; michael@0: this.pageIndex = pageIndex; michael@0: this.breakPoints = initialBreakPoints; michael@0: this.currentIdx = -1; michael@0: this.operatorListIdx = 0; michael@0: } michael@0: Stepper.prototype = { michael@0: init: function init() { michael@0: var panel = this.panel; michael@0: var content = c('div', 'c=continue, s=step'); michael@0: var table = c('table'); michael@0: content.appendChild(table); michael@0: table.cellSpacing = 0; michael@0: var headerRow = c('tr'); michael@0: table.appendChild(headerRow); michael@0: headerRow.appendChild(c('th', 'Break')); michael@0: headerRow.appendChild(c('th', 'Idx')); michael@0: headerRow.appendChild(c('th', 'fn')); michael@0: headerRow.appendChild(c('th', 'args')); michael@0: panel.appendChild(content); michael@0: this.table = table; michael@0: if (!opMap) { michael@0: opMap = Object.create(null); michael@0: for (var key in PDFJS.OPS) { michael@0: opMap[PDFJS.OPS[key]] = key; michael@0: } michael@0: } michael@0: }, michael@0: updateOperatorList: function updateOperatorList(operatorList) { michael@0: var self = this; michael@0: michael@0: function cboxOnClick() { michael@0: var x = +this.dataset.idx; michael@0: if (this.checked) { michael@0: self.breakPoints.push(x); michael@0: } else { michael@0: self.breakPoints.splice(self.breakPoints.indexOf(x), 1); michael@0: } michael@0: StepperManager.saveBreakPoints(self.pageIndex, self.breakPoints); michael@0: } michael@0: michael@0: var MAX_OPERATORS_COUNT = 15000; michael@0: if (this.operatorListIdx > MAX_OPERATORS_COUNT) { michael@0: return; michael@0: } michael@0: michael@0: var chunk = document.createDocumentFragment(); michael@0: var operatorsToDisplay = Math.min(MAX_OPERATORS_COUNT, michael@0: operatorList.fnArray.length); michael@0: for (var i = this.operatorListIdx; i < operatorsToDisplay; i++) { michael@0: var line = c('tr'); michael@0: line.className = 'line'; michael@0: line.dataset.idx = i; michael@0: chunk.appendChild(line); michael@0: var checked = this.breakPoints.indexOf(i) != -1; michael@0: var args = operatorList.argsArray[i] || []; michael@0: michael@0: var breakCell = c('td'); michael@0: var cbox = c('input'); michael@0: cbox.type = 'checkbox'; michael@0: cbox.className = 'points'; michael@0: cbox.checked = checked; michael@0: cbox.dataset.idx = i; michael@0: cbox.onclick = cboxOnClick; michael@0: michael@0: breakCell.appendChild(cbox); michael@0: line.appendChild(breakCell); michael@0: line.appendChild(c('td', i.toString())); michael@0: var fn = opMap[operatorList.fnArray[i]]; michael@0: var decArgs = args; michael@0: if (fn in glyphCommands) { michael@0: var glyphIndex = glyphCommands[fn]; michael@0: var glyphs = args[glyphIndex]; michael@0: decArgs = args.slice(); michael@0: var newArg; michael@0: if (fn === 'showSpacedText') { michael@0: newArg = []; michael@0: for (var j = 0; j < glyphs.length; j++) { michael@0: if (typeof glyphs[j] === 'number') { michael@0: newArg.push(glyphs[j]); michael@0: } else { michael@0: newArg.push(glyphsToString(glyphs[j])); michael@0: } michael@0: } michael@0: } else { michael@0: newArg = glyphsToString(glyphs); michael@0: } michael@0: decArgs[glyphIndex] = newArg; michael@0: } michael@0: line.appendChild(c('td', fn)); michael@0: line.appendChild(c('td', JSON.stringify(simplifyArgs(decArgs)))); michael@0: } michael@0: if (operatorsToDisplay < operatorList.fnArray.length) { michael@0: line = c('tr'); michael@0: var lastCell = c('td', '...'); michael@0: lastCell.colspan = 4; michael@0: chunk.appendChild(lastCell); michael@0: } michael@0: this.operatorListIdx = operatorList.fnArray.length; michael@0: this.table.appendChild(chunk); michael@0: }, michael@0: getNextBreakPoint: function getNextBreakPoint() { michael@0: this.breakPoints.sort(function(a, b) { return a - b; }); michael@0: for (var i = 0; i < this.breakPoints.length; i++) { michael@0: if (this.breakPoints[i] > this.currentIdx) { michael@0: return this.breakPoints[i]; michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: breakIt: function breakIt(idx, callback) { michael@0: StepperManager.selectStepper(this.pageIndex, true); michael@0: var self = this; michael@0: var dom = document; michael@0: self.currentIdx = idx; michael@0: var listener = function(e) { michael@0: switch (e.keyCode) { michael@0: case 83: // step michael@0: dom.removeEventListener('keydown', listener, false); michael@0: self.nextBreakPoint = self.currentIdx + 1; michael@0: self.goTo(-1); michael@0: callback(); michael@0: break; michael@0: case 67: // continue michael@0: dom.removeEventListener('keydown', listener, false); michael@0: var breakPoint = self.getNextBreakPoint(); michael@0: self.nextBreakPoint = breakPoint; michael@0: self.goTo(-1); michael@0: callback(); michael@0: break; michael@0: } michael@0: }; michael@0: dom.addEventListener('keydown', listener, false); michael@0: self.goTo(idx); michael@0: }, michael@0: goTo: function goTo(idx) { michael@0: var allRows = this.panel.getElementsByClassName('line'); michael@0: for (var x = 0, xx = allRows.length; x < xx; ++x) { michael@0: var row = allRows[x]; michael@0: if (row.dataset.idx == idx) { michael@0: row.style.backgroundColor = 'rgb(251,250,207)'; michael@0: row.scrollIntoView(); michael@0: } else { michael@0: row.style.backgroundColor = null; michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: return Stepper; michael@0: })(); michael@0: michael@0: var Stats = (function Stats() { michael@0: var stats = []; michael@0: function clear(node) { michael@0: while (node.hasChildNodes()) { michael@0: node.removeChild(node.lastChild); michael@0: } michael@0: } michael@0: function getStatIndex(pageNumber) { michael@0: for (var i = 0, ii = stats.length; i < ii; ++i) { michael@0: if (stats[i].pageNumber === pageNumber) { michael@0: return i; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: return { michael@0: // Properties/functions needed by PDFBug. michael@0: id: 'Stats', michael@0: name: 'Stats', michael@0: panel: null, michael@0: manager: null, michael@0: init: function init() { michael@0: this.panel.setAttribute('style', 'padding: 5px;'); michael@0: PDFJS.enableStats = true; michael@0: }, michael@0: enabled: false, michael@0: active: false, michael@0: // Stats specific functions. michael@0: add: function(pageNumber, stat) { michael@0: if (!stat) { michael@0: return; michael@0: } michael@0: var statsIndex = getStatIndex(pageNumber); michael@0: if (statsIndex !== false) { michael@0: var b = stats[statsIndex]; michael@0: this.panel.removeChild(b.div); michael@0: stats.splice(statsIndex, 1); michael@0: } michael@0: var wrapper = document.createElement('div'); michael@0: wrapper.className = 'stats'; michael@0: var title = document.createElement('div'); michael@0: title.className = 'title'; michael@0: title.textContent = 'Page: ' + pageNumber; michael@0: var statsDiv = document.createElement('div'); michael@0: statsDiv.textContent = stat.toString(); michael@0: wrapper.appendChild(title); michael@0: wrapper.appendChild(statsDiv); michael@0: stats.push({ pageNumber: pageNumber, div: wrapper }); michael@0: stats.sort(function(a, b) { return a.pageNumber - b.pageNumber; }); michael@0: clear(this.panel); michael@0: for (var i = 0, ii = stats.length; i < ii; ++i) { michael@0: this.panel.appendChild(stats[i].div); michael@0: } michael@0: }, michael@0: cleanup: function () { michael@0: stats = []; michael@0: clear(this.panel); michael@0: } michael@0: }; michael@0: })(); michael@0: michael@0: // Manages all the debugging tools. michael@0: var PDFBug = (function PDFBugClosure() { michael@0: var panelWidth = 300; michael@0: var buttons = []; michael@0: var activePanel = null; michael@0: michael@0: return { michael@0: tools: [ michael@0: FontInspector, michael@0: StepperManager, michael@0: Stats michael@0: ], michael@0: enable: function(ids) { michael@0: var all = false, tools = this.tools; michael@0: if (ids.length === 1 && ids[0] === 'all') { michael@0: all = true; michael@0: } michael@0: for (var i = 0; i < tools.length; ++i) { michael@0: var tool = tools[i]; michael@0: if (all || ids.indexOf(tool.id) !== -1) { michael@0: tool.enabled = true; michael@0: } michael@0: } michael@0: if (!all) { michael@0: // Sort the tools by the order they are enabled. michael@0: tools.sort(function(a, b) { michael@0: var indexA = ids.indexOf(a.id); michael@0: indexA = indexA < 0 ? tools.length : indexA; michael@0: var indexB = ids.indexOf(b.id); michael@0: indexB = indexB < 0 ? tools.length : indexB; michael@0: return indexA - indexB; michael@0: }); michael@0: } michael@0: }, michael@0: init: function init() { michael@0: /* michael@0: * Basic Layout: michael@0: * PDFBug michael@0: * Controls michael@0: * Panels michael@0: * Panel michael@0: * Panel michael@0: * ... michael@0: */ michael@0: var ui = document.createElement('div'); michael@0: ui.id = 'PDFBug'; michael@0: michael@0: var controls = document.createElement('div'); michael@0: controls.setAttribute('class', 'controls'); michael@0: ui.appendChild(controls); michael@0: michael@0: var panels = document.createElement('div'); michael@0: panels.setAttribute('class', 'panels'); michael@0: ui.appendChild(panels); michael@0: michael@0: var container = document.getElementById('viewerContainer'); michael@0: container.appendChild(ui); michael@0: container.style.right = panelWidth + 'px'; michael@0: michael@0: // Initialize all the debugging tools. michael@0: var tools = this.tools; michael@0: var self = this; michael@0: for (var i = 0; i < tools.length; ++i) { michael@0: var tool = tools[i]; michael@0: var panel = document.createElement('div'); michael@0: var panelButton = document.createElement('button'); michael@0: panelButton.textContent = tool.name; michael@0: panelButton.addEventListener('click', (function(selected) { michael@0: return function(event) { michael@0: event.preventDefault(); michael@0: self.selectPanel(selected); michael@0: }; michael@0: })(i)); michael@0: controls.appendChild(panelButton); michael@0: panels.appendChild(panel); michael@0: tool.panel = panel; michael@0: tool.manager = this; michael@0: if (tool.enabled) { michael@0: tool.init(); michael@0: } else { michael@0: panel.textContent = tool.name + ' is disabled. To enable add ' + michael@0: ' "' + tool.id + '" to the pdfBug parameter ' + michael@0: 'and refresh (seperate multiple by commas).'; michael@0: } michael@0: buttons.push(panelButton); michael@0: } michael@0: this.selectPanel(0); michael@0: }, michael@0: cleanup: function cleanup() { michael@0: for (var i = 0, ii = this.tools.length; i < ii; i++) { michael@0: if (this.tools[i].enabled) { michael@0: this.tools[i].cleanup(); michael@0: } michael@0: } michael@0: }, michael@0: selectPanel: function selectPanel(index) { michael@0: if (typeof index !== 'number') { michael@0: index = this.tools.indexOf(index); michael@0: } michael@0: if (index === activePanel) { michael@0: return; michael@0: } michael@0: activePanel = index; michael@0: var tools = this.tools; michael@0: for (var j = 0; j < tools.length; ++j) { michael@0: if (j == index) { michael@0: buttons[j].setAttribute('class', 'active'); michael@0: tools[j].active = true; michael@0: tools[j].panel.removeAttribute('hidden'); michael@0: } else { michael@0: buttons[j].setAttribute('class', ''); michael@0: tools[j].active = false; michael@0: tools[j].panel.setAttribute('hidden', 'true'); michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: })();