diff --git a/canvas_proxy.js b/canvas_proxy.js new file mode 100644 index 000000000..d6f5a0a25 --- /dev/null +++ b/canvas_proxy.js @@ -0,0 +1,250 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +var JpegStreamProxyCounter = 0; +// WebWorker Proxy for JpegStream. +var JpegStreamProxy = (function() { + function constructor(bytes, dict) { + this.id = JpegStreamProxyCounter++; + this.dict = dict; + + // Tell the main thread to create an image. + postMessage({ + action: "jpeg_stream", + data: { + id: this.id, + raw: bytesToString(bytes) + } + }); + } + + constructor.prototype = { + getImage: function() { + return this; + }, + getChar: function() { + error("internal error: getChar is not valid on JpegStream"); + } + }; + + return constructor; +})(); + +// Really simple GradientProxy. There is currently only one active gradient at +// the time, meaning you can't create a gradient, create a second one and then +// use the first one again. As this isn't used in pdf.js right now, it's okay. +function GradientProxy(cmdQueue, x0, y0, x1, y1) { + cmdQueue.push(["$createLinearGradient", [x0, y0, x1, y1]]); + this.addColorStop = function(i, rgba) { + cmdQueue.push(["$addColorStop", [i, rgba]]); + } +} + +// Really simple PatternProxy. +var patternProxyCounter = 0; +function PatternProxy(cmdQueue, object, kind) { + this.id = patternProxyCounter++; + + if (!(object instanceof CanvasProxy) ) { + throw "unkown type to createPattern"; + } + + // Flush the object here to ensure it's available on the main thread. + // TODO: Make some kind of dependency management, such that the object + // gets flushed only if needed. + object.flush(); + cmdQueue.push(["$createPatternFromCanvas", [this.id, object.id, kind]]); +} + +var canvasProxyCounter = 0; +function CanvasProxy(width, height) { + this.id = canvasProxyCounter++; + + // The `stack` holds the rendering calls and gets flushed to the main thead. + var cmdQueue = this.cmdQueue = []; + + // Dummy context that gets exposed. + var ctx = {}; + this.getContext = function(type) { + if (type != "2d") { + throw "CanvasProxy can only provide a 2d context."; + } + return ctx; + } + + // Expose only the minimum of the canvas object - there is no dom to do + // more here. + this.width = width; + this.height = height; + ctx.canvas = this; + + // Setup function calls to `ctx`. + var ctxFunc = [ + "createRadialGradient", + "arcTo", + "arc", + "fillText", + "strokeText", + "createImageData", + "drawWindow", + "save", + "restore", + "scale", + "rotate", + "translate", + "transform", + "setTransform", + "clearRect", + "fillRect", + "strokeRect", + "beginPath", + "closePath", + "moveTo", + "lineTo", + "quadraticCurveTo", + "bezierCurveTo", + "rect", + "fill", + "stroke", + "clip", + "measureText", + "isPointInPath", + + // These functions are necessary to track the rendering currentX state. + // The exact values can be computed on the main thread only, as the + // worker has no idea about text width. + "$setCurrentX", + "$addCurrentX", + "$saveCurrentX", + "$restoreCurrentX", + "$showText" + ]; + + function buildFuncCall(name) { + return function() { + // console.log("funcCall", name) + cmdQueue.push([name, Array.prototype.slice.call(arguments)]); + } + } + var name; + for (var i = 0; i < ctxFunc.length; i++) { + name = ctxFunc[i]; + ctx[name] = buildFuncCall(name); + } + + // Some function calls that need more work. + + ctx.createPattern = function(object, kind) { + return new PatternProxy(cmdQueue, object, kind); + } + + ctx.createLinearGradient = function(x0, y0, x1, y1) { + return new GradientProxy(cmdQueue, x0, y0, x1, y1); + } + + ctx.getImageData = function(x, y, w, h) { + return { + width: w, + height: h, + data: Uint8ClampedArray(w * h * 4) + }; + } + + ctx.putImageData = function(data, x, y, width, height) { + cmdQueue.push(["$putImageData", [data, x, y, width, height]]); + } + + ctx.drawImage = function(image, x, y, width, height, sx, sy, swidth, sheight) { + if (image instanceof CanvasProxy) { + // Send the image/CanvasProxy to the main thread. + image.flush(); + cmdQueue.push(["$drawCanvas", [image.id, x, y, sx, sy, swidth, sheight]]); + } else if(image instanceof JpegStreamProxy) { + cmdQueue.push(["$drawImage", [image.id, x, y, sx, sy, swidth, sheight]]) + } else { + throw "unkown type to drawImage"; + } + } + + // Setup property access to `ctx`. + var ctxProp = { + // "canvas" + "globalAlpha": "1", + "globalCompositeOperation": "source-over", + "strokeStyle": "#000000", + "fillStyle": "#000000", + "lineWidth": "1", + "lineCap": "butt", + "lineJoin": "miter", + "miterLimit": "10", + "shadowOffsetX": "0", + "shadowOffsetY": "0", + "shadowBlur": "0", + "shadowColor": "rgba(0, 0, 0, 0)", + "font": "10px sans-serif", + "textAlign": "start", + "textBaseline": "alphabetic", + "mozTextStyle": "10px sans-serif", + "mozImageSmoothingEnabled": "true" + } + + function buildGetter(name) { + return function() { + return ctx["$" + name]; + } + } + + function buildSetter(name) { + return function(value) { + cmdQueue.push(["$", name, value]); + return ctx["$" + name] = value; + } + } + + // Setting the value to `stroke|fillStyle` needs special handling, as it + // might gets an gradient/pattern. + function buildSetterStyle(name) { + return function(value) { + if (value instanceof GradientProxy) { + cmdQueue.push(["$" + name + "Gradient"]); + } else if (value instanceof PatternProxy) { + cmdQueue.push(["$" + name + "Pattern", [value.id]]); + } else { + cmdQueue.push(["$", name, value]); + return ctx["$" + name] = value; + } + } + } + + for (var name in ctxProp) { + ctx["$" + name] = ctxProp[name]; + ctx.__defineGetter__(name, buildGetter(name)); + + // Special treatment for `fillStyle` and `strokeStyle`: The passed style + // might be a gradient. Need to check for that. + if (name == "fillStyle" || name == "strokeStyle") { + ctx.__defineSetter__(name, buildSetterStyle(name)); + } else { + ctx.__defineSetter__(name, buildSetter(name)); + } + } +} + +/** +* Sends the current cmdQueue of the CanvasProxy over to the main thread and +* resets the cmdQueue. +*/ +CanvasProxy.prototype.flush = function() { + postMessage({ + action: "canvas_proxy_cmd_queue", + data: { + id: this.id, + cmdQueue: this.cmdQueue, + width: this.width, + height: this.height + } + }); + this.cmdQueue.length = 0; +} diff --git a/fonts.js b/fonts.js index d5943b7a3..f00f5c75f 100644 --- a/fonts.js +++ b/fonts.js @@ -135,15 +135,28 @@ var Font = (function () { break; } + var data = this.font; Fonts[name] = { - data: this.font, + data: data, properties: properties, loading: true, cache: Object.create(null) } - // Attach the font to the document - this.bind(); + // Convert data to a string. + var dataStr = ""; + var length = data.length; + for (var i = 0; i < length; ++i) + dataStr += String.fromCharCode(data[i]); + + // Attach the font to the document. If this script is runnig in a worker, + // call `bindWorker`, which sends stuff over to the main thread. + if (typeof window != "undefined") { + this.bindDOM(dataStr); + } else { + this.bindWorker(dataStr); + } + }; function stringToArray(str) { @@ -755,12 +768,21 @@ var Font = (function () { return fontData; }, - bind: function font_bind() { - var data = this.font; + bindWorker: function font_bind_worker(dataStr) { + postMessage({ + action: "font", + data: { + raw: dataStr, + fontName: this.name, + mimetype: this.mimetype + } + }); + }, + + bindDOM: function font_bind_dom(dataStr) { var fontName = this.name; /** Hack begin */ - // Actually there is not event when a font has finished downloading so // the following code are a dirty hack to 'guess' when a font is ready var canvas = document.createElement("canvas"); @@ -774,7 +796,7 @@ var Font = (function () { // Get the font size canvas think it will be for 'spaces' var ctx = canvas.getContext("2d"); ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; - var testString = " "; + var testString = " "; // When debugging use the characters provided by the charsets to visually // see what's happening instead of 'spaces' @@ -830,14 +852,9 @@ var Font = (function () { }, 30, this); /** Hack end */ - - // Get the base64 encoding of the binary font data - var str = ""; - var length = data.length; - for (var i = 0; i < length; ++i) - str += String.fromCharCode(data[i]); - - var base64 = window.btoa(str); + + // Convert the data string and add it to the page. + var base64 = window.btoa(dataStr); // Add the @font-face rule to the document var url = "url(data:" + this.mimetype + ";base64," + base64 + ");"; diff --git a/multi-page-viewer.css b/multi-page-viewer.css deleted file mode 100644 index 7f4701022..000000000 --- a/multi-page-viewer.css +++ /dev/null @@ -1,197 +0,0 @@ -/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- / -/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */ - -body { - background-color: #929292; - font-family: 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif; - margin: 0px; - padding: 0px; -} - -canvas { - box-shadow: 0px 4px 10px #000; - -moz-box-shadow: 0px 4px 10px #000; - -webkit-box-shadow: 0px 4px 10px #000; -} - -span { - font-size: 0.8em; -} - -.control { - display: inline-block; - float: left; - margin: 0px 20px 0px 0px; - padding: 0px 4px 0px 0px; -} - -.control > input { - float: left; - border: 1px solid #4d4d4d; - height: 20px; - padding: 0px; - margin: 0px 2px 0px 0px; - border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); - -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); - -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); -} - -.control > select { - float: left; - border: 1px solid #4d4d4d; - height: 22px; - padding: 2px 0px 0px; - margin: 0px 0px 1px; - border-radius: 4px; - -moz-border-radius: 4px; - -webkit-border-radius: 4px; - box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); - -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); - -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); -} - -.control > span { - cursor: default; - float: left; - height: 18px; - margin: 5px 2px 0px; - padding: 0px; - user-select: none; - -moz-user-select: none; - -webkit-user-select: none; -} - -.control .label { - clear: both; - float: left; - font-size: 0.65em; - margin: 2px 0px 0px; - position: relative; - text-align: center; - width: 100%; -} - -.page { - width: 816px; - height: 1056px; - margin: 10px auto; -} - -#controls { - background-color: #eee; - border-bottom: 1px solid #666; - padding: 4px 0px 0px 8px; - position: fixed; - left: 0px; - top: 0px; - height: 40px; - width: 100%; - box-shadow: 0px 2px 8px #000; - -moz-box-shadow: 0px 2px 8px #000; - -webkit-box-shadow: 0px 2px 8px #000; -} - -#controls input { - user-select: text; - -moz-user-select: text; - -webkit-user-select: text; -} - -#previousPageButton { - background: url('images/buttons.png') no-repeat 0px -23px; - cursor: default; - display: inline-block; - float: left; - margin: 0px; - width: 28px; - height: 23px; -} - -#previousPageButton.down { - background: url('images/buttons.png') no-repeat 0px -46px; -} - -#previousPageButton.disabled { - background: url('images/buttons.png') no-repeat 0px 0px; -} - -#nextPageButton { - background: url('images/buttons.png') no-repeat -28px -23px; - cursor: default; - display: inline-block; - float: left; - margin: 0px; - width: 28px; - height: 23px; -} - -#nextPageButton.down { - background: url('images/buttons.png') no-repeat -28px -46px; -} - -#nextPageButton.disabled { - background: url('images/buttons.png') no-repeat -28px 0px; -} - -#openFileButton { - background: url('images/buttons.png') no-repeat -56px -23px; - cursor: default; - display: inline-block; - float: left; - margin: 0px 0px 0px 3px; - width: 29px; - height: 23px; -} - -#openFileButton.down { - background: url('images/buttons.png') no-repeat -56px -46px; -} - -#openFileButton.disabled { - background: url('images/buttons.png') no-repeat -56px 0px; -} - -#fileInput { - display: none; -} - -#pageNumber { - text-align: right; -} - -#sidebar { - background-color: rgba(0, 0, 0, 0.8); - position: fixed; - width: 150px; - top: 62px; - bottom: 18px; - border-top-right-radius: 8px; - border-bottom-right-radius: 8px; - -moz-border-radius-topright: 8px; - -moz-border-radius-bottomright: 8px; - -webkit-border-top-right-radius: 8px; - -webkit-border-bottom-right-radius: 8px; -} - -#sidebarScrollView { - position: absolute; - overflow: hidden; - overflow-y: auto; - top: 40px; - right: 10px; - bottom: 10px; - left: 10px; -} - -#sidebarContentView { - height: auto; - width: 100px; -} - -#viewer { - margin: 44px 0px 0px; - padding: 8px 0px; -} diff --git a/multi-page-viewer.html b/multi-page-viewer.html deleted file mode 100644 index ffbdfe707..000000000 --- a/multi-page-viewer.html +++ /dev/null @@ -1,51 +0,0 @@ - - - -pdf.js Multi-Page Viewer - - - - - - - - -
- - - - Previous/Next - - - - / - -- - Page Number - - - - Zoom - - - - - Open File - -
- -
- - diff --git a/multi-page-viewer.js b/multi-page-viewer.js deleted file mode 100644 index baad7809e..000000000 --- a/multi-page-viewer.js +++ /dev/null @@ -1,466 +0,0 @@ -/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- / -/* vim: set shiftwidth=4 tabstop=8 autoindent cindent expandtab: */ - -"use strict"; - -var PDFViewer = { - queryParams: {}, - - element: null, - - previousPageButton: null, - nextPageButton: null, - pageNumberInput: null, - scaleSelect: null, - fileInput: null, - - willJumpToPage: false, - - pdf: null, - - url: 'compressed.tracemonkey-pldi-09.pdf', - pageNumber: 1, - numberOfPages: 1, - - scale: 1.0, - - pageWidth: function() { - return 816 * PDFViewer.scale; - }, - - pageHeight: function() { - return 1056 * PDFViewer.scale; - }, - - lastPagesDrawn: [], - - visiblePages: function() { - var pageHeight = PDFViewer.pageHeight() + 20; // Add 20 for the margins. - var windowTop = window.pageYOffset; - var windowBottom = window.pageYOffset + window.innerHeight; - var pageStartIndex = Math.floor(windowTop / pageHeight); - var pageStopIndex = Math.ceil(windowBottom / pageHeight); - - var pages = []; - - for (var i = pageStartIndex; i <= pageStopIndex; i++) { - pages.push(i + 1); - } - - return pages; - }, - - createPage: function(num) { - var anchor = document.createElement('a'); - anchor.name = '' + num; - - var div = document.createElement('div'); - div.id = 'pageContainer' + num; - div.className = 'page'; - div.style.width = PDFViewer.pageWidth() + 'px'; - div.style.height = PDFViewer.pageHeight() + 'px'; - - PDFViewer.element.appendChild(anchor); - PDFViewer.element.appendChild(div); - }, - - removePage: function(num) { - var div = document.getElementById('pageContainer' + num); - - if (div) { - while (div.hasChildNodes()) { - div.removeChild(div.firstChild); - } - } - }, - - drawPage: function(num) { - if (!PDFViewer.pdf) { - return; - } - - var div = document.getElementById('pageContainer' + num); - var canvas = document.createElement('canvas'); - - if (div && !div.hasChildNodes()) { - div.appendChild(canvas); - - var page = PDFViewer.pdf.getPage(num); - - canvas.id = 'page' + num; - canvas.mozOpaque = true; - - // Canvas dimensions must be specified in CSS pixels. CSS pixels - // are always 96 dpi. These dimensions are 8.5in x 11in at 96dpi. - canvas.width = PDFViewer.pageWidth(); - canvas.height = PDFViewer.pageHeight(); - - var ctx = canvas.getContext('2d'); - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.restore(); - - var gfx = new CanvasGraphics(ctx); - var fonts = []; - - // page.compile will collect all fonts for us, once we have loaded them - // we can trigger the actual page rendering with page.display - page.compile(gfx, fonts); - - var areFontsReady = true; - - // Inspect fonts and translate the missing one - var fontCount = fonts.length; - - for (var i = 0; i < fontCount; i++) { - var font = fonts[i]; - - if (Fonts[font.name]) { - areFontsReady = areFontsReady && !Fonts[font.name].loading; - continue; - } - - new Font(font.name, font.file, font.properties); - - areFontsReady = false; - } - - var pageInterval; - - var delayLoadFont = function() { - for (var i = 0; i < fontCount; i++) { - if (Fonts[font.name].loading) { - return; - } - } - - clearInterval(pageInterval); - - while (div.hasChildNodes()) { - div.removeChild(div.firstChild); - } - - PDFViewer.drawPage(num); - } - - if (!areFontsReady) { - pageInterval = setInterval(delayLoadFont, 10); - return; - } - - page.display(gfx); - } - }, - - changeScale: function(num) { - while (PDFViewer.element.hasChildNodes()) { - PDFViewer.element.removeChild(PDFViewer.element.firstChild); - } - - PDFViewer.scale = num / 100; - - var i; - - if (PDFViewer.pdf) { - for (i = 1; i <= PDFViewer.numberOfPages; i++) { - PDFViewer.createPage(i); - } - - if (PDFViewer.numberOfPages > 0) { - PDFViewer.drawPage(1); - } - } - - for (i = 0; i < PDFViewer.scaleSelect.childNodes; i++) { - var option = PDFViewer.scaleSelect.childNodes[i]; - - if (option.value == num) { - if (!option.selected) { - option.selected = 'selected'; - } - } else { - if (option.selected) { - option.removeAttribute('selected'); - } - } - } - - PDFViewer.scaleSelect.value = Math.floor(PDFViewer.scale * 100) + '%'; - }, - - goToPage: function(num) { - if (1 <= num && num <= PDFViewer.numberOfPages) { - PDFViewer.pageNumber = num; - PDFViewer.pageNumberInput.value = PDFViewer.pageNumber; - PDFViewer.willJumpToPage = true; - - document.location.hash = PDFViewer.pageNumber; - - PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? - 'disabled' : ''; - PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? - 'disabled' : ''; - } - }, - - goToPreviousPage: function() { - if (PDFViewer.pageNumber > 1) { - PDFViewer.goToPage(--PDFViewer.pageNumber); - } - }, - - goToNextPage: function() { - if (PDFViewer.pageNumber < PDFViewer.numberOfPages) { - PDFViewer.goToPage(++PDFViewer.pageNumber); - } - }, - - openURL: function(url) { - PDFViewer.url = url; - document.title = url; - - var req = new XMLHttpRequest(); - req.open('GET', url); - req.mozResponseType = req.responseType = 'arraybuffer'; - req.expected = (document.URL.indexOf('file:') === 0) ? 0 : 200; - - req.onreadystatechange = function() { - if (req.readyState === 4 && req.status === req.expected) { - var data = req.mozResponseArrayBuffer || - req.mozResponse || - req.responseArrayBuffer || - req.response; - - PDFViewer.readPDF(data); - } - }; - - req.send(null); - }, - - readPDF: function(data) { - while (PDFViewer.element.hasChildNodes()) { - PDFViewer.element.removeChild(PDFViewer.element.firstChild); - } - - PDFViewer.pdf = new PDFDoc(new Stream(data)); - PDFViewer.numberOfPages = PDFViewer.pdf.numPages; - document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString(); - - for (var i = 1; i <= PDFViewer.numberOfPages; i++) { - PDFViewer.createPage(i); - } - - if (PDFViewer.numberOfPages > 0) { - PDFViewer.drawPage(1); - document.location.hash = 1; - } - - PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? - 'disabled' : ''; - PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? - 'disabled' : ''; - } -}; - -window.onload = function() { - - // Parse the URL query parameters into a cached object. - PDFViewer.queryParams = function() { - var qs = window.location.search.substring(1); - var kvs = qs.split('&'); - var params = {}; - for (var i = 0; i < kvs.length; ++i) { - var kv = kvs[i].split('='); - params[unescape(kv[0])] = unescape(kv[1]); - } - - return params; - }(); - - PDFViewer.element = document.getElementById('viewer'); - - PDFViewer.pageNumberInput = document.getElementById('pageNumber'); - PDFViewer.pageNumberInput.onkeydown = function(evt) { - var charCode = evt.charCode || evt.keyCode; - - // Up arrow key. - if (charCode === 38) { - PDFViewer.goToNextPage(); - this.select(); - } - - // Down arrow key. - else if (charCode === 40) { - PDFViewer.goToPreviousPage(); - this.select(); - } - - // All other non-numeric keys (excluding Left arrow, Right arrow, - // Backspace, and Delete keys). - else if ((charCode < 48 || charCode > 57) && - charCode !== 8 && // Backspace - charCode !== 46 && // Delete - charCode !== 37 && // Left arrow - charCode !== 39 // Right arrow - ) { - return false; - } - - return true; - }; - PDFViewer.pageNumberInput.onkeyup = function(evt) { - var charCode = evt.charCode || evt.keyCode; - - // All numeric keys, Backspace, and Delete. - if ((charCode >= 48 && charCode <= 57) || - charCode === 8 || // Backspace - charCode === 46 // Delete - ) { - PDFViewer.goToPage(this.value); - } - - this.focus(); - }; - - PDFViewer.previousPageButton = document.getElementById('previousPageButton'); - PDFViewer.previousPageButton.onclick = function(evt) { - if (this.className.indexOf('disabled') === -1) { - PDFViewer.goToPreviousPage(); - } - }; - PDFViewer.previousPageButton.onmousedown = function(evt) { - if (this.className.indexOf('disabled') === -1) { - this.className = 'down'; - } - }; - PDFViewer.previousPageButton.onmouseup = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - PDFViewer.previousPageButton.onmouseout = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - - PDFViewer.nextPageButton = document.getElementById('nextPageButton'); - PDFViewer.nextPageButton.onclick = function(evt) { - if (this.className.indexOf('disabled') === -1) { - PDFViewer.goToNextPage(); - } - }; - PDFViewer.nextPageButton.onmousedown = function(evt) { - if (this.className.indexOf('disabled') === -1) { - this.className = 'down'; - } - }; - PDFViewer.nextPageButton.onmouseup = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - PDFViewer.nextPageButton.onmouseout = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - - PDFViewer.scaleSelect = document.getElementById('scaleSelect'); - PDFViewer.scaleSelect.onchange = function(evt) { - PDFViewer.changeScale(parseInt(this.value)); - }; - - if (window.File && window.FileReader && window.FileList && window.Blob) { - var openFileButton = document.getElementById('openFileButton'); - openFileButton.onclick = function(evt) { - if (this.className.indexOf('disabled') === -1) { - PDFViewer.fileInput.click(); - } - }; - openFileButton.onmousedown = function(evt) { - if (this.className.indexOf('disabled') === -1) { - this.className = 'down'; - } - }; - openFileButton.onmouseup = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - openFileButton.onmouseout = function(evt) { - this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; - }; - - PDFViewer.fileInput = document.getElementById('fileInput'); - PDFViewer.fileInput.onchange = function(evt) { - var files = evt.target.files; - - if (files.length > 0) { - var file = files[0]; - var fileReader = new FileReader(); - - document.title = file.name; - - // Read the local file into a Uint8Array. - fileReader.onload = function(evt) { - var data = evt.target.result; - var buffer = new ArrayBuffer(data.length); - var uint8Array = new Uint8Array(buffer); - - for (var i = 0; i < data.length; i++) { - uint8Array[i] = data.charCodeAt(i); - } - - PDFViewer.readPDF(uint8Array); - }; - - // Read as a binary string since "readAsArrayBuffer" is not yet - // implemented in Firefox. - fileReader.readAsBinaryString(file); - } - }; - PDFViewer.fileInput.value = null; - } else { - document.getElementById('fileWrapper').style.display = 'none'; - } - - PDFViewer.pageNumber = parseInt(PDFViewer.queryParams.page) || PDFViewer.pageNumber; - PDFViewer.scale = parseInt(PDFViewer.scaleSelect.value) / 100 || 1.0; - - PDFViewer.openURL(PDFViewer.queryParams.file || PDFViewer.url); - - window.onscroll = function(evt) { - var lastPagesDrawn = PDFViewer.lastPagesDrawn; - var visiblePages = PDFViewer.visiblePages(); - - var pagesToDraw = []; - var pagesToKeep = []; - var pagesToRemove = []; - - var i; - - // Determine which visible pages were not previously drawn. - for (i = 0; i < visiblePages.length; i++) { - if (lastPagesDrawn.indexOf(visiblePages[i]) === -1) { - pagesToDraw.push(visiblePages[i]); - PDFViewer.drawPage(visiblePages[i]); - } else { - pagesToKeep.push(visiblePages[i]); - } - } - - // Determine which previously drawn pages are no longer visible. - for (i = 0; i < lastPagesDrawn.length; i++) { - if (visiblePages.indexOf(lastPagesDrawn[i]) === -1) { - pagesToRemove.push(lastPagesDrawn[i]); - PDFViewer.removePage(lastPagesDrawn[i]); - } - } - - PDFViewer.lastPagesDrawn = pagesToDraw.concat(pagesToKeep); - - // Update the page number input with the current page number. - if (!PDFViewer.willJumpToPage && visiblePages.length > 0) { - PDFViewer.pageNumber = PDFViewer.pageNumberInput.value = visiblePages[0]; - PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? - 'disabled' : ''; - PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? - 'disabled' : ''; - } else { - PDFViewer.willJumpToPage = false; - } - }; -}; diff --git a/multi_page_viewer.css b/multi_page_viewer.css new file mode 100644 index 000000000..b3eaab792 --- /dev/null +++ b/multi_page_viewer.css @@ -0,0 +1,197 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +body { + background-color: #929292; + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif; + margin: 0px; + padding: 0px; +} + +canvas { + box-shadow: 0px 4px 10px #000; + -moz-box-shadow: 0px 4px 10px #000; + -webkit-box-shadow: 0px 4px 10px #000; +} + +span { + font-size: 0.8em; +} + +.control { + display: inline-block; + float: left; + margin: 0px 20px 0px 0px; + padding: 0px 4px 0px 0px; +} + +.control > input { + float: left; + border: 1px solid #4d4d4d; + height: 20px; + padding: 0px; + margin: 0px 2px 0px 0px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); +} + +.control > select { + float: left; + border: 1px solid #4d4d4d; + height: 22px; + padding: 2px 0px 0px; + margin: 0px 0px 1px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); +} + +.control > span { + cursor: default; + float: left; + height: 18px; + margin: 5px 2px 0px; + padding: 0px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +.control .label { + clear: both; + float: left; + font-size: 0.65em; + margin: 2px 0px 0px; + position: relative; + text-align: center; + width: 100%; +} + +.page { + width: 816px; + height: 1056px; + margin: 10px auto; +} + +#controls { + background-color: #eee; + border-bottom: 1px solid #666; + padding: 4px 0px 0px 8px; + position: fixed; + left: 0px; + top: 0px; + height: 40px; + width: 100%; + box-shadow: 0px 2px 8px #000; + -moz-box-shadow: 0px 2px 8px #000; + -webkit-box-shadow: 0px 2px 8px #000; +} + +#controls input { + user-select: text; + -moz-user-select: text; + -webkit-user-select: text; +} + +#previousPageButton { + background: url('images/buttons.png') no-repeat 0px -23px; + cursor: default; + display: inline-block; + float: left; + margin: 0px; + width: 28px; + height: 23px; +} + +#previousPageButton.down { + background: url('images/buttons.png') no-repeat 0px -46px; +} + +#previousPageButton.disabled { + background: url('images/buttons.png') no-repeat 0px 0px; +} + +#nextPageButton { + background: url('images/buttons.png') no-repeat -28px -23px; + cursor: default; + display: inline-block; + float: left; + margin: 0px; + width: 28px; + height: 23px; +} + +#nextPageButton.down { + background: url('images/buttons.png') no-repeat -28px -46px; +} + +#nextPageButton.disabled { + background: url('images/buttons.png') no-repeat -28px 0px; +} + +#openFileButton { + background: url('images/buttons.png') no-repeat -56px -23px; + cursor: default; + display: inline-block; + float: left; + margin: 0px 0px 0px 3px; + width: 29px; + height: 23px; +} + +#openFileButton.down { + background: url('images/buttons.png') no-repeat -56px -46px; +} + +#openFileButton.disabled { + background: url('images/buttons.png') no-repeat -56px 0px; +} + +#fileInput { + display: none; +} + +#pageNumber { + text-align: right; +} + +#sidebar { + background-color: rgba(0, 0, 0, 0.8); + position: fixed; + width: 150px; + top: 62px; + bottom: 18px; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + -moz-border-radius-topright: 8px; + -moz-border-radius-bottomright: 8px; + -webkit-border-top-right-radius: 8px; + -webkit-border-bottom-right-radius: 8px; +} + +#sidebarScrollView { + position: absolute; + overflow: hidden; + overflow-y: auto; + top: 40px; + right: 10px; + bottom: 10px; + left: 10px; +} + +#sidebarContentView { + height: auto; + width: 100px; +} + +#viewer { + margin: 44px 0px 0px; + padding: 8px 0px; +} diff --git a/multi_page_viewer.html b/multi_page_viewer.html new file mode 100644 index 000000000..47234686d --- /dev/null +++ b/multi_page_viewer.html @@ -0,0 +1,51 @@ + + + +pdf.js Multi-Page Viewer + + + + + + + + +
+ + + + Previous/Next + + + + / + -- + Page Number + + + + Zoom + + + + + Open File + +
+ +
+ + diff --git a/multi_page_viewer.js b/multi_page_viewer.js new file mode 100644 index 000000000..3a02ea332 --- /dev/null +++ b/multi_page_viewer.js @@ -0,0 +1,458 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +var PDFViewer = { + queryParams: {}, + + element: null, + + previousPageButton: null, + nextPageButton: null, + pageNumberInput: null, + scaleSelect: null, + fileInput: null, + + willJumpToPage: false, + + pdf: null, + + url: 'compressed.tracemonkey-pldi-09.pdf', + pageNumber: 1, + numberOfPages: 1, + + scale: 1.0, + + pageWidth: function() { + return 816 * PDFViewer.scale; + }, + + pageHeight: function() { + return 1056 * PDFViewer.scale; + }, + + lastPagesDrawn: [], + + visiblePages: function() { + var pageHeight = PDFViewer.pageHeight() + 20; // Add 20 for the margins. + var windowTop = window.pageYOffset; + var windowBottom = window.pageYOffset + window.innerHeight; + var pageStartIndex = Math.floor(windowTop / pageHeight); + var pageStopIndex = Math.ceil(windowBottom / pageHeight); + + var pages = []; + + for (var i = pageStartIndex; i <= pageStopIndex; i++) { + pages.push(i + 1); + } + + return pages; + }, + + createPage: function(num) { + var anchor = document.createElement('a'); + anchor.name = '' + num; + + var div = document.createElement('div'); + div.id = 'pageContainer' + num; + div.className = 'page'; + div.style.width = PDFViewer.pageWidth() + 'px'; + div.style.height = PDFViewer.pageHeight() + 'px'; + + PDFViewer.element.appendChild(anchor); + PDFViewer.element.appendChild(div); + }, + + removePage: function(num) { + var div = document.getElementById('pageContainer' + num); + + if (div) { + while (div.hasChildNodes()) { + div.removeChild(div.firstChild); + } + } + }, + + drawPage: function(num) { + if (!PDFViewer.pdf) { + return; + } + + var div = document.getElementById('pageContainer' + num); + var canvas = document.createElement('canvas'); + + if (div && !div.hasChildNodes()) { + div.appendChild(canvas); + + var page = PDFViewer.pdf.getPage(num); + + canvas.id = 'page' + num; + canvas.mozOpaque = true; + + // Canvas dimensions must be specified in CSS pixels. CSS pixels + // are always 96 dpi. These dimensions are 8.5in x 11in at 96dpi. + canvas.width = PDFViewer.pageWidth(); + canvas.height = PDFViewer.pageHeight(); + + var ctx = canvas.getContext('2d'); + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + + var gfx = new CanvasGraphics(ctx); + var fonts = []; + + // page.compile will collect all fonts for us, once we have loaded them + // we can trigger the actual page rendering with page.display + page.compile(gfx, fonts); + + var areFontsReady = true; + + // Inspect fonts and translate the missing one + var fontCount = fonts.length; + + for (var i = 0; i < fontCount; i++) { + var font = fonts[i]; + + if (Fonts[font.name]) { + areFontsReady = areFontsReady && !Fonts[font.name].loading; + continue; + } + + new Font(font.name, font.file, font.properties); + + areFontsReady = false; + } + + var pageInterval; + + var delayLoadFont = function() { + for (var i = 0; i < fontCount; i++) { + if (Fonts[font.name].loading) { + return; + } + } + + clearInterval(pageInterval); + + while (div.hasChildNodes()) { + div.removeChild(div.firstChild); + } + + PDFViewer.drawPage(num); + } + + if (!areFontsReady) { + pageInterval = setInterval(delayLoadFont, 10); + return; + } + + page.display(gfx); + } + }, + + changeScale: function(num) { + while (PDFViewer.element.hasChildNodes()) { + PDFViewer.element.removeChild(PDFViewer.element.firstChild); + } + + PDFViewer.scale = num / 100; + + var i; + + if (PDFViewer.pdf) { + for (i = 1; i <= PDFViewer.numberOfPages; i++) { + PDFViewer.createPage(i); + } + + if (PDFViewer.numberOfPages > 0) { + PDFViewer.drawPage(1); + } + } + + for (i = 0; i < PDFViewer.scaleSelect.childNodes; i++) { + var option = PDFViewer.scaleSelect.childNodes[i]; + + if (option.value == num) { + if (!option.selected) { + option.selected = 'selected'; + } + } else { + if (option.selected) { + option.removeAttribute('selected'); + } + } + } + + PDFViewer.scaleSelect.value = Math.floor(PDFViewer.scale * 100) + '%'; + }, + + goToPage: function(num) { + if (1 <= num && num <= PDFViewer.numberOfPages) { + PDFViewer.pageNumber = num; + PDFViewer.pageNumberInput.value = PDFViewer.pageNumber; + PDFViewer.willJumpToPage = true; + + document.location.hash = PDFViewer.pageNumber; + + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? 'disabled' : ''; + } + }, + + goToPreviousPage: function() { + if (PDFViewer.pageNumber > 1) { + PDFViewer.goToPage(--PDFViewer.pageNumber); + } + }, + + goToNextPage: function() { + if (PDFViewer.pageNumber < PDFViewer.numberOfPages) { + PDFViewer.goToPage(++PDFViewer.pageNumber); + } + }, + + openURL: function(url) { + PDFViewer.url = url; + document.title = url; + + var req = new XMLHttpRequest(); + req.open('GET', url); + req.mozResponseType = req.responseType = 'arraybuffer'; + req.expected = (document.URL.indexOf('file:') === 0) ? 0 : 200; + + req.onreadystatechange = function() { + if (req.readyState === 4 && req.status === req.expected) { + var data = req.mozResponseArrayBuffer || req.mozResponse || req.responseArrayBuffer || req.response; + + PDFViewer.readPDF(data); + } + }; + + req.send(null); + }, + + readPDF: function(data) { + while (PDFViewer.element.hasChildNodes()) { + PDFViewer.element.removeChild(PDFViewer.element.firstChild); + } + + PDFViewer.pdf = new PDFDoc(new Stream(data)); + PDFViewer.numberOfPages = PDFViewer.pdf.numPages; + document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString(); + + for (var i = 1; i <= PDFViewer.numberOfPages; i++) { + PDFViewer.createPage(i); + } + + if (PDFViewer.numberOfPages > 0) { + PDFViewer.drawPage(1); + document.location.hash = 1; + } + + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? 'disabled' : ''; + } +}; + +window.onload = function() { + + // Parse the URL query parameters into a cached object. + PDFViewer.queryParams = function() { + var qs = window.location.search.substring(1); + var kvs = qs.split('&'); + var params = {}; + + for (var i = 0; i < kvs.length; ++i) { + var kv = kvs[i].split('='); + params[unescape(kv[0])] = unescape(kv[1]); + } + + return params; + }(); + + PDFViewer.element = document.getElementById('viewer'); + + PDFViewer.pageNumberInput = document.getElementById('pageNumber'); + PDFViewer.pageNumberInput.onkeydown = function(evt) { + var charCode = evt.charCode || evt.keyCode; + + // Up arrow key. + if (charCode === 38) { + PDFViewer.goToNextPage(); + this.select(); + } + + // Down arrow key. + else if (charCode === 40) { + PDFViewer.goToPreviousPage(); + this.select(); + } + + // All other non-numeric keys (excluding Left arrow, Right arrow, + // Backspace, and Delete keys). + else if ((charCode < 48 || charCode > 57) && + charCode !== 8 && // Backspace + charCode !== 46 && // Delete + charCode !== 37 && // Left arrow + charCode !== 39 // Right arrow + ) { + return false; + } + + return true; + }; + PDFViewer.pageNumberInput.onkeyup = function(evt) { + var charCode = evt.charCode || evt.keyCode; + + // All numeric keys, Backspace, and Delete. + if ((charCode >= 48 && charCode <= 57) || + charCode === 8 || // Backspace + charCode === 46 // Delete + ) { + PDFViewer.goToPage(this.value); + } + + this.focus(); + }; + + PDFViewer.previousPageButton = document.getElementById('previousPageButton'); + PDFViewer.previousPageButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.goToPreviousPage(); + } + }; + PDFViewer.previousPageButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + PDFViewer.previousPageButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + PDFViewer.previousPageButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.nextPageButton = document.getElementById('nextPageButton'); + PDFViewer.nextPageButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.goToNextPage(); + } + }; + PDFViewer.nextPageButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + PDFViewer.nextPageButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + PDFViewer.nextPageButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.scaleSelect = document.getElementById('scaleSelect'); + PDFViewer.scaleSelect.onchange = function(evt) { + PDFViewer.changeScale(parseInt(this.value)); + }; + + if (window.File && window.FileReader && window.FileList && window.Blob) { + var openFileButton = document.getElementById('openFileButton'); + openFileButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.fileInput.click(); + } + }; + openFileButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + openFileButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + openFileButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.fileInput = document.getElementById('fileInput'); + PDFViewer.fileInput.onchange = function(evt) { + var files = evt.target.files; + + if (files.length > 0) { + var file = files[0]; + var fileReader = new FileReader(); + + document.title = file.name; + + // Read the local file into a Uint8Array. + fileReader.onload = function(evt) { + var data = evt.target.result; + var buffer = new ArrayBuffer(data.length); + var uint8Array = new Uint8Array(buffer); + + for (var i = 0; i < data.length; i++) { + uint8Array[i] = data.charCodeAt(i); + } + + PDFViewer.readPDF(uint8Array); + }; + + // Read as a binary string since "readAsArrayBuffer" is not yet + // implemented in Firefox. + fileReader.readAsBinaryString(file); + } + }; + PDFViewer.fileInput.value = null; + } else { + document.getElementById('fileWrapper').style.display = 'none'; + } + + PDFViewer.pageNumber = parseInt(PDFViewer.queryParams.page) || PDFViewer.pageNumber; + PDFViewer.scale = parseInt(PDFViewer.scaleSelect.value) / 100 || 1.0; + + PDFViewer.openURL(PDFViewer.queryParams.file || PDFViewer.url); + + window.onscroll = function(evt) { + var lastPagesDrawn = PDFViewer.lastPagesDrawn; + var visiblePages = PDFViewer.visiblePages(); + + var pagesToDraw = []; + var pagesToKeep = []; + var pagesToRemove = []; + + var i; + + // Determine which visible pages were not previously drawn. + for (i = 0; i < visiblePages.length; i++) { + if (lastPagesDrawn.indexOf(visiblePages[i]) === -1) { + pagesToDraw.push(visiblePages[i]); + PDFViewer.drawPage(visiblePages[i]); + } else { + pagesToKeep.push(visiblePages[i]); + } + } + + // Determine which previously drawn pages are no longer visible. + for (i = 0; i < lastPagesDrawn.length; i++) { + if (visiblePages.indexOf(lastPagesDrawn[i]) === -1) { + pagesToRemove.push(lastPagesDrawn[i]); + PDFViewer.removePage(lastPagesDrawn[i]); + } + } + + PDFViewer.lastPagesDrawn = pagesToDraw.concat(pagesToKeep); + + // Update the page number input with the current page number. + if (!PDFViewer.willJumpToPage && visiblePages.length > 0) { + PDFViewer.pageNumber = PDFViewer.pageNumberInput.value = visiblePages[0]; + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? 'disabled' : ''; + } else { + PDFViewer.willJumpToPage = false; + } + }; +}; diff --git a/pdf.js b/pdf.js index 00d07df6f..dea681265 100644 --- a/pdf.js +++ b/pdf.js @@ -479,17 +479,17 @@ var FlateStream = (function() { array[i++] = what; } - var bytes = this.bytes; - var bytesPos = this.bytesPos; - // read block header var hdr = this.getBits(3); if (hdr & 1) this.eof = true; hdr >>= 1; - var b; if (hdr == 0) { // uncompressed block + var bytes = this.bytes; + var bytesPos = this.bytesPos; + var b; + if (typeof (b = bytes[bytesPos++]) == "undefined") error("Bad block header in flate stream"); var blockLen = b; @@ -502,18 +502,24 @@ var FlateStream = (function() { if (typeof (b = bytes[bytesPos++]) == "undefined") error("Bad block header in flate stream"); check |= (b << 8); - if (check != (~this.blockLen & 0xffff)) + if (check != (~blockLen & 0xffff)) error("Bad uncompressed block length in flate stream"); + + this.codeBuf = 0; + this.codeSize = 0; + var bufferLength = this.bufferLength; var buffer = this.ensureBuffer(bufferLength + blockLen); - this.bufferLength = bufferLength + blockLen; - for (var n = bufferLength; n < blockLen; ++n) { + var end = bufferLength + blockLen; + this.bufferLength = end; + for (var n = bufferLength; n < end; ++n) { if (typeof (b = bytes[bytesPos++]) == "undefined") { this.eof = true; break; } buffer[n] = b; } + this.bytesPos = bytesPos; return; } @@ -3242,15 +3248,34 @@ var Encodings = { } }; +function ScratchCanvas(width, height) { + var canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + return canvas; +} + var CanvasGraphics = (function() { - function constructor(canvasCtx) { + function constructor(canvasCtx, imageCanvas) { this.ctx = canvasCtx; this.current = new CanvasExtraState(); this.stateStack = [ ]; this.pendingClip = null; this.res = null; this.xobjs = null; - this.map = { + this.ScratchCanvas = imageCanvas || ScratchCanvas; + } + + var LINE_CAP_STYLES = [ "butt", "round", "square" ]; + var LINE_JOIN_STYLES = [ "miter", "round", "bevel" ]; + var NORMAL_CLIP = {}; + var EO_CLIP = {}; + + // Used for tiling patterns + var PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; + + constructor.prototype = { + map: { // Graphics state w: "setLineWidth", J: "setLineCap", @@ -3343,18 +3368,8 @@ var CanvasGraphics = (function() { // Compatibility BX: "beginCompat", EX: "endCompat", - }; - } - - var LINE_CAP_STYLES = [ "butt", "round", "square" ]; - var LINE_JOIN_STYLES = [ "miter", "round", "bevel" ]; - var NORMAL_CLIP = {}; - var EO_CLIP = {}; - - // Used for tiling patterns - var PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; - - constructor.prototype = { + }, + translateFont: function(fontDict, xref, resources) { var fd = fontDict.get("FontDescriptor"); if (!fd) @@ -3642,12 +3657,18 @@ var CanvasGraphics = (function() { }, save: function() { this.ctx.save(); + if (this.ctx.$saveCurrentX) { + this.ctx.$saveCurrentX(); + } this.stateStack.push(this.current); this.current = new CanvasExtraState(); }, restore: function() { var prev = this.stateStack.pop(); if (prev) { + if (this.ctx.$restoreCurrentX) { + this.ctx.$restoreCurrentX(); + } this.current = prev; this.ctx.restore(); } @@ -3728,6 +3749,9 @@ var CanvasGraphics = (function() { // Text beginText: function() { this.current.textMatrix = IDENTITY_MATRIX; + if (this.ctx.$setCurrentX) { + this.ctx.$setCurrentX(0) + } this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, @@ -3782,6 +3806,9 @@ var CanvasGraphics = (function() { moveText: function (x, y) { this.current.x = this.current.lineX += x; this.current.y = this.current.lineY += y; + if (this.ctx.$setCurrentX) { + this.ctx.$setCurrentX(this.current.x) + } }, setLeadingMoveText: function(x, y) { this.setLeading(-y); @@ -3789,6 +3816,10 @@ var CanvasGraphics = (function() { }, setTextMatrix: function(a, b, c, d, e, f) { this.current.textMatrix = [ a, b, c, d, e, f ]; + + if (this.ctx.$setCurrentX) { + this.ctx.$setCurrentX(0) + } this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, @@ -3799,11 +3830,15 @@ var CanvasGraphics = (function() { this.ctx.save(); this.ctx.transform.apply(this.ctx, this.current.textMatrix); this.ctx.scale(1, -1); - this.ctx.translate(0, -2 * this.current.y); - text = Fonts.charsToUnicode(text); - this.ctx.fillText(text, this.current.x, this.current.y); - this.current.x += this.ctx.measureText(text).width; + if (this.ctx.$showText) { + this.ctx.$showText(this.current.y, Fonts.charsToUnicode(text)); + } else { + text = Fonts.charsToUnicode(text); + this.ctx.translate(this.current.x, -1 * this.current.y); + this.ctx.fillText(text, 0, 0); + this.current.x += this.ctx.measureText(text).width; + } this.ctx.restore(); }, @@ -3811,7 +3846,11 @@ var CanvasGraphics = (function() { for (var i = 0; i < arr.length; ++i) { var e = arr[i]; if (IsNum(e)) { - this.current.x -= e * 0.001 * this.current.fontSize; + if (this.ctx.$addCurrentX) { + this.ctx.$addCurrentX(-e * 0.001 * this.current.fontSize) + } else { + this.current.x -= e * 0.001 * this.current.fontSize; + } } else if (IsString(e)) { this.showText(e); } else { @@ -3950,9 +3989,10 @@ var CanvasGraphics = (function() { // we want the canvas to be as large as the step size var botRight = applyMatrix([x0 + xstep, y0 + ystep], matrix); - var tmpCanvas = document.createElement("canvas"); - tmpCanvas.width = Math.ceil(botRight[0] - topLeft[0]); - tmpCanvas.height = Math.ceil(botRight[1] - topLeft[1]); + var tmpCanvas = new this.ScratchCanvas( + Math.ceil(botRight[0] - topLeft[0]), // width + Math.ceil(botRight[1] - topLeft[1]) // height + ); // set the new canvas element context as the graphics context var tmpCtx = tmpCanvas.getContext("2d"); @@ -4014,6 +4054,7 @@ var CanvasGraphics = (function() { shadingFill: function(entryRef) { var xref = this.xref; var res = this.res; + var shadingRes = xref.fetchIfRef(res.get("Shading")); if (!shadingRes) error("No shading resource found"); @@ -4310,9 +4351,7 @@ var CanvasGraphics = (function() { // handle matte object } - var tmpCanvas = document.createElement("canvas"); - tmpCanvas.width = w; - tmpCanvas.height = h; + var tmpCanvas = new this.ScratchCanvas(w, h); var tmpCtx = tmpCanvas.getContext("2d"); var imgData = tmpCtx.getImageData(0, 0, w, h); var pixels = imgData.data; @@ -4480,6 +4519,7 @@ var ColorSpace = (function() { break; case "ICCBased": var dict = stream.dict; + this.stream = stream; this.dict = dict; this.numComps = dict.get("N"); @@ -4586,6 +4626,7 @@ var PDFFunction = (function() { v = encode[i2] + ((v - domain[i2]) * (encode[i2 + 1] - encode[i2]) / (domain[i2 + 1] - domain[i2])); + // clip to the size args[i] = clip(v, 0, size[i] - 1); } @@ -4613,6 +4654,7 @@ var PDFFunction = (function() { // decode v = decode[i2] + (v * (decode[i2 + 1] - decode[i2]) / ((1 << bps) - 1)); + // clip to the domain output.push(clip(v, range[i2], range[i2 + 1])); } diff --git a/pdf_worker.js b/pdf_worker.js new file mode 100644 index 000000000..fa29428c7 --- /dev/null +++ b/pdf_worker.js @@ -0,0 +1,88 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +var consoleTimer = {}; +var console = { + log: function log() { + var args = Array.prototype.slice.call(arguments); + postMessage({ + action: "log", + data: args + }); + }, + + time: function(name) { + consoleTimer[name] = Date.now(); + }, + + timeEnd: function(name) { + var time = consoleTimer[name]; + if (time == null) { + throw "Unkown timer name " + name; + } + this.log("Timer:", name, Date.now() - time); + } +} + +// +importScripts("canvas_proxy.js"); +importScripts("pdf.js"); +importScripts("fonts.js"); +importScripts("glyphlist.js") + +// Use the JpegStreamProxy proxy. +JpegStream = JpegStreamProxy; + +// Create the WebWorkerProxyCanvas. +var canvas = new CanvasProxy(1224, 1584); + +// Listen for messages from the main thread. +var pdfDocument = null; +onmessage = function(event) { + var data = event.data; + // If there is no pdfDocument yet, then the sent data is the PDFDocument. + if (!pdfDocument) { + pdfDocument = new PDFDoc(new Stream(data)); + postMessage({ + action: "pdf_num_pages", + data: pdfDocument.numPages + }); + return; + } + // User requested to render a certain page. + else { + console.time("compile"); + + // Let's try to render the first page... + var page = pdfDocument.getPage(parseInt(data)); + + // page.compile will collect all fonts for us, once we have loaded them + // we can trigger the actual page rendering with page.display + var fonts = []; + var gfx = new CanvasGraphics(canvas.getContext("2d"), CanvasProxy); + page.compile(gfx, fonts); + console.timeEnd("compile"); + + console.time("fonts"); + // Inspect fonts and translate the missing one. + var count = fonts.length; + for (var i = 0; i < count; i++) { + var font = fonts[i]; + if (Fonts[font.name]) { + fontsReady = fontsReady && !Fonts[font.name].loading; + continue; + } + + // This "builds" the font and sents it over to the main thread. + new Font(font.name, font.file, font.properties); + } + console.timeEnd("fonts"); + + console.time("display"); + page.display(gfx); + canvas.flush(); + console.timeEnd("display"); + } +} diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore new file mode 100644 index 000000000..ef853ef61 --- /dev/null +++ b/test/pdfs/.gitignore @@ -0,0 +1 @@ +pdf.pdf diff --git a/tests/canvas.pdf b/test/pdfs/canvas.pdf similarity index 100% rename from tests/canvas.pdf rename to test/pdfs/canvas.pdf diff --git a/tests/pdf.pdf.link b/test/pdfs/pdf.pdf.link similarity index 100% rename from tests/pdf.pdf.link rename to test/pdfs/pdf.pdf.link diff --git a/tests/tracemonkey.pdf b/test/pdfs/tracemonkey.pdf similarity index 100% rename from tests/tracemonkey.pdf rename to test/pdfs/tracemonkey.pdf diff --git a/test/resources/browser_manifests/browser_manifest.json.mac b/test/resources/browser_manifests/browser_manifest.json.mac new file mode 100644 index 000000000..7c9dda943 --- /dev/null +++ b/test/resources/browser_manifests/browser_manifest.json.mac @@ -0,0 +1,10 @@ +[ + { + "name":"firefox5", + "path":"/Applications/Firefox.app" + }, + { + "name":"firefox6", + "path":"/Users/sayrer/firefoxen/Aurora.app" + } +] diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest new file mode 100644 index 000000000..614f31c3a --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest @@ -0,0 +1,4 @@ +content specialpowers chrome/specialpowers/content/ +component {59a52458-13e0-4d93-9d85-a637344f29a1} components/SpecialPowersObserver.js +contract @mozilla.org/special-powers-observer;1 {59a52458-13e0-4d93-9d85-a637344f29a1} +category profile-after-change @mozilla.org/special-powers-observer;1 @mozilla.org/special-powers-observer;1 diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js new file mode 100644 index 000000000..538b104eb --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js @@ -0,0 +1,372 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Special Powers code + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Clint Talbert cmtalbert@gmail.com + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK *****/ +/* This code is loaded in every child process that is started by mochitest in + * order to be used as a replacement for UniversalXPConnect + */ + +var Ci = Components.interfaces; +var Cc = Components.classes; + +function SpecialPowers(window) { + this.window = window; + bindDOMWindowUtils(this, window); + this._encounteredCrashDumpFiles = []; + this._unexpectedCrashDumpFiles = { }; + this._crashDumpDir = null; + this._pongHandlers = []; + this._messageListener = this._messageReceived.bind(this); + addMessageListener("SPPingService", this._messageListener); +} + +function bindDOMWindowUtils(sp, window) { + var util = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + // This bit of magic brought to you by the letters + // B Z, and E, S and the number 5. + // + // Take all of the properties on the nsIDOMWindowUtils-implementing + // object, and rebind them onto a new object with a stub that uses + // apply to call them from this privileged scope. This way we don't + // have to explicitly stub out new methods that appear on + // nsIDOMWindowUtils. + var proto = Object.getPrototypeOf(util); + var target = {}; + function rebind(desc, prop) { + if (prop in desc && typeof(desc[prop]) == "function") { + var oldval = desc[prop]; + desc[prop] = function() { return oldval.apply(util, arguments); }; + } + } + for (var i in proto) { + var desc = Object.getOwnPropertyDescriptor(proto, i); + rebind(desc, "get"); + rebind(desc, "set"); + rebind(desc, "value"); + Object.defineProperty(target, i, desc); + } + sp.DOMWindowUtils = target; +} + +SpecialPowers.prototype = { + toString: function() { return "[SpecialPowers]"; }, + sanityCheck: function() { return "foo"; }, + + // This gets filled in in the constructor. + DOMWindowUtils: undefined, + + // Mimic the get*Pref API + getBoolPref: function(aPrefName) { + return (this._getPref(aPrefName, 'BOOL')); + }, + getIntPref: function(aPrefName) { + return (this._getPref(aPrefName, 'INT')); + }, + getCharPref: function(aPrefName) { + return (this._getPref(aPrefName, 'CHAR')); + }, + getComplexValue: function(aPrefName, aIid) { + return (this._getPref(aPrefName, 'COMPLEX', aIid)); + }, + + // Mimic the set*Pref API + setBoolPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'BOOL', aValue)); + }, + setIntPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'INT', aValue)); + }, + setCharPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'CHAR', aValue)); + }, + setComplexValue: function(aPrefName, aIid, aValue) { + return (this._setPref(aPrefName, 'COMPLEX', aValue, aIid)); + }, + + // Mimic the clearUserPref API + clearUserPref: function(aPrefName) { + var msg = {'op':'clear', 'prefName': aPrefName, 'prefType': ""}; + sendSyncMessage('SPPrefService', msg); + }, + + // Private pref functions to communicate to chrome + _getPref: function(aPrefName, aPrefType, aIid) { + var msg = {}; + if (aIid) { + // Overloading prefValue to handle complex prefs + msg = {'op':'get', 'prefName': aPrefName, 'prefType':aPrefType, 'prefValue':[aIid]}; + } else { + msg = {'op':'get', 'prefName': aPrefName,'prefType': aPrefType}; + } + return(sendSyncMessage('SPPrefService', msg)[0]); + }, + _setPref: function(aPrefName, aPrefType, aValue, aIid) { + var msg = {}; + if (aIid) { + msg = {'op':'set','prefName':aPrefName, 'prefType': aPrefType, 'prefValue': [aIid,aValue]}; + } else { + msg = {'op':'set', 'prefName': aPrefName, 'prefType': aPrefType, 'prefValue': aValue}; + } + return(sendSyncMessage('SPPrefService', msg)[0]); + }, + + //XXX: these APIs really ought to be removed, they're not e10s-safe. + // (also they're pretty Firefox-specific) + _getTopChromeWindow: function(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + }, + _getDocShell: function(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + }, + _getMUDV: function(window) { + return this._getDocShell(window).contentViewer + .QueryInterface(Ci.nsIMarkupDocumentViewer); + }, + _getAutoCompletePopup: function(window) { + return this._getTopChromeWindow(window).document + .getElementById("PopupAutoComplete"); + }, + addAutoCompletePopupEventListener: function(window, listener) { + this._getAutoCompletePopup(window).addEventListener("popupshowing", + listener, + false); + }, + removeAutoCompletePopupEventListener: function(window, listener) { + this._getAutoCompletePopup(window).removeEventListener("popupshowing", + listener, + false); + }, + isBackButtonEnabled: function(window) { + return !this._getTopChromeWindow(window).document + .getElementById("Browser:Back") + .hasAttribute("disabled"); + }, + + addChromeEventListener: function(type, listener, capture, allowUntrusted) { + addEventListener(type, listener, capture, allowUntrusted); + }, + removeChromeEventListener: function(type, listener, capture) { + removeEventListener(type, listener, capture); + }, + + getFullZoom: function(window) { + return this._getMUDV(window).fullZoom; + }, + setFullZoom: function(window, zoom) { + this._getMUDV(window).fullZoom = zoom; + }, + getTextZoom: function(window) { + return this._getMUDV(window).textZoom; + }, + setTextZoom: function(window, zoom) { + this._getMUDV(window).textZoom = zoom; + }, + + createSystemXHR: function() { + return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + }, + + gc: function() { + this.DOMWindowUtils.garbageCollect(); + }, + + hasContentProcesses: function() { + try { + var rt = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + return rt.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + } catch (e) { + return true; + } + }, + + registerProcessCrashObservers: function() { + addMessageListener("SPProcessCrashService", this._messageListener); + sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); + }, + + _messageReceived: function(aMessage) { + switch (aMessage.name) { + case "SPProcessCrashService": + if (aMessage.json.type == "crash-observed") { + var self = this; + aMessage.json.dumpIDs.forEach(function(id) { + self._encounteredCrashDumpFiles.push(id + ".dmp"); + self._encounteredCrashDumpFiles.push(id + ".extra"); + }); + } + break; + + case "SPPingService": + if (aMessage.json.op == "pong") { + var handler = this._pongHandlers.shift(); + if (handler) { + handler(); + } + } + break; + } + return true; + }, + + removeExpectedCrashDumpFiles: function(aExpectingProcessCrash) { + var success = true; + if (aExpectingProcessCrash) { + var message = { + op: "delete-crash-dump-files", + filenames: this._encounteredCrashDumpFiles + }; + if (!sendSyncMessage("SPProcessCrashService", message)[0]) { + success = false; + } + } + this._encounteredCrashDumpFiles.length = 0; + return success; + }, + + findUnexpectedCrashDumpFiles: function() { + var self = this; + var message = { + op: "find-crash-dump-files", + crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles + }; + var crashDumpFiles = sendSyncMessage("SPProcessCrashService", message)[0]; + crashDumpFiles.forEach(function(aFilename) { + self._unexpectedCrashDumpFiles[aFilename] = true; + }); + return crashDumpFiles; + }, + + executeAfterFlushingMessageQueue: function(aCallback) { + this._pongHandlers.push(aCallback); + sendAsyncMessage("SPPingService", { op: "ping" }); + }, + + executeSoon: function(aFunc) { + var tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + tm.mainThread.dispatch({ + run: function() { + aFunc(); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /* from http://mxr.mozilla.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/quit.js + * by Bob Clary, Jeff Walden, and Robert Sayre. + */ + quitApplication: function() { + function canQuitApplication() + { + var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + if (!os) + return true; + + try { + var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); + os.notifyObservers(cancelQuit, "quit-application-requested", null); + + // Something aborted the quit process. + if (cancelQuit.data) + return false; + } catch (ex) {} + return true; + } + + if (!canQuitApplication()) + return false; + + var appService = Cc['@mozilla.org/toolkit/app-startup;1'].getService(Ci.nsIAppStartup); + appService.quit(Ci.nsIAppStartup.eForceQuit); + return true; + } +}; + +// Expose everything but internal APIs (starting with underscores) to +// web content. +SpecialPowers.prototype.__exposedProps__ = {}; +for each (i in Object.keys(SpecialPowers.prototype).filter(function(v) {return v.charAt(0) != "_";})) { + SpecialPowers.prototype.__exposedProps__[i] = "r"; +} + +// Attach our API to the window. +function attachSpecialPowersToWindow(aWindow) { + try { + if ((aWindow !== null) && + (aWindow !== undefined) && + (aWindow.wrappedJSObject) && + !(aWindow.wrappedJSObject.SpecialPowers)) { + aWindow.wrappedJSObject.SpecialPowers = new SpecialPowers(aWindow); + } + } catch(ex) { + dump("TEST-INFO | specialpowers.js | Failed to attach specialpowers to window exception: " + ex + "\n"); + } +} + +// This is a frame script, so it may be running in a content process. +// In any event, it is targeted at a specific "tab", so we listen for +// the DOMWindowCreated event to be notified about content windows +// being created in this context. + +function SpecialPowersManager() { + addEventListener("DOMWindowCreated", this, false); +} + +SpecialPowersManager.prototype = { + handleEvent: function handleEvent(aEvent) { + var window = aEvent.target.defaultView; + + // Need to make sure we are called on what we care about - + // content windows. DOMWindowCreated is called on *all* HTMLDocuments, + // some of which belong to chrome windows or other special content. + // + var uri = window.document.documentURIObject; + if (uri.scheme === "chrome" || uri.spec.split(":")[0] == "about") { + return; + } + + attachSpecialPowersToWindow(window); + } +}; + +var specialpowersmanager = new SpecialPowersManager(); diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js b/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js new file mode 100755 index 000000000..90655e2e7 --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js @@ -0,0 +1,293 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Special Powers code + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jesse Ruderman + * Robert Sayre + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK *****/ + +// Based on: +// https://bugzilla.mozilla.org/show_bug.cgi?id=549539 +// https://bug549539.bugzilla.mozilla.org/attachment.cgi?id=429661 +// https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_1.9.3 +// http://mxr.mozilla.org/mozilla-central/source/toolkit/components/console/hudservice/HUDService.jsm#3240 +// https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; + +const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js" + +/** + * Special Powers Exception - used to throw exceptions nicely + **/ +function SpecialPowersException(aMsg) { + this.message = aMsg; + this.name = "SpecialPowersException"; +} + +SpecialPowersException.prototype.toString = function() { + return this.name + ': "' + this.message + '"'; +}; + +/* XPCOM gunk */ +function SpecialPowersObserver() { + this._isFrameScriptLoaded = false; + this._messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. + getService(Ci.nsIChromeFrameMessageManager); +} + +SpecialPowersObserver.prototype = { + classDescription: "Special powers Observer for use in testing.", + classID: Components.ID("{59a52458-13e0-4d93-9d85-a637344f29a1}"), + contractID: "@mozilla.org/special-powers-observer;1", + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]), + _xpcom_categories: [{category: "profile-after-change", service: true }], + + observe: function(aSubject, aTopic, aData) + { + switch (aTopic) { + case "profile-after-change": + this.init(); + break; + + case "chrome-document-global-created": + if (!this._isFrameScriptLoaded) { + // Register for any messages our API needs us to handle + this._messageManager.addMessageListener("SPPrefService", this); + this._messageManager.addMessageListener("SPProcessCrashService", this); + this._messageManager.addMessageListener("SPPingService", this); + + this._messageManager.loadFrameScript(CHILD_SCRIPT, true); + this._isFrameScriptLoaded = true; + } + break; + + case "xpcom-shutdown": + this.uninit(); + break; + + case "plugin-crashed": + case "ipc:content-shutdown": + function addDumpIDToMessage(propertyName) { + var id = aSubject.getPropertyAsAString(propertyName); + if (id) { + message.dumpIDs.push(id); + } + } + + var message = { type: "crash-observed", dumpIDs: [] }; + aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); + if (aTopic == "plugin-crashed") { + addDumpIDToMessage("pluginDumpID"); + addDumpIDToMessage("browserDumpID"); + } else { // ipc:content-shutdown + addDumpIDToMessage("dumpID"); + } + this._messageManager.sendAsyncMessage("SPProcessCrashService", message); + break; + } + }, + + init: function() + { + var obs = Services.obs; + obs.addObserver(this, "xpcom-shutdown", false); + obs.addObserver(this, "chrome-document-global-created", false); + }, + + uninit: function() + { + var obs = Services.obs; + obs.removeObserver(this, "chrome-document-global-created", false); + this.removeProcessCrashObservers(); + }, + + addProcessCrashObservers: function() { + if (this._processCrashObserversRegistered) { + return; + } + + Services.obs.addObserver(this, "plugin-crashed", false); + Services.obs.addObserver(this, "ipc:content-shutdown", false); + this._processCrashObserversRegistered = true; + }, + + removeProcessCrashObservers: function() { + if (!this._processCrashObserversRegistered) { + return; + } + + Services.obs.removeObserver(this, "plugin-crashed"); + Services.obs.removeObserver(this, "ipc:content-shutdown"); + this._processCrashObserversRegistered = false; + }, + + getCrashDumpDir: function() { + if (!this._crashDumpDir) { + var directoryService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + this._crashDumpDir = directoryService.get("ProfD", Ci.nsIFile); + this._crashDumpDir.append("minidumps"); + } + return this._crashDumpDir; + }, + + deleteCrashDumpFiles: function(aFilenames) { + var crashDumpDir = this.getCrashDumpDir(); + if (!crashDumpDir.exists()) { + return false; + } + + var success = aFilenames.length != 0; + aFilenames.forEach(function(crashFilename) { + var file = crashDumpDir.clone(); + file.append(crashFilename); + if (file.exists()) { + file.remove(false); + } else { + success = false; + } + }); + return success; + }, + + findCrashDumpFiles: function(aToIgnore) { + var crashDumpDir = this.getCrashDumpDir(); + var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries; + if (!entries) { + return []; + } + + var crashDumpFiles = []; + while (entries.hasMoreElements()) { + var file = entries.getNext().QueryInterface(Ci.nsIFile); + var path = String(file.path); + if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) { + crashDumpFiles.push(path); + } + } + return crashDumpFiles.concat(); + }, + + /** + * messageManager callback function + * This will get requests from our API in the window and process them in chrome for it + **/ + receiveMessage: function(aMessage) { + switch(aMessage.name) { + case "SPPrefService": + var prefs = Services.prefs; + var prefType = aMessage.json.prefType.toUpperCase(); + var prefName = aMessage.json.prefName; + var prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null; + + if (aMessage.json.op == "get") { + if (!prefName || !prefType) + throw new SpecialPowersException("Invalid parameters for get in SPPrefService"); + } else if (aMessage.json.op == "set") { + if (!prefName || !prefType || prefValue === null) + throw new SpecialPowersException("Invalid parameters for set in SPPrefService"); + } else if (aMessage.json.op == "clear") { + if (!prefName) + throw new SpecialPowersException("Invalid parameters for clear in SPPrefService"); + } else { + throw new SpecialPowersException("Invalid operation for SPPrefService"); + } + // Now we make the call + switch(prefType) { + case "BOOL": + if (aMessage.json.op == "get") + return(prefs.getBoolPref(prefName)); + else + return(prefs.setBoolPref(prefName, prefValue)); + case "INT": + if (aMessage.json.op == "get") + return(prefs.getIntPref(prefName)); + else + return(prefs.setIntPref(prefName, prefValue)); + case "CHAR": + if (aMessage.json.op == "get") + return(prefs.getCharPref(prefName)); + else + return(prefs.setCharPref(prefName, prefValue)); + case "COMPLEX": + if (aMessage.json.op == "get") + return(prefs.getComplexValue(prefName, prefValue[0])); + else + return(prefs.setComplexValue(prefName, prefValue[0], prefValue[1])); + case "": + if (aMessage.json.op == "clear") { + prefs.clearUserPref(prefName); + return; + } + } + break; + + case "SPProcessCrashService": + switch (aMessage.json.op) { + case "register-observer": + this.addProcessCrashObservers(); + break; + case "unregister-observer": + this.removeProcessCrashObservers(); + break; + case "delete-crash-dump-files": + return this.deleteCrashDumpFiles(aMessage.json.filenames); + case "find-crash-dump-files": + return this.findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore); + default: + throw new SpecialPowersException("Invalid operation for SPProcessCrashService"); + } + break; + + case "SPPingService": + if (aMessage.json.op == "ping") { + aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager + .sendAsyncMessage("SPPingService", { op: "pong" }); + } + break; + + default: + throw new SpecialPowersException("Unrecognized Special Powers API"); + } + } +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([SpecialPowersObserver]); diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/install.rdf b/test/resources/firefox/extensions/special-powers@mozilla.org/install.rdf new file mode 100644 index 000000000..db8de988e --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/install.rdf @@ -0,0 +1,26 @@ + + + + + + special-powers@mozilla.org + 2010.07.23 + 2 + + + + + toolkit@mozilla.org + 3.0 + 7.0a1 + + + + + Special Powers + Special powers for use in testing. + Mozilla + + diff --git a/test/resources/firefox/user.js b/test/resources/firefox/user.js new file mode 100644 index 000000000..d4b9d4130 --- /dev/null +++ b/test/resources/firefox/user.js @@ -0,0 +1,34 @@ +user_pref("browser.console.showInPanel", true); +user_pref("browser.dom.window.dump.enabled", true); +user_pref("browser.firstrun.show.localepicker", false); +user_pref("browser.firstrun.show.uidiscovery", false); +user_pref("dom.allow_scripts_to_close_windows", true); +user_pref("dom.disable_open_during_load", false); +user_pref("dom.max_script_run_time", 0); // no slow script dialogs +user_pref("dom.max_chrome_script_run_time", 0); +user_pref("dom.popup_maximum", -1); +user_pref("dom.send_after_paint_to_content", true); +user_pref("dom.successive_dialog_time_limit", 0); +user_pref("security.warn_submit_insecure", false); +user_pref("browser.shell.checkDefaultBrowser", false); +user_pref("shell.checkDefaultClient", false); +user_pref("browser.warnOnQuit", false); +user_pref("accessibility.typeaheadfind.autostart", false); +user_pref("javascript.options.showInConsole", true); +user_pref("devtools.errorconsole.enabled", true); +user_pref("layout.debug.enable_data_xbl", true); +user_pref("browser.EULA.override", true); +user_pref("javascript.options.tracejit.content", true); +user_pref("javascript.options.methodjit.content", true); +user_pref("javascript.options.jitprofiling.content", true); +user_pref("javascript.options.methodjit_always", false); +user_pref("gfx.color_management.force_srgb", true); +user_pref("network.manage-offline-status", false); +user_pref("test.mousescroll", true); +user_pref("network.http.prompt-temp-redirect", false); +user_pref("media.cache_size", 100); +user_pref("security.warn_viewing_mixed", false); +user_pref("app.update.enabled", false); +user_pref("browser.panorama.experienced_first_run", true); // Assume experienced +user_pref("dom.w3c_touch_events.enabled", true); +user_pref("extensions.checkCompatibility", false); diff --git a/test.py b/test/test.py similarity index 66% rename from test.py rename to test/test.py index 9eab0e80e..53f65f78b 100644 --- a/test.py +++ b/test/test.py @@ -1,11 +1,13 @@ -import json, os, sys, subprocess, urllib2 +import json, platform, os, shutil, sys, subprocess, tempfile, threading, urllib, urllib2 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer +import SocketServer +from optparse import OptionParser from urlparse import urlparse -def prompt(question): - '''Return True iff the user answered "yes" to |question|.''' - inp = raw_input(question +' [yes/no] > ') - return inp == 'yes' +USAGE_EXAMPLE = "%prog" + +# The local web server uses the git repo as the document root. +DOC_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__),"..")) ANAL = True DEFAULT_MANIFEST_FILE = 'test_manifest.json' @@ -14,6 +16,34 @@ REFDIR = 'ref' TMPDIR = 'tmp' VERBOSE = False +class TestOptions(OptionParser): + def __init__(self, **kwargs): + OptionParser.__init__(self, **kwargs) + self.add_option("-m", "--masterMode", action="store_true", dest="masterMode", + help="Run the script in master mode.", default=False) + self.add_option("--manifestFile", action="store", type="string", dest="manifestFile", + help="A JSON file in the form of test_manifest.json (the default).") + self.add_option("-b", "--browser", action="store", type="string", dest="browser", + help="The path to a single browser (right now, only Firefox is supported).") + self.add_option("--browserManifestFile", action="store", type="string", + dest="browserManifestFile", + help="A JSON file in the form of those found in resources/browser_manifests") + self.set_usage(USAGE_EXAMPLE) + + def verifyOptions(self, options): + if options.masterMode and options.manifestFile: + self.error("--masterMode and --manifestFile must not be specified at the same time.") + if not options.manifestFile: + options.manifestFile = DEFAULT_MANIFEST_FILE + if options.browser and options.browserManifestFile: + print "Warning: ignoring browser argument since manifest file was also supplied" + return options + +def prompt(question): + '''Return True iff the user answered "yes" to |question|.''' + inp = raw_input(question +' [yes/no] > ') + return inp == 'yes' + MIMEs = { '.css': 'text/css', '.html': 'text/html', @@ -43,8 +73,11 @@ class Result: self.snapshot = snapshot self.failure = failure +class TestServer(SocketServer.TCPServer): + allow_reuse_address = True class PDFTestHandler(BaseHTTPRequestHandler): + # Disable annoying noise by default def log_request(code=0, size=0): if VERBOSE: @@ -54,13 +87,11 @@ class PDFTestHandler(BaseHTTPRequestHandler): url = urlparse(self.path) # Ignore query string path, _ = url.path, url.query - cwd = os.getcwd() - path = os.path.abspath(os.path.realpath(cwd + os.sep + path)) - cwd = os.path.abspath(cwd) - prefix = os.path.commonprefix(( path, cwd )) + path = os.path.abspath(os.path.realpath(DOC_ROOT + os.sep + path)) + prefix = os.path.commonprefix(( path, DOC_ROOT )) _, ext = os.path.splitext(path) - if not (prefix == cwd + if not (prefix == DOC_ROOT and os.path.isfile(path) and ext in MIMEs): self.send_error(404) @@ -102,13 +133,49 @@ class PDFTestHandler(BaseHTTPRequestHandler): State.done = (0 == State.remaining) +# this just does Firefox for now +class BrowserCommand(): + def __init__(self, browserRecord): + self.name = browserRecord["name"] + self.path = browserRecord["path"] -def setUp(manifestFile, masterMode): + if platform.system() == "Darwin" and (self.path.endswith(".app") or self.path.endswith(".app/")): + self._fixupMacPath() + + if not os.path.exists(self.path): + throw("Path to browser '%s' does not exist." % self.path) + + def _fixupMacPath(self): + self.path = os.path.join(self.path, "Contents", "MacOS", "firefox-bin") + + def setup(self): + self.tempDir = tempfile.mkdtemp() + self.profileDir = os.path.join(self.tempDir, "profile") + print self.profileDir + shutil.copytree(os.path.join(DOC_ROOT, "test", "resources", "firefox"), + self.profileDir) + + def teardown(self): + shutil.rmtree(self.tempDir) + + def start(self, url): + cmds = [self.path] + if platform.system() == "Darwin": + cmds.append("-foreground") + cmds.extend(["-no-remote", "-profile", self.profileDir, url]) + subprocess.call(cmds) + +def makeBrowserCommands(browserManifestFile): + with open(browserManifestFile) as bmf: + browsers = [BrowserCommand(browser) for browser in json.load(bmf)] + return browsers + +def setUp(options): # Only serve files from a pdf.js clone - assert not ANAL or os.path.isfile('pdf.js') and os.path.isdir('.git') + assert not ANAL or os.path.isfile('../pdf.js') and os.path.isdir('../.git') - State.masterMode = masterMode - if masterMode and os.path.isdir(TMPDIR): + State.masterMode = options.masterMode + if options.masterMode and os.path.isdir(TMPDIR): print 'Temporary snapshot dir tmp/ is still around.' print 'tmp/ can be removed if it has nothing you need.' if prompt('SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY'): @@ -116,14 +183,16 @@ def setUp(manifestFile, masterMode): assert not os.path.isdir(TMPDIR) - testBrowsers = [ b for b in - ( 'firefox5', 'firefox6', ) -#'chrome12', 'chrome13', 'firefox4', 'opera11' ): - if os.access(b, os.R_OK | os.X_OK) ] - - mf = open(manifestFile) - manifestList = json.load(mf) - mf.close() + testBrowsers = [] + if options.browserManifestFile: + testBrowsers = makeBrowserCommands(options.browserManifestFile) + elif options.browser: + testBrowsers = [BrowserCommand({"path":options.browser, "name":"firefox"})] + else: + print "No test browsers found. Use --browserManifest or --browser args." + + with open(options.manifestFile) as mf: + manifestList = json.load(mf) for item in manifestList: f, isLink = item['file'], item.get('link', False) @@ -143,23 +212,25 @@ def setUp(manifestFile, masterMode): print 'done' for b in testBrowsers: - State.taskResults[b] = { } + State.taskResults[b.name] = { } for item in manifestList: id, rounds = item['id'], int(item['rounds']) State.manifest[id] = item taskResults = [ ] for r in xrange(rounds): taskResults.append([ ]) - State.taskResults[b][id] = taskResults + State.taskResults[b.name][id] = taskResults State.remaining = len(testBrowsers) * len(manifestList) for b in testBrowsers: - print 'Launching', b - qs = 'browser='+ b +'&manifestFile='+ manifestFile - subprocess.Popen(( os.path.abspath(os.path.realpath(b)), - 'http://localhost:8080/test_slave.html?'+ qs)) - + try: + b.setup() + print 'Launching', b.name + qs = 'browser='+ urllib.quote(b.name) +'&manifestFile='+ urllib.quote(options.manifestFile) + b.start('http://localhost:8080/test/test_slave.html?'+ qs) + finally: + b.teardown() def check(task, results, browser): failed = False @@ -302,20 +373,20 @@ def processResults(): print 'done' -def main(args): - masterMode = False - manifestFile = DEFAULT_MANIFEST_FILE - if len(args) == 1: - masterMode = (args[0] == '-m') - manifestFile = args[0] if not masterMode else manifestFile +def main(): + optionParser = TestOptions() + options, args = optionParser.parse_args() + options = optionParser.verifyOptions(options) + if options == None: + sys.exit(1) - setUp(manifestFile, masterMode) - - server = HTTPServer(('127.0.0.1', 8080), PDFTestHandler) - while not State.done: - server.handle_request() + httpd = TestServer(('127.0.0.1', 8080), PDFTestHandler) + httpd_thread = threading.Thread(target=httpd.serve_forever) + httpd_thread.setDaemon(True) + httpd_thread.start() + setUp(options) processResults() if __name__ == '__main__': - main(sys.argv[1:]) + main() diff --git a/test_manifest.json b/test/test_manifest.json similarity index 70% rename from test_manifest.json rename to test/test_manifest.json index 036b7aafc..e4a7ada81 100644 --- a/test_manifest.json +++ b/test/test_manifest.json @@ -1,21 +1,21 @@ [ { "id": "tracemonkey-eq", - "file": "tests/tracemonkey.pdf", + "file": "pdfs/tracemonkey.pdf", "rounds": 1, "type": "eq" }, { "id": "tracemonkey-fbf", - "file": "tests/tracemonkey.pdf", + "file": "pdfs/tracemonkey.pdf", "rounds": 2, "type": "fbf" }, { "id": "html5-canvas-cheat-sheet-load", - "file": "tests/canvas.pdf", + "file": "pdfs/canvas.pdf", "rounds": 1, "type": "load" }, { "id": "pdfspec-load", - "file": "tests/pdf.pdf", + "file": "pdfs/pdf.pdf", "link": true, "rounds": 1, "type": "load" diff --git a/test_slave.html b/test/test_slave.html similarity index 93% rename from test_slave.html rename to test/test_slave.html index 718e887e0..1053025e1 100644 --- a/test_slave.html +++ b/test/test_slave.html @@ -2,9 +2,9 @@ pdf.js test slave - - - + + + + + + + +
+ + + + + + -- + +
+ +
+ + + +
+ + + diff --git a/worker_client.js b/worker_client.js new file mode 100644 index 000000000..359a5f7c1 --- /dev/null +++ b/worker_client.js @@ -0,0 +1,272 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +if (typeof console.time == "undefined") { + var consoleTimer = {}; + console.time = function(name) { + consoleTimer[name] = Date.now(); + }; + + console.timeEnd = function(name) { + var time = consoleTimer[name]; + if (time == null) { + throw "Unkown timer name " + name; + } + this.log("Timer:", name, Date.now() - time); + }; +} + +function WorkerPDFDoc(canvas) { + var timer = null + + this.ctx = canvas.getContext("2d"); + this.canvas = canvas; + this.worker = new Worker('pdf_worker.js'); + + this.numPage = 1; + this.numPages = null; + + var imagesList = {}; + var canvasList = { + 0: canvas + }; + var patternList = {}; + var gradient; + + var currentX = 0; + var currentXStack = []; + + var ctxSpecial = { + "$setCurrentX": function(value) { + currentX = value; + }, + + "$addCurrentX": function(value) { + currentX += value; + }, + + "$saveCurrentX": function() { + currentXStack.push(currentX); + }, + + "$restoreCurrentX": function() { + currentX = currentXStack.pop(); + }, + + "$showText": function(y, text) { + this.translate(currentX, -1 * y); + this.fillText(text, 0, 0); + currentX += this.measureText(text).width; + }, + + "$putImageData": function(imageData, x, y) { + var imgData = this.getImageData(0, 0, imageData.width, imageData.height); + + // Store the .data property to avaid property lookups. + var imageRealData = imageData.data; + var imgRealData = imgData.data; + + // Copy over the imageData. + var len = imageRealData.length; + while (len--) + imgRealData[len] = imageRealData[len] + + this.putImageData(imgData, x, y); + }, + + "$drawImage": function(id, x, y, sx, sy, swidth, sheight) { + var image = imagesList[id]; + if (!image) { + throw "Image not found: " + id; + } + this.drawImage(image, x, y, image.width, image.height, + sx, sy, swidth, sheight); + }, + + "$drawCanvas": function(id, x, y, sx, sy, swidth, sheight) { + var canvas = canvasList[id]; + if (!canvas) { + throw "Canvas not found"; + } + if (sheight != null) { + this.drawImage(canvas, x, y, canvas.width, canvas.height, + sx, sy, swidth, sheight); + } else { + this.drawImage(canvas, x, y, canvas.width, canvas.height); + } + }, + + "$createLinearGradient": function(x0, y0, x1, y1) { + gradient = this.createLinearGradient(x0, y0, x1, y1); + }, + + "$createPatternFromCanvas": function(patternId, canvasId, kind) { + var canvas = canvasList[canvasId]; + if (!canvas) { + throw "Canvas not found"; + } + patternList[patternId] = this.createPattern(canvas, kind); + }, + + "$addColorStop": function(i, rgba) { + gradient.addColorStop(i, rgba); + }, + + "$fillStyleGradient": function() { + this.fillStyle = gradient; + }, + + "$fillStylePattern": function(id) { + var pattern = patternList[id]; + if (!pattern) { + throw "Pattern not found"; + } + this.fillStyle = pattern; + }, + + "$strokeStyleGradient": function() { + this.strokeStyle = gradient; + }, + + "$strokeStylePattern": function(id) { + var pattern = patternList[id]; + if (!pattern) { + throw "Pattern not found"; + } + this.strokeStyle = pattern; + } + } + + function renderProxyCanvas(canvas, cmdQueue) { + var ctx = canvas.getContext("2d"); + var cmdQueueLength = cmdQueue.length; + for (var i = 0; i < cmdQueueLength; i++) { + var opp = cmdQueue[i]; + if (opp[0] == "$") { + ctx[opp[1]] = opp[2]; + } else if (opp[0] in ctxSpecial) { + ctxSpecial[opp[0]].apply(ctx, opp[1]); + } else { + ctx[opp[0]].apply(ctx, opp[1]); + } + } + } + + /** + * Functions to handle data sent by the WebWorker. + */ + var actionHandler = { + "log": function(data) { + console.log.apply(console, data); + }, + + "pdf_num_pages": function(data) { + this.numPages = parseInt(data); + if (this.loadCallback) { + this.loadCallback(); + } + }, + + "font": function(data) { + var base64 = window.btoa(data.raw); + + // Add the @font-face rule to the document + var url = "url(data:" + data.mimetype + ";base64," + base64 + ");"; + var rule = "@font-face { font-family:'" + data.fontName + "';src:" + url + "}"; + var styleSheet = document.styleSheets[0]; + styleSheet.insertRule(rule, styleSheet.length); + + // Just adding the font-face to the DOM doesn't make it load. It + // seems it's loaded once Gecko notices it's used. Therefore, + // add a div on the page using the loaded font. + var div = document.createElement("div"); + var style = 'font-family:"' + data.fontName + + '";position: absolute;top:-99999;left:-99999;z-index:-99999'; + div.setAttribute("style", style); + document.body.appendChild(div); + }, + + "jpeg_stream": function(data) { + var img = new Image(); + img.src = "data:image/jpeg;base64," + window.btoa(data.raw); + imagesList[data.id] = img; + }, + + "canvas_proxy_cmd_queue": function(data) { + var id = data.id; + var cmdQueue = data.cmdQueue; + + // Check if there is already a canvas with the given id. If not, + // create a new canvas. + if (!canvasList[id]) { + var newCanvas = document.createElement("canvas"); + newCanvas.width = data.width; + newCanvas.height = data.height; + canvasList[id] = newCanvas; + } + + // There might be fonts that need to get loaded. Shedule the + // rendering at the end of the event queue ensures this. + setTimeout(function() { + if (id == 0) { + console.time("canvas rendering"); + var ctx = this.ctx; + ctx.save(); + ctx.fillStyle = "rgb(255, 255, 255)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + } + renderProxyCanvas(canvasList[id], cmdQueue); + if (id == 0) console.timeEnd("canvas rendering") + }, 0, this); + } + } + + // List to the WebWorker for data and call actionHandler on it. + this.worker.onmessage = function(event) { + var data = event.data; + if (data.action in actionHandler) { + actionHandler[data.action].call(this, data.data); + } else { + throw "Unkown action from worker: " + data.action; + } + } +} + +WorkerPDFDoc.prototype.open = function(url, callback) { + var req = new XMLHttpRequest(); + req.open("GET", url); + req.mozResponseType = req.responseType = "arraybuffer"; + req.expected = (document.URL.indexOf("file:") == 0) ? 0 : 200; + req.onreadystatechange = function() { + if (req.readyState == 4 && req.status == req.expected) { + var data = req.mozResponseArrayBuffer || req.mozResponse || + req.responseArrayBuffer || req.response; + + this.loadCallback = callback; + this.worker.postMessage(data); + this.showPage(this.numPage); + } + }.bind(this); + req.send(null); +} + +WorkerPDFDoc.prototype.showPage = function(numPage) { + this.numPage = parseInt(numPage); + this.worker.postMessage(numPage); + if (this.onChangePage) { + this.onChangePage(numPage); + } +} + +WorkerPDFDoc.prototype.nextPage = function() { + if (this.numPage == this.numPages) return; + this.showPage(++this.numPage); +} + +WorkerPDFDoc.prototype.prevPage = function() { + if (this.numPage == 1) return; + this.showPage(--this.numPage); +}