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
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ --
+
+
+
+
+
+
+
+
+
+