Merge branch 'master' into colorspace

Conflicts:
	pdf.js
This commit is contained in:
sbarman 2011-06-28 14:23:56 -07:00
commit 5a1a35813b
39 changed files with 5601 additions and 1841 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
pdf.pdf
intelisa.pdf
openweb_tm-PRINT.pdf

250
canvas_proxy.js Normal file
View File

@ -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;
}

260
crypto.js Normal file
View File

@ -0,0 +1,260 @@
/* -*- Mode: Java; tab-width: s; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=s tabstop=2 autoindent cindent expandtab: */
"use strict";
var ARCFourCipher = (function() {
function constructor(key) {
this.a = 0;
this.b = 0;
var s = new Uint8Array(256);
var i, j = 0, tmp, keyLength = key.length;
for (i = 0; i < 256; ++i)
s[i] = i;
for (i = 0; i < 256; ++i) {
tmp = s[i];
j = (j + tmp + key[i % keyLength]) & 0xFF;
s[i] = s[j];
s[j] = tmp;
}
this.s = s;
}
constructor.prototype = {
encryptBlock: function(data) {
var i, n = data.length, tmp, tmp2;
var a = this.a, b = this.b, s = this.s;
var output = new Uint8Array(n);
for (i = 0; i < n; ++i) {
var tmp;
a = (a + 1) & 0xFF;
tmp = s[a];
b = (b + tmp) & 0xFF;
tmp2 = s[b]
s[a] = tmp2;
s[b] = tmp;
output[i] = data[i] ^ s[(tmp + tmp2) & 0xFF];
}
this.a = a;
this.b = b;
return output;
}
};
return constructor;
})();
var md5 = (function() {
const r = new Uint8Array([
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]);
const k = new Int32Array([
-680876936, -389564586, 606105819, -1044525330, -176418897, 1200080426,
-1473231341, -45705983, 1770035416, -1958414417, -42063, -1990404162,
1804603682, -40341101, -1502002290, 1236535329, -165796510, -1069501632,
643717713, -373897302, -701558691, 38016083, -660478335, -405537848, 568446438,
-1019803690, -187363961, 1163531501, -1444681467, -51403784, 1735328473,
-1926607734, -378558, -2022574463, 1839030562, -35309556, -1530992060,
1272893353, -155497632, -1094730640, 681279174, -358537222, -722521979,
76029189, -640364487, -421815835, 530742520, -995338651, -198630844, 1126891415,
-1416354905, -57434055, 1700485571, -1894986606, -1051523, -2054922799,
1873313359, -30611744, -1560198380, 1309151649, -145523070, -1120210379,
718787259, -343485551]);
function hash(data, offset, length) {
var h0 = 1732584193, h1 = -271733879, h2 = -1732584194, h3 = 271733878;
// pre-processing
var paddedLength = (length + 72) & ~63; // data + 9 extra bytes
var padded = new Uint8Array(paddedLength);
var i, j, n;
for (i = 0; i < length; ++i)
padded[i] = data[offset++];
padded[i++] = 0x80;
n = paddedLength - 8;
for (; i < n; ++i)
padded[i] = 0;
padded[i++] = (length << 3) & 0xFF;
padded[i++] = (length >> 5) & 0xFF;
padded[i++] = (length >> 13) & 0xFF;
padded[i++] = (length >> 21) & 0xFF;
padded[i++] = (length >>> 29) & 0xFF;
padded[i++] = 0;
padded[i++] = 0;
padded[i++] = 0;
// chunking
// TODO ArrayBuffer ?
var w = new Int32Array(16);
for (i = 0; i < paddedLength;) {
for (j = 0; j < 16; ++j, i += 4)
w[j] = padded[i] | (padded[i + 1] << 8) | (padded[i + 2] << 16) | (padded[i + 3] << 24);
var a = h0, b = h1, c = h2, d = h3, f, g;
for (j = 0; j < 64; ++j) {
if (j < 16) {
f = (b & c) | ((~b) & d);
g = j;
} else if (j < 32) {
f = (d & b) | ((~d) & c);
g = (5 * j + 1) & 15;
} else if (j < 48) {
f = b ^ c ^ d;
g = (3 * j + 5) & 15;
} else {
f = c ^ (b | (~d));
g = (7 * j) & 15;
}
var tmp = d, rotateArg = (a + f + k[j] + w[g]) | 0, rotate = r[j];
d = c;
c = b;
b = (b + ((rotateArg << rotate) | (rotateArg >>> (32 - rotate)))) | 0;
a = tmp;
}
h0 = (h0 + a) | 0;
h1 = (h1 + b) | 0;
h2 = (h2 + c) | 0;
h3 = (h3 + d) | 0;
}
return new Uint8Array([
h0 & 0xFF, (h0 >> 8) & 0xFF, (h0 >> 16) & 0xFF, (h0 >>> 24) & 0xFF,
h1 & 0xFF, (h1 >> 8) & 0xFF, (h1 >> 16) & 0xFF, (h1 >>> 24) & 0xFF,
h2 & 0xFF, (h2 >> 8) & 0xFF, (h2 >> 16) & 0xFF, (h2 >>> 24) & 0xFF,
h3 & 0xFF, (h3 >> 8) & 0xFF, (h3 >> 16) & 0xFF, (h3 >>> 24) & 0xFF
]);
}
return hash;
})();
var CipherTransform = (function() {
function constructor(stringCipherConstructor, streamCipherConstructor) {
this.stringCipherConstructor = stringCipherConstructor;
this.streamCipherConstructor = streamCipherConstructor;
}
constructor.prototype = {
createStream: function (stream) {
var cipher = new this.streamCipherConstructor();
return new DecryptStream(stream, function(data) {
return cipher.encryptBlock(data);
});
},
decryptString: function(s) {
var cipher = new this.stringCipherConstructor();
var data = string2bytes(s);
data = cipher.encryptBlock(data);
return bytes2string(data);
}
};
return constructor;
})();
var CipherTransformFactory = (function() {
function prepareKeyData(fileId, password, ownerPassword, userPassword, flags, revision, keyLength) {
const defaultPasswordBytes = new Uint8Array([
0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08,
0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A]);
var hashData = new Uint8Array(88), i = 0, j, n;
if (password) {
n = Math.min(32, password.length);
for (; i < n; ++i)
hashData[i] = password[i];
}
j = 0;
while (i < 32) {
hashData[i++] = defaultPasswordBytes[j++];
}
// as now the padded password in the hashData[0..i]
for (j = 0, n = ownerPassword.length; j < n; ++j)
hashData[i++] = ownerPassword[j];
hashData[i++] = flags & 0xFF;
hashData[i++] = (flags >> 8) & 0xFF;
hashData[i++] = (flags >> 16) & 0xFF;
hashData[i++] = (flags >>> 24) & 0xFF;
for (j = 0, n = fileId.length; j < n; ++j)
hashData[i++] = fileId[j];
// TODO rev 4, if metadata is not encrypted pass 0xFFFFFF also
var hash = md5(hashData, 0, i);
var keyLengthInBytes = keyLength >> 3;
if (revision >= 3) {
for (j = 0; j < 50; ++j) {
hash = md5(hash, 0, keyLengthInBytes);
}
}
var encryptionKey = hash.subarray(0, keyLengthInBytes);
var cipher, checkData;
if (revision >= 3) {
// padded password in hashData, we can use this array for user password check
i = 32;
for(j = 0, n = fileId.length; j < n; ++j)
hashData[i++] = fileId[j];
cipher = new ARCFourCipher(encryptionKey);
var checkData = cipher.encryptBlock(md5(hashData, 0, i));
n = encryptionKey.length;
var derrivedKey = new Uint8Array(n), k;
for (j = 1; j <= 19; ++j) {
for (k = 0; k < n; ++k)
derrivedKey[k] = encryptionKey[k] ^ j;
cipher = new ARCFourCipher(derrivedKey);
checkData = cipher.encryptBlock(checkData);
}
} else {
cipher = new ARCFourCipher(encryptionKey);
checkData = cipher.encryptBlock(hashData.subarray(0, 32));
}
for (j = 0, n = checkData.length; j < n; ++j) {
if (userPassword[j] != checkData[j])
error("incorrect password");
}
return encryptionKey;
}
function constructor(dict, fileId, password) {
var filter = dict.get("Filter");
if (!IsName(filter) || filter.name != "Standard")
error("unknown encryption method");
this.dict = dict;
var algorithm = dict.get("V");
if (!IsInt(algorithm) ||
(algorithm != 1 && algorithm != 2))
error("unsupported encryption algorithm");
// TODO support algorithm 4
var keyLength = dict.get("Length") || 40;
if (!IsInt(keyLength) ||
keyLength < 40 || (keyLength % 8) != 0)
error("invalid key length");
// prepare keys
var ownerPassword = stringToBytes(dict.get("O"));
var userPassword = stringToBytes(dict.get("U"));
var flags = dict.get("P");
var revision = dict.get("R");
var fileIdBytes = stringToBytes(fileId);
var passwordBytes;
if (password)
passwordBytes = stringToBytes(password);
this.encryptionKey = prepareKeyData(fileIdBytes, passwordBytes,
ownerPassword, userPassword, flags, revision, keyLength);
}
constructor.prototype = {
createCipherTransform: function(num, gen) {
var encryptionKey = this.encryptionKey;
var key = new Uint8Array(encryptionKey.length + 5), i, n;
for (i = 0, n = encryptionKey.length; i < n; ++i)
key[i] = encryptionKey[i];
key[i++] = num & 0xFF;
key[i++] = (num >> 8) & 0xFF;
key[i++] = (num >> 16) & 0xFF;
key[i++] = gen & 0xFF;
key[i++] = (gen >> 8) & 0xFF;
var hash = md5(key, 0, i);
key = hash.subarray(0, Math.min(key.length, 16));
var cipherConstructor = function() {
return new ARCFourCipher(key);
};
return new CipherTransform(cipherConstructor, cipherConstructor);
}
};
return constructor;
})();

493
fonts.js
View File

@ -80,6 +80,36 @@ var Fonts = {
}
};
var FontLoader = {
bind: function(fonts) {
var worker = (typeof window == "undefined");
var ready = true;
for (var i = 0; i < fonts.length; i++) {
var font = fonts[i];
if (Fonts[font.name]) {
ready = ready && !Fonts[font.name].loading;
continue;
}
ready = false;
var obj = new Font(font.name, font.file, font.properties);
var str = "";
var data = Fonts[font.name].data;
var length = data.length;
for (var j = 0; j < length; j++)
str += String.fromCharCode(data[j]);
worker ? obj.bindWorker(str) : obj.bindDOM(str);
}
return ready;
}
};
/**
* 'Font' is the class the outside world should use, it encapsulate all the font
* decoding logics whatever type it is (assuming the font type is supported).
@ -103,7 +133,7 @@ var Font = (function () {
// If the font is to be ignored, register it like an already loaded font
// to avoid the cost of waiting for it be be loaded by the platform.
if (properties.ignore || properties.type == "TrueType" || kDisableFonts) {
if (properties.ignore || kDisableFonts) {
Fonts[name] = {
data: file,
loading: false,
@ -113,13 +143,14 @@ var Font = (function () {
return;
}
var data;
switch (properties.type) {
case "Type1":
var cff = new CFF(name, file, properties);
this.mimetype = "font/opentype";
// Wrap the CFF data inside an OTF font file
this.font = this.convert(name, cff, properties);
data = this.convert(name, cff, properties);
break;
case "TrueType":
@ -127,7 +158,7 @@ var Font = (function () {
// Repair the TrueType file if it is can be damaged in the point of
// view of the sanitizer
this.font = this.checkAndRepair(name, file, properties);
data = this.checkAndRepair(name, file, properties);
break;
default:
@ -136,14 +167,11 @@ var Font = (function () {
}
Fonts[name] = {
data: this.font,
data: data,
properties: properties,
loading: true,
cache: Object.create(null)
}
// Attach the font to the document
this.bind();
};
};
function stringToArray(str) {
@ -242,7 +270,7 @@ var Font = (function () {
return ranges;
};
function createCMAPTable(glyphs) {
function createCMapTable(glyphs) {
var ranges = getRanges(glyphs);
var headerSize = (12 * 2 + (ranges.length * 4 * 2));
@ -284,8 +312,8 @@ var Font = (function () {
idDeltas += string16(delta);
idRangeOffsets += string16(0);
for (var j = start; j <= end; j++)
glyphsIds += String.fromCharCode(j);
for (var j = 0; j < range.length; j++)
glyphsIds += String.fromCharCode(range[j]);
}
startCount += "\xFF\xFF";
@ -297,9 +325,8 @@ var Font = (function () {
idDeltas + idRangeOffsets + glyphsIds);
};
function createOS2Table() {
var OS2 = stringToArray(
"\x00\x03" + // version
function createOS2Table(properties) {
return "\x00\x03" + // version
"\x02\x24" + // xAvgCharWidth
"\x01\xF4" + // usWeightClass
"\x00\x05" + // usWidthClass
@ -327,27 +354,31 @@ var Font = (function () {
"\x00\x03" + // sTypoAscender
"\x00\x20" + // sTypeDescender
"\x00\x38" + // sTypoLineGap
"\x00\x5A" + // usWinAscent
"\x02\xB4" + // usWinDescent
string16(properties.ascent) + // usWinAscent
string16(properties.descent) + // usWinDescent
"\x00\xCE\x00\x00" + // ulCodePageRange1 (Bits 0-31)
"\x00\x01\x00\x00" + // ulCodePageRange2 (Bits 32-63)
"\x00\x00" + // sxHeight
"\x00\x00" + // sCapHeight
string16(properties.xHeight) + // sxHeight
string16(properties.capHeight) + // sCapHeight
"\x00\x01" + // usDefaultChar
"\x00\xCD" + // usBreakChar
"\x00\x02" // usMaxContext
);
return OS2;
"\x00\x02"; // usMaxContext
};
function createPostTable(properties) {
TODO("Fill with real values from the font dict");
return "\x00\x03\x00\x00" + // Version number
string32(properties.italicAngle) + // italicAngle
"\x00\x00" + // underlinePosition
"\x00\x00" + // underlineThickness
"\x00\x00\x00\x00" + // isFixedPitch
"\x00\x00\x00\x00" + // minMemType42
"\x00\x00\x00\x00" + // maxMemType42
"\x00\x00\x00\x00" + // minMemType1
"\x00\x00\x00\x00"; // maxMemType1
};
/**
* A bunch of the OpenType code is duplicate between this class and the
* TrueType code, this is intentional and will merge in a future version
* where all the code relative to OpenType will probably have its own
* class and will take decision without the Fonts consent.
* But at the moment it allows to develop around the TrueType rewriting
* on the fly without messing up with the 'regular' Type1 to OTF conversion.
*/
constructor.prototype = {
name: null,
font: null,
@ -368,11 +399,11 @@ var Font = (function () {
var length = FontsUtils.bytesToInteger(file.getBytes(4));
// Read the table associated data
var currentPosition = file.pos;
file.pos = file.start + offset;
var previousPosition = file.pos;
file.pos = file.start ? file.start : 0;
file.skip(offset);
var data = file.getBytes(length);
file.pos = currentPosition;
file.pos = previousPosition;
return {
tag: tag,
@ -393,6 +424,77 @@ var Font = (function () {
}
};
function replaceCMapTable(cmap, font, properties) {
var version = FontsUtils.bytesToInteger(font.getBytes(2));
var numTables = FontsUtils.bytesToInteger(font.getBytes(2));
for (var i = 0; i < numTables; i++) {
var platformID = FontsUtils.bytesToInteger(font.getBytes(2));
var encodingID = FontsUtils.bytesToInteger(font.getBytes(2));
var offset = FontsUtils.bytesToInteger(font.getBytes(4));
var format = FontsUtils.bytesToInteger(font.getBytes(2));
var length = FontsUtils.bytesToInteger(font.getBytes(2));
var language = FontsUtils.bytesToInteger(font.getBytes(2));
if ((format == 0 && numTables == 1) ||
(format == 6 && numTables == 1 && !properties.encoding.empty)) {
// Format 0 alone is not allowed by the sanitizer so let's rewrite
// that to a 3-1-4 Unicode BMP table
TODO("Use an other source of informations than charset here, it is not reliable");
var charset = properties.charset;
var glyphs = [];
for (var j = 0; j < charset.length; j++) {
glyphs.push({
unicode: GlyphsUnicode[charset[j]] || 0
});
}
cmap.data = createCMapTable(glyphs);
} else if (format == 6 && numTables == 1) {
// Format 6 is a 2-bytes dense mapping, which means the font data
// lives glue together even if they are pretty far in the unicode
// table. (This looks weird, so I can have missed something), this
// works on Linux but seems to fails on Mac so let's rewrite the
// cmap table to a 3-1-4 style
var firstCode = FontsUtils.bytesToInteger(font.getBytes(2));
var entryCount = FontsUtils.bytesToInteger(font.getBytes(2));
var glyphs = [];
var min = 0xffff, max = 0;
for (var j = 0; j < entryCount; j++) {
var charcode = FontsUtils.bytesToInteger(font.getBytes(2));
glyphs.push(charcode);
if (charcode < min)
min = charcode;
if (charcode > max)
max = charcode;
}
// Since Format 6 is a dense array, check for gaps
for (var j = min; j < max; j++) {
if (glyphs.indexOf(j) == -1)
glyphs.push(j);
}
for (var j = 0; j < glyphs.length; j++)
glyphs[j] = { unicode: glyphs[j] + firstCode };
var ranges= getRanges(glyphs);
assert(ranges.length == 1, "Got " + ranges.length + " ranges in a dense array");
var encoding = properties.encoding;
var denseRange = ranges[0];
var start = denseRange[0];
var end = denseRange[1];
var index = firstCode;
for (var j = start; j <= end; j++)
encoding[index++] = glyphs[j - firstCode - 1].unicode;
cmap.data = createCMapTable(glyphs);
}
}
};
// Check that required tables are present
var requiredTables = [ "OS/2", "cmap", "head", "hhea",
"hmtx", "maxp", "name", "post" ];
@ -425,7 +527,7 @@ var Font = (function () {
if (requiredTables.length && requiredTables[0] == "OS/2") {
// Create a new file to hold the new version of our truetype with a new
// header and new offsets
var ttf = Uint8Array(kMaxFontFileSize);
var ttf = new Uint8Array(kMaxFontFileSize);
// The offsets object holds at the same time a representation of where
// to write the table entry information about a table and another offset
@ -442,41 +544,19 @@ var Font = (function () {
createOpenTypeHeader("\x00\x01\x00\x00", ttf, offsets, numTables);
// Insert the missing table
var OS2 = createOS2Table();
tables.push({
tag: "OS/2",
data: OS2
data: stringToArray(createOS2Table(properties))
});
// If the font is missing a OS/2 table it's could be an old mac font
// without a 3-1-4 Unicode BMP table, so let's rewrite it.
var charset = properties.charset;
var glyphs = [];
for (var i = 0; i < charset.length; i++) {
glyphs.push({
unicode: GlyphsUnicode[charset[i]]
});
}
// Replace the old CMAP table with a shiny new one
cmap.data = createCMAPTable(glyphs);
replaceCMapTable(cmap, font, properties);
// Rewrite the 'post' table if needed
if (!post) {
post =
"\x00\x03\x00\x00" + // Version number
"\x00\x00\x01\x00" + // italicAngle
"\x00\x00" + // underlinePosition
"\x00\x00" + // underlineThickness
"\x00\x00\x00\x00" + // isFixedPitch
"\x00\x00\x00\x00" + // minMemType42
"\x00\x00\x00\x00" + // maxMemType42
"\x00\x00\x00\x00" + // minMemType1
"\x00\x00\x00\x00"; // maxMemType1
tables.unshift({
tables.push({
tag: "post",
data: stringToArray(post)
data: stringToArray(createPostTable(properties))
});
}
@ -520,16 +600,16 @@ var Font = (function () {
return font.getBytes();
},
convert: function font_convert(name, font, properties) {
var otf = Uint8Array(kMaxFontFileSize);
convert: function font_convert(fontName, font, properties) {
var otf = new Uint8Array(kMaxFontFileSize);
function createNameTable(name) {
var names = [
"See original licence", // Copyright
name, // Font family
fontName, // Font family
"undefined", // Font subfamily (font weight)
"uniqueID", // Unique ID
name, // Full font name
fontName, // Full font name
"0.1", // Version
"undefined", // Postscript name
"undefined", // Trademark
@ -537,7 +617,7 @@ var Font = (function () {
"undefined" // Designer
];
var name =
var nameTable =
"\x00\x00" + // format
"\x00\x0A" + // Number of names Record
"\x00\x7E"; // Storage
@ -554,13 +634,13 @@ var Font = (function () {
"\x00\x00" + // name ID
string16(str.length) +
string16(strOffset);
name += nameRecord;
nameTable += nameRecord;
strOffset += str.length;
}
name += names.join("");
return name;
nameTable += names.join("");
return nameTable;
}
// Required Tables
@ -568,7 +648,7 @@ var Font = (function () {
font.data, // PostScript Font Program
OS2, // OS/2 and Windows Specific metrics
cmap, // Character to glyphs mapping
head, // Font eader
head, // Font header
hhea, // Horizontal header
hmtx, // Horizontal metrics
maxp, // Maximum profile
@ -592,14 +672,12 @@ var Font = (function () {
createTableEntry(otf, offsets, "CFF ", CFF);
/** OS/2 */
OS2 = createOS2Table();
OS2 = stringToArray(createOS2Table(properties));
createTableEntry(otf, offsets, "OS/2", OS2);
//XXX Getting charstrings here seems wrong since this is another CFF glue
var charstrings = font.getOrderedCharStrings(properties.glyphs);
/** CMAP */
cmap = createCMAPTable(charstrings);
var charstrings = font.charstrings;
cmap = createCMapTable(charstrings);
createTableEntry(otf, offsets, "cmap", cmap);
/** HEAD */
@ -647,11 +725,15 @@ var Font = (function () {
createTableEntry(otf, offsets, "hhea", hhea);
/** HMTX */
hmtx = "\x01\xF4\x00\x00";
/* For some reasons, probably related to how the backend handle fonts,
* Linux seems to ignore this file and prefer the data from the CFF itself
* while Windows use this data. So be careful if you hack on Linux and
* have to touch the 'hmtx' table
*/
hmtx = "\x01\xF4\x00\x00"; // Fake .notdef
var width = 0, lsb = 0;
for (var i = 0; i < charstrings.length; i++) {
var charstring = charstrings[i].charstring;
var width = charstring[1];
var lsb = charstring[0];
width = charstrings[i].charstring[1];
hmtx += string16(width) + string16(lsb);
}
hmtx = stringToArray(hmtx);
@ -668,17 +750,7 @@ var Font = (function () {
createTableEntry(otf, offsets, "name", name);
/** POST */
// TODO: get those informations from the FontInfo structure
post = "\x00\x03\x00\x00" + // Version number
"\x00\x00\x01\x00" + // italicAngle
"\x00\x00" + // underlinePosition
"\x00\x00" + // underlineThickness
"\x00\x00\x00\x00" + // isFixedPitch
"\x00\x00\x00\x00" + // minMemType42
"\x00\x00\x00\x00" + // maxMemType42
"\x00\x00\x00\x00" + // minMemType1
"\x00\x00\x00\x00"; // maxMemType1
post = stringToArray(post);
post = stringToArray(createPostTable(properties));
createTableEntry(otf, offsets, "post", post);
// Once all the table entries header are written, dump the data!
@ -695,55 +767,29 @@ var Font = (function () {
return fontData;
},
bind: function font_bind() {
var data = this.font;
bindWorker: function font_bindWorker(data) {
postMessage({
action: "font",
data: {
raw: data,
fontName: this.name,
mimetype: this.mimetype
}
});
},
bindDOM: function font_bindDom(data) {
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
// This code could go away when bug 471915 has landed
var canvas = document.createElement("canvas");
var style = "border: 1px solid black; position:absolute; top: " +
(debug ? (100 * fontCount) : "-200") + "px; left: 2px; width: 340px; height: 100px";
canvas.setAttribute("style", style);
canvas.setAttribute("width", 340);
canvas.setAttribute("heigth", 100);
document.body.appendChild(canvas);
// 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 = " ";
// When debugging use the characters provided by the charsets to visually
// see what's happening instead of 'spaces'
var debug = false;
if (debug) {
var name = document.createElement("font");
name.setAttribute("style", "position: absolute; left: 20px; top: " +
(100 * fontCount + 60) + "px");
name.innerHTML = fontName;
document.body.appendChild(name);
// Retrieve font charset
var charset = Fonts[fontName].properties.charset || [];
// if the charset is too small make it repeat a few times
var count = 30;
while (count-- && charset.length <= 30)
charset = charset.concat(charset.slice());
for (var i = 0; i < charset.length; i++) {
var unicode = GlyphsUnicode[charset[i]];
if (!unicode)
continue;
testString += String.fromCharCode(unicode);
}
ctx.fillText(testString, 20, 20);
}
// Periodicaly check for the width of the testString, it will be
// different once the real font has loaded
var textWidth = ctx.measureText(testString).width;
@ -757,30 +803,19 @@ var Font = (function () {
if ((Date.now() - this.start) >= kMaxWaitForFontFace) {
window.clearInterval(interval);
Fonts[fontName].loading = false;
warn("Is " + fontName + " for charset: " + charset + " loaded?");
warn("Is " + fontName + " loaded?");
this.start = 0;
} else if (textWidth != ctx.measureText(testString).width) {
window.clearInterval(interval);
Fonts[fontName].loading = false;
this.start = 0;
}
if (debug)
ctx.fillText(testString, 20, 50);
}, 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);
// Add the @font-face rule to the document
var url = "url(data:" + this.mimetype + ";base64," + base64 + ");";
var url = "url(data:" + this.mimetype + ";base64," + window.btoa(data) + ");";
var rule = "@font-face { font-family:'" + fontName + "';src:" + url + "}";
var styleSheet = document.styleSheets[0];
styleSheet.insertRule(rule, styleSheet.length);
@ -810,6 +845,9 @@ var FontsUtils = {
bytes.set([value >> 24, value >> 16, value >> 8, value]);
return [bytes[0], bytes[1], bytes[2], bytes[3]];
}
error("This number of bytes " + bytesCount + " is not supported");
return null;
},
bytesToInteger: function fu_bytesToInteger(bytesArray) {
@ -938,6 +976,7 @@ var Type1Parser = function() {
"6": -1, // seac
"7": -1, //sbw
"11": "sub",
"12": "div",
// callothersubr is a mechanism to make calls on the postscript
@ -1088,7 +1127,7 @@ var Type1Parser = function() {
* The CFF class takes a Type1 file and wrap it into a 'Compact Font Format',
* which itself embed Type2 charstrings.
*/
const CFFStrings = [
var CFFStrings = [
".notdef","space","exclam","quotedbl","numbersign","dollar","percent","ampersand",
"quoteright","parenleft","parenright","asterisk","plus","comma","hyphen","period",
"slash","zero","one","two","three","four","five","six","seven","eight","nine",
@ -1146,6 +1185,8 @@ const CFFStrings = [
"001.003","Black","Bold","Book","Light","Medium","Regular","Roman","Semibold"
];
var type1Parser = new Type1Parser();
var CFF = function(name, file, properties) {
// Get the data block containing glyphs and subrs informations
var length1 = file.dict.get("Length1");
@ -1153,17 +1194,15 @@ var CFF = function(name, file, properties) {
file.skip(length1);
var eexecBlock = file.getBytes(length2);
// Decrypt the data blocks and retrieve the informations from it
var parser = new Type1Parser();
var fontInfo = parser.extractFontProgram(eexecBlock);
// Decrypt the data blocks and retrieve it's content
var data = type1Parser.extractFontProgram(eexecBlock);
properties.subrs = fontInfo.subrs;
properties.glyphs = fontInfo.charstrings;
this.data = this.wrap(name, properties);
this.charstrings = this.getOrderedCharStrings(data.charstrings);
this.data = this.wrap(name, this.charstrings, data.subrs, properties);
};
CFF.prototype = {
createCFFIndexHeader: function(objects, isByte) {
createCFFIndexHeader: function cff_createCFFIndexHeader(objects, isByte) {
// First 2 bytes contains the number of objects contained into this index
var count = objects.length;
@ -1200,18 +1239,18 @@ CFF.prototype = {
return data;
},
encodeNumber: function(value) {
encodeNumber: function cff_encodeNumber(value) {
var x = 0;
if (value >= -32768 && value <= 32767) {
return [ 28, value >> 8, value & 0xFF ];
} else if (value >= (-2147483647-1) && value <= 2147483647) {
return [ 0xFF, value >> 24, Value >> 16, value >> 8, value & 0xFF ];
} else {
error("Value: " + value + " is not allowed");
}
error("Value: " + value + " is not allowed");
return null;
},
getOrderedCharStrings: function(glyphs) {
getOrderedCharStrings: function cff_getOrderedCharStrings(glyphs) {
var charstrings = [];
for (var i = 0; i < glyphs.length; i++) {
@ -1224,7 +1263,7 @@ CFF.prototype = {
charstrings.push({
glyph: glyph,
unicode: unicode,
charstring: glyphs[i].data.slice()
charstring: glyphs[i].data
});
}
};
@ -1250,6 +1289,11 @@ CFF.prototype = {
"hlineto": 6,
"vlineto": 7,
"rrcurveto": 8,
"callsubr": 10,
"return": 11,
"sub": [12, 11],
"div": [12, 12],
"pop": [1, 12, 18],
"endchar": 14,
"rmoveto": 21,
"hmoveto": 22,
@ -1257,7 +1301,7 @@ CFF.prototype = {
"hvcurveto": 31,
},
flattenCharstring: function flattenCharstring(glyph, charstring, subrs) {
flattenCharstring: function flattenCharstring(charstring) {
var i = 0;
while (true) {
var obj = charstring[i];
@ -1266,62 +1310,39 @@ CFF.prototype = {
if (obj.charAt) {
switch (obj) {
case "callsubr":
var subr = subrs[charstring[i - 1]].slice();
if (subr.length > 1) {
subr = this.flattenCharstring(glyph, subr, subrs);
subr.pop();
charstring.splice(i - 1, 2, subr);
} else {
charstring.splice(i - 1, 2);
}
i -= 1;
break;
case "callothersubr":
var index = charstring[i - 1];
var count = charstring[i - 2];
var data = charstring[i - 3];
// XXX The callothersubr needs to support at least the 3 defaults
// otherSubrs of the spec
if (index != 3)
error("callothersubr for index: " + index + " (" + charstring + ")");
// If the flex mechanishm is not used in a font program, Adobe
// state that that entries 0, 1 and 2 can simply be replace by
// {}, which means that we can simply ignore them.
if (index < 3) {
i -= 3;
continue;
}
// This is the same things about hint replacment, if it is not used
// entry 3 can be replaced by {}
if (index == 3) {
if (!data) {
charstring.splice(i - 2, 4, "pop", 3);
charstring.splice(i - 2, 4, 3);
i -= 3;
} else {
// 5 to remove the arguments, the callothersubr call and the pop command
charstring.splice(i - 3, 5, 3);
i -= 3;
}
}
break;
case "div":
var num2 = charstring[i - 1];
var num1 = charstring[i - 2];
charstring.splice(i - 2, 3, num1 / num2);
i -= 2;
break;
case "pop":
if (i)
charstring.splice(i - 2, 2);
else
charstring.splice(i - 1, 1);
i -= 1;
break;
case "hsbw":
var charWidthVector = charstring[i - 1];
var leftSidebearing = charstring[i - 2];
var charWidthVector = charstring[1];
var leftSidebearing = charstring[0];
if (leftSidebearing)
charstring.splice(i - 2, 3, charWidthVector, leftSidebearing, "hmoveto");
else
charstring.splice(i - 2, 3, charWidthVector);
charstring.splice(i, 1, leftSidebearing, "hmoveto");
charstring.splice(0, 1);
break;
case "endchar":
@ -1333,21 +1354,16 @@ CFF.prototype = {
charstring.splice(j, 1, 28, command >> 8, command);
j+= 2;
} else if (command.charAt) {
var command = this.commandsMap[command];
if (IsArray(command)) {
charstring.splice(j - 1, 1, command[0], command[1]);
var cmd = this.commandsMap[command];
if (!cmd)
error(command);
if (IsArray(cmd)) {
charstring.splice(j, 1, cmd[0], cmd[1]);
j += 1;
} else {
charstring[j] = command;
charstring[j] = cmd;
}
} else {
charstring.splice(j, 1);
// command has already been translated, just add them to the
// charstring directly
for (var k = 0; k < command.length; k++)
charstring.splice(j + k, 0, command[k]);
j+= command.length - 1;
}
}
return charstring;
@ -1359,23 +1375,16 @@ CFF.prototype = {
i++;
}
error("failing with i = " + i + " in charstring:" + charstring + "(" + charstring.length + ")");
return [];
},
wrap: function wrap(name, properties) {
var charstrings = this.getOrderedCharStrings(properties.glyphs);
wrap: function wrap(name, charstrings, subrs, properties) {
// Starts the conversion of the Type1 charstrings to Type2
var charstringsCount = 0;
var charstringsDataLength = 0;
var glyphs = [];
for (var i = 0; i < charstrings.length; i++) {
var charstring = charstrings[i].charstring.slice();
var glyph = charstrings[i].glyph;
var flattened = this.flattenCharstring(glyph, charstring, properties.subrs);
glyphs.push(flattened);
charstringsCount++;
charstringsDataLength += flattened.length;
var glyphsCount = charstrings.length;
for (var i = 0; i < glyphsCount; i++) {
var charstring = charstrings[i].charstring;
glyphs.push(this.flattenCharstring(charstring.slice()));
}
// Create a CFF font data
@ -1410,17 +1419,16 @@ CFF.prototype = {
// Fill the charset header (first byte is the encoding)
var charset = [0x00];
for (var i = 0; i < glyphs.length; i++) {
for (var i = 0; i < glyphsCount; i++) {
var index = CFFStrings.indexOf(charstrings[i].glyph);
if (index == -1)
index = CFFStrings.length + strings.indexOf(glyph);
index = CFFStrings.length + strings.indexOf(charstrings[i].glyph);
var bytes = FontsUtils.integerToBytes(index, 2);
charset.push(bytes[0]);
charset.push(bytes[1]);
}
var charstringsIndex = this.createCFFIndexHeader([[0x40, 0x0E]].concat(glyphs), true);
charstringsIndex = charstringsIndex.join(" ").split(" "); // XXX why?
//Top Dict Index
var topDictIndex = [
@ -1446,7 +1454,7 @@ CFF.prototype = {
topDictIndex = topDictIndex.concat([28, 0, 0, 16]) // Encoding
var charstringsOffset = charsetOffset + (charstringsCount * 2) + 1;
var charstringsOffset = charsetOffset + (glyphsCount * 2) + 1;
topDictIndex = topDictIndex.concat(this.encodeNumber(charstringsOffset));
topDictIndex.push(17); // charstrings
@ -1454,7 +1462,6 @@ CFF.prototype = {
var privateOffset = charstringsOffset + charstringsIndex.length;
topDictIndex = topDictIndex.concat(this.encodeNumber(privateOffset));
topDictIndex.push(18); // Private
topDictIndex = topDictIndex.join(" ").split(" ");
var indexes = [
topDictIndex, stringsIndex,
@ -1482,23 +1489,35 @@ CFF.prototype = {
247, 32, 11,
247, 10, 161, 147, 154, 150, 143, 12, 13,
139, 12, 14,
28, 0, 55, 19
28, 0, 55, 19 // Subrs offset
]);
privateData = privateData.join(" ").split(" ");
cff.set(privateData, currentOffset);
currentOffset += privateData.length;
// Dump shit at the end of the file
var shit = [
0x00, 0x01, 0x01, 0x01,
0x13, 0x5D, 0x65, 0x64,
0x5E, 0x5B, 0xAF, 0x66,
0xBA, 0xBB, 0xB1, 0xB0,
0xB9, 0xBA, 0x65, 0xB2,
0x5C, 0x1F, 0x0B
];
cff.set(shit, currentOffset);
currentOffset += shit.length;
// Local Subrs
var flattenedSubrs = [];
var bias = 0;
var subrsCount = subrs.length;
if (subrsCount < 1240)
bias = 107;
else if (subrsCount < 33900)
bias = 1131;
else
bias = 32768;
// Add a bunch of empty subrs to deal with the Type2 bias
for (var i = 0; i < bias; i++)
flattenedSubrs.push([0x0B]);
for (var i = 0; i < subrsCount; i++) {
var subr = subrs[i];
flattenedSubrs.push(this.flattenCharstring(subr));
}
var subrsData = this.createCFFIndexHeader(flattenedSubrs, true);
cff.set(subrsData, currentOffset);
currentOffset += subrsData.length;
var fontData = [];
for (var i = 0; i < currentOffset; i++)

View File

@ -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;
}

View File

@ -1,51 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>pdf.js Multi-Page Viewer</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8"/>
<link rel="stylesheet" href="multi-page-viewer.css" type="text/css" media="screen"/>
<script type="text/javascript" src="pdf.js"></script>
<script type="text/javascript" src="fonts.js"></script>
<script type="text/javascript" src="glyphlist.js"></script>
<script type="text/javascript" src="multi-page-viewer.js"></script>
</head>
<body>
<div id="controls">
<span class="control">
<span id="previousPageButton" class="disabled"></span>
<span id="nextPageButton" class="disabled"></span>
<span class="label">Previous/Next</span>
</span>
<span class="control">
<input type="text" id="pageNumber" value="1" size="2"/>
<span>/</span>
<span id="numPages">--</span>
<span class="label">Page Number</span>
</span>
<span class="control">
<select id="scaleSelect">
<option value="50">50%</option>
<option value="75">75%</option>
<option value="100" selected="selected">100%</option>
<option value="125">125%</option>
<option value="150">150%</option>
<option value="200">200%</option>
</select>
<span class="label">Zoom</span>
</span>
<span class="control">
<span id="openFileButton"></span>
<input type="file" id="fileInput"/>
<span class="label">Open File</span>
</span>
</div>
<!--<div id="sidebar">
<div id="sidebarScrollView">
<div id="sidebarContentView">
</div>
</div>
</div>-->
<div id="viewer"></div>
</body>
</html>

View File

@ -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;
}
};
};

230
multi_page_viewer.css Normal file
View File

@ -0,0 +1,230 @@
/* -*- 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%;
}
.thumbnailPageNumber {
color: #fff;
font-size: 0.55em;
text-align: right;
margin: -6px 2px 6px 0px;
width: 102px;
}
.thumbnail {
width: 104px;
height: 134px;
margin: 0px auto 10px;
}
.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 {
position: fixed;
width: 200px;
top: 62px;
bottom: 18px;
left: -170px;
transition: left 0.25s ease-in-out 1s;
-moz-transition: left 0.25s ease-in-out 1s;
-webkit-transition: left 0.25s ease-in-out 1s;
}
#sidebar:hover {
left: 0px;
transition: left 0.25s ease-in-out 0s;
-moz-transition: left 0.25s ease-in-out 0s;
-webkit-transition: left 0.25s ease-in-out 0s;
}
#sidebarBox {
background-color: rgba(0, 0, 0, 0.7);
width: 150px;
height: 100%;
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;
box-shadow: 0px 2px 8px #000;
-moz-box-shadow: 0px 2px 8px #000;
-webkit-box-shadow: 0px 2px 8px #000;
}
#sidebarScrollView {
position: absolute;
overflow: hidden;
overflow-y: auto;
top: 10px;
bottom: 10px;
left: 10px;
width: 130px;
}
#sidebarContentView {
height: auto;
width: 100px;
}
#viewer {
margin: 44px 0px 0px;
padding: 8px 0px;
}

55
multi_page_viewer.html Normal file
View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<title>pdf.js Multi-Page Viewer</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8"/>
<link rel="stylesheet" href="multi_page_viewer.css" type="text/css" media="screen"/>
<script type="text/javascript" src="pdf.js"></script>
<script type="text/javascript" src="fonts.js"></script>
<script type="text/javascript" src="crypto.js"></script>
<script type="text/javascript" src="glyphlist.js"></script>
<script type="text/javascript" src="multi_page_viewer.js"></script>
</head>
<body>
<div id="controls">
<span class="control">
<span id="previousPageButton" class="disabled"></span>
<span id="nextPageButton" class="disabled"></span>
<span class="label">Previous/Next</span>
</span>
<span class="control">
<input type="text" id="pageNumber" value="1" size="2"/>
<span>/</span>
<span id="numPages">--</span>
<span class="label">Page Number</span>
</span>
<span class="control">
<select id="scaleSelect">
<option value="50">50%</option>
<option value="75">75%</option>
<option value="100" selected="selected">100%</option>
<option value="125">125%</option>
<option value="150">150%</option>
<option value="200">200%</option>
</select>
<span class="label">Zoom</span>
</span>
<span class="control">
<span id="openFileButton"></span>
<input type="file" id="fileInput"/>
<span class="label">Open File</span>
</span>
</div>
<!-- EXPERIMENTAL: Slide-out sidebar with page thumbnails (comment-out to disable) -->
<div id="sidebar">
<div id="sidebarBox">
<div id="sidebarScrollView">
<div id="sidebarContentView"></div>
</div>
</div>
</div>
<div id="viewer"></div>
</body>
</html>

525
multi_page_viewer.js Normal file
View File

@ -0,0 +1,525 @@
/* -*- 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 pageTimeout;
var PDFViewer = {
queryParams: {},
element: null,
sidebarContentView: 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(page) {
return page.mediaBox[2] * PDFViewer.scale;
},
pageHeight: function(page) {
return page.mediaBox[3] * PDFViewer.scale;
},
lastPagesDrawn: [],
visiblePages: function() {
const pageBottomMargin = 20;
var windowTop = window.pageYOffset;
var windowBottom = window.pageYOffset + window.innerHeight;
var pageHeight, page;
var i, n = PDFViewer.numberOfPages, currentHeight = 0;
for (i = 1; i <= n; i++) {
var page = PDFViewer.pdf.getPage(i);
pageHeight = PDFViewer.pageHeight(page) + pageBottomMargin;
if (currentHeight + pageHeight > windowTop)
break;
currentHeight += pageHeight;
}
var pages = [];
for (; i <= n && currentHeight < windowBottom; i++) {
var page = PDFViewer.pdf.getPage(i);
pageHeight = PDFViewer.pageHeight(page) + pageBottomMargin;
currentHeight += pageHeight;
pages.push(i);
}
return pages;
},
createThumbnail: function(num) {
if (PDFViewer.sidebarContentView) {
var anchor = document.createElement('a');
anchor.href = '#' + num;
var containerDiv = document.createElement('div');
containerDiv.id = 'thumbnailContainer' + num;
containerDiv.className = 'thumbnail';
var pageNumberDiv = document.createElement('div');
pageNumberDiv.className = 'thumbnailPageNumber';
pageNumberDiv.innerHTML = '' + num;
anchor.appendChild(containerDiv);
PDFViewer.sidebarContentView.appendChild(anchor);
PDFViewer.sidebarContentView.appendChild(pageNumberDiv);
}
},
removeThumbnail: function(num) {
var div = document.getElementById('thumbnailContainer' + num);
if (div) {
while (div.hasChildNodes()) {
div.removeChild(div.firstChild);
}
}
},
drawThumbnail: function(num) {
if (!PDFViewer.pdf)
return;
var div = document.getElementById('thumbnailContainer' + num);
if (div && !div.hasChildNodes()) {
var page = PDFViewer.pdf.getPage(num);
var canvas = document.createElement('canvas');
canvas.id = 'thumbnail' + 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 = 104;
canvas.height = 134;
div.appendChild(canvas);
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);
// 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 = [];
page.compile(gfx, fonts);
var loadFont = function() {
if (!FontLoader.bind(fonts)) {
pageTimeout = window.setTimeout(loadFont, 10);
return;
}
page.display(gfx);
}
loadFont();
}
},
createPage: function(num) {
var page = PDFViewer.pdf.getPage(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(page) + 'px';
div.style.height = PDFViewer.pageHeight(page) + '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);
if (div && !div.hasChildNodes()) {
var page = PDFViewer.pdf.getPage(num);
var canvas = document.createElement('canvas');
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(page);
canvas.height = PDFViewer.pageHeight(page);
div.appendChild(canvas);
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);
// 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 = [];
page.compile(gfx, fonts);
var loadFont = function() {
if (!FontLoader.bind(fonts)) {
pageTimeout = window.setTimeout(loadFont, 10);
return;
}
page.display(gfx);
}
loadFont();
}
},
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.createThumbnail(i);
PDFViewer.createPage(i);
}
}
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) + '%';
// Clear the array of the last pages drawn to force a redraw.
PDFViewer.lastPagesDrawn = [];
// Jump the scroll position to the correct page.
PDFViewer.goToPage(PDFViewer.pageNumber);
},
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);
}
while (PDFViewer.sidebarContentView.hasChildNodes()) {
PDFViewer.sidebarContentView.removeChild(PDFViewer.sidebarContentView.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;
setTimeout(function() {
for (var i = 1; i <= PDFViewer.numberOfPages; i++) {
PDFViewer.createThumbnail(i);
PDFViewer.drawThumbnail(i);
}
}, 500);
}
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.sidebarContentView = document.getElementById('sidebarContentView');
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;
}
};
};

1784
pdf.js

File diff suppressed because it is too large Load Diff

88
pdf_worker.js Normal file
View File

@ -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");
}
}

304
test.py
View File

@ -1,304 +0,0 @@
import json, os, sys, subprocess, urllib2
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
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'
ANAL = True
DEFAULT_MANIFEST_FILE = 'test_manifest.json'
REFDIR = 'ref'
TMPDIR = 'tmp'
VERBOSE = False
MIMEs = {
'.css': 'text/css',
'.html': 'text/html',
'.js': 'application/json',
'.json': 'application/json',
'.pdf': 'application/pdf',
'.xhtml': 'application/xhtml+xml',
}
class State:
browsers = [ ]
manifest = { }
taskResults = { }
remaining = 0
results = { }
done = False
masterMode = False
numErrors = 0
numEqFailures = 0
numEqNoSnapshot = 0
numFBFFailures = 0
numLoadFailures = 0
class Result:
def __init__(self, snapshot, failure):
self.snapshot = snapshot
self.failure = failure
class PDFTestHandler(BaseHTTPRequestHandler):
# Disable annoying noise by default
def log_request(code=0, size=0):
if VERBOSE:
BaseHTTPRequestHandler.log_request(code, size)
def do_GET(self):
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 ))
_, ext = os.path.splitext(path)
if not (prefix == cwd
and os.path.isfile(path)
and ext in MIMEs):
self.send_error(404)
return
if 'Range' in self.headers:
# TODO for fetch-as-you-go
self.send_error(501)
return
self.send_response(200)
self.send_header("Content-Type", MIMEs[ext])
self.end_headers()
# Sigh, os.sendfile() plz
f = open(path)
self.wfile.write(f.read())
f.close()
def do_POST(self):
numBytes = int(self.headers['Content-Length'])
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
result = json.loads(self.rfile.read(numBytes))
browser, id, failure, round, page, snapshot = result['browser'], result['id'], result['failure'], result['round'], result['page'], result['snapshot']
taskResults = State.taskResults[browser][id]
taskResults[round].append(Result(snapshot, failure))
assert len(taskResults[round]) == page
if result['taskDone']:
check(State.manifest[id], taskResults, browser)
# Please oh please GC this ...
del State.taskResults[browser][id]
State.remaining -= 1
State.done = (0 == State.remaining)
def setUp(manifestFile, masterMode):
# Only serve files from a pdf.js clone
assert not ANAL or os.path.isfile('pdf.js') and os.path.isdir('.git')
State.masterMode = masterMode
if 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'):
subprocess.call(( 'rm', '-rf', 'tmp' ))
assert not os.path.isdir(TMPDIR)
testBrowsers = [ b for b in
( 'firefox5', )
#'chrome12', 'chrome13', 'firefox4', 'firefox6','opera11' ):
if os.access(b, os.R_OK | os.X_OK) ]
mf = open(manifestFile)
manifestList = json.load(mf)
mf.close()
for item in manifestList:
f, isLink = item['file'], item.get('link', False)
if isLink and not os.access(f, os.R_OK):
linkFile = open(f +'.link')
link = linkFile.read()
linkFile.close()
sys.stdout.write('Downloading '+ link +' to '+ f +' ...')
sys.stdout.flush()
response = urllib2.urlopen(link)
out = open(f, 'w')
out.write(response.read())
out.close()
print 'done'
for b in testBrowsers:
State.taskResults[b] = { }
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.remaining = 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))
def check(task, results, browser):
failed = False
for r in xrange(len(results)):
pageResults = results[r]
for p in xrange(len(pageResults)):
pageResult = pageResults[p]
if pageResult is None:
continue
failure = pageResult.failure
if failure:
failed = True
State.numErrors += 1
print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure
if failed:
return
kind = task['type']
if 'eq' == kind:
checkEq(task, results, browser)
elif 'fbf' == kind:
checkFBF(task, results, browser)
elif 'load' == kind:
checkLoad(task, results, browser)
else:
assert 0 and 'Unknown test type'
def checkEq(task, results, browser):
pfx = os.path.join(REFDIR, sys.platform, browser, task['id'])
results = results[0]
passed = True
for page in xrange(len(results)):
snapshot = results[page].snapshot
ref = None
eq = True
path = os.path.join(pfx, str(page + 1))
if not os.access(path, os.R_OK):
print 'WARNING: no reference snapshot', path
State.numEqNoSnapshot += 1
else:
f = open(path)
ref = f.read()
f.close()
eq = (ref == snapshot)
if not eq:
print 'TEST-UNEXPECTED-FAIL | eq', task['id'], '| in', browser, '| rendering of page', page + 1, '!= reference rendering'
passed = False
State.numEqFailures += 1
if State.masterMode and (ref is None or not eq):
tmpTaskDir = os.path.join(TMPDIR, sys.platform, browser, task['id'])
try:
os.makedirs(tmpTaskDir)
except OSError, e:
pass
of = open(os.path.join(tmpTaskDir, str(page + 1)), 'w')
of.write(snapshot)
of.close()
if passed:
print 'TEST-PASS | eq test', task['id'], '| in', browser
def checkFBF(task, results, browser):
round0, round1 = results[0], results[1]
assert len(round0) == len(round1)
passed = True
for page in xrange(len(round1)):
r0Page, r1Page = round0[page], round1[page]
if r0Page is None:
break
if r0Page.snapshot != r1Page.snapshot:
print 'TEST-UNEXPECTED-FAIL | forward-back-forward test', task['id'], '| in', browser, '| first rendering of page', page + 1, '!= second'
passed = False
State.numFBFFailures += 1
if passed:
print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser
def checkLoad(task, results, browser):
# Load just checks for absence of failure, so if we got here the
# test has passed
print 'TEST-PASS | load test', task['id'], '| in', browser
def processResults():
print ''
numErrors, numEqFailures, numEqNoSnapshot, numFBFFailures = State.numErrors, State.numEqFailures, State.numEqNoSnapshot, State.numFBFFailures
numFatalFailures = (numErrors + numFBFFailures)
if 0 == numEqFailures and 0 == numFatalFailures:
print 'All tests passed.'
else:
print 'OHNOES! Some tests failed!'
if 0 < numErrors:
print ' errors:', numErrors
if 0 < numEqFailures:
print ' different ref/snapshot:', numEqFailures
if 0 < numFBFFailures:
print ' different first/second rendering:', numFBFFailures
if State.masterMode and (0 < numEqFailures or 0 < numEqNoSnapshot):
print "Some eq tests failed or didn't have snapshots."
print 'Checking to see if master references can be updated...'
if 0 < numFatalFailures:
print ' No. Some non-eq tests failed.'
else:
' Yes! The references in tmp/ can be synced with ref/.'
if not prompt('Would you like to update the master copy in ref/?'):
print ' OK, not updating.'
else:
sys.stdout.write(' Updating ... ')
# XXX unclear what to do on errors here ...
# NB: do *NOT* pass --delete to rsync. That breaks this
# entire scheme.
subprocess.check_call(( 'rsync', '-arv', 'tmp/', 'ref/' ))
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
setUp(manifestFile, masterMode)
server = HTTPServer(('127.0.0.1', 8080), PDFTestHandler)
while not State.done:
server.handle_request()
processResults()
if __name__ == '__main__':
main(sys.argv[1:])

0
test/.gitignore vendored Normal file
View File

3
test/pdfs/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
pdf.pdf
intelisa.pdf
openweb_tm-PRINT.pdf

View File

@ -0,0 +1 @@
http://www.intel.com/Assets/PDF/manual/253665.pdf

View File

@ -0,0 +1 @@
http://openweb.flossmanuals.net/materials/openweb_tm-PRINT.pdf

BIN
test/pdfs/sizes.pdf Normal file

Binary file not shown.

View File

@ -0,0 +1,10 @@
[
{
"name":"firefox5",
"path":"/Applications/Firefox.app"
},
{
"name":"firefox6",
"path":"/Users/sayrer/firefoxen/Aurora.app"
}
]

BIN
test/resources/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

@ -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();

View File

@ -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 <jruderman@mozilla.com>
* Robert Sayre <sayrer@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 *****/
// 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]);

View File

@ -0,0 +1,26 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>special-powers@mozilla.org</em:id>
<em:version>2010.07.23</em:version>
<em:type>2</em:type>
<!-- Target Application this extension can install into,
with minimum and maximum supported versions. -->
<em:targetApplication>
<Description>
<em:id>toolkit@mozilla.org</em:id>
<em:minVersion>3.0</em:minVersion>
<em:maxVersion>7.0a1</em:maxVersion>
</Description>
</em:targetApplication>
<!-- Front End MetaData -->
<em:name>Special Powers</em:name>
<em:description>Special powers for use in testing.</em:description>
<em:creator>Mozilla</em:creator>
</Description>
</RDF>

View File

@ -0,0 +1,38 @@
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);
user_pref("extensions.installDistroAddons", false); // prevent testpilot etc
user_pref("browser.safebrowsing.enable", false); // prevent traffic to google servers
user_pref("toolkit.telemetry.prompted", true); // prevent telemetry banner
user_pref("toolkit.telemetry.enabled", false);

View File

@ -0,0 +1,601 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim: set shiftwidth=4 tabstop=4 autoindent noexpandtab: -->
<!-- ***** 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 reftest-analyzer.html.
-
- The Initial Developer of the Original Code is the Mozilla Foundation.
- Portions created by the Initial Developer are Copyright (C) 2008
- the Initial Developer. All Rights Reserved.
-
- Contributor(s):
- L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
-
- 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 LGPL or the GPL. 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 ***** -->
<!--
Features to add:
* make the left and right parts of the viewer independently scrollable
* make the test list filterable
** default to only showing unexpecteds
* add other ways to highlight differences other than circling?
* add zoom/pan to images
* Add ability to load log via XMLHttpRequest (also triggered via URL param)
* color the test list based on pass/fail and expected/unexpected/random/skip
* ability to load multiple logs ?
** rename them by clicking on the name and editing
** turn the test list into a collapsing tree view
** move log loading into popup from viewer UI
-->
<html lang="en-US" xml:lang="en-US" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Reftest analyzer</title>
<style type="text/css"><![CDATA[
html, body { margin: 0; }
html { padding: 0; }
body { padding: 4px; }
#pixelarea, #itemlist, #images { position: absolute; }
#itemlist, #images { overflow: auto; }
#pixelarea { top: 0; left: 0; width: 320px; height: 84px; overflow: visible }
#itemlist { top: 84px; left: 0; width: 320px; bottom: 0; }
#images { top: 0; bottom: 0; left: 320px; right: 0; }
#leftpane { width: 320px; }
#images { position: fixed; top: 10px; left: 340px; }
form#imgcontrols { margin: 0; display: block; }
#itemlist > table { border-collapse: collapse; }
#itemlist > table > tbody > tr > td { border: 1px solid; padding: 1px; }
/*
#itemlist > table > tbody > tr.pass > td.url { background: lime; }
#itemlist > table > tbody > tr.fail > td.url { background: red; }
*/
#magnification > svg { display: block; width: 84px; height: 84px; }
#pixelinfo { font: small sans-serif; position: absolute; width: 200px; left: 84px; }
#pixelinfo table { border-collapse: collapse; }
#pixelinfo table th { white-space: nowrap; text-align: left; padding: 0; }
#pixelinfo table td { font-family: monospace; padding: 0 0 0 0.25em; }
#pixelhint { display: inline; color: #88f; cursor: help; }
#pixelhint > * { display: none; position: absolute; margin: 8px 0 0 8px; padding: 4px; width: 400px; background: #ffa; color: black; box-shadow: 3px 3px 2px #888; z-index: 1; }
#pixelhint:hover { color: #000; }
#pixelhint:hover > * { display: block; }
#pixelhint p { margin: 0; }
#pixelhint p + p { margin-top: 1em; }
]]></style>
<script type="text/javascript"><![CDATA[
var XLINK_NS = "http://www.w3.org/1999/xlink";
var SVG_NS = "http://www.w3.org/2000/svg";
var gPhases = null;
var gIDCache = {};
var gMagPixPaths = []; // 2D array of array-of-two <path> objects used in the pixel magnifier
var gMagWidth = 5; // number of zoomed in pixels to show horizontally
var gMagHeight = 5; // number of zoomed in pixels to show vertically
var gMagZoom = 16; // size of the zoomed in pixels
var gImage1Data; // ImageData object for the reference image
var gImage2Data; // ImageData object for the test output image
var gFlashingPixels = []; // array of <path> objects that should be flashed due to pixel color mismatch
function ID(id) {
if (!(id in gIDCache))
gIDCache[id] = document.getElementById(id);
return gIDCache[id];
}
function hash_parameters() {
var result = { };
var params = window.location.hash.substr(1).split(/[&;]/);
for (var i = 0; i < params.length; i++) {
var parts = params[i].split("=");
result[parts[0]] = unescape(unescape(parts[1]));
}
return result;
}
function load() {
gPhases = [ ID("entry"), ID("loading"), ID("viewer") ];
build_mag();
var params = hash_parameters();
if (params.log) {
ID("logentry").value = params.log;
log_pasted();
} else if (params.web) {
loadFromWeb(params.web);
}
}
function build_mag() {
var mag = ID("mag");
var r = document.createElementNS(SVG_NS, "rect");
r.setAttribute("x", gMagZoom * -gMagWidth / 2);
r.setAttribute("y", gMagZoom * -gMagHeight / 2);
r.setAttribute("width", gMagZoom * gMagWidth);
r.setAttribute("height", gMagZoom * gMagHeight);
mag.appendChild(r);
mag.setAttribute("transform", "translate(" + (gMagZoom * (gMagWidth / 2) + 1) + "," + (gMagZoom * (gMagHeight / 2) + 1) + ")");
for (var x = 0; x < gMagWidth; x++) {
gMagPixPaths[x] = [];
for (var y = 0; y < gMagHeight; y++) {
var p1 = document.createElementNS(SVG_NS, "path");
p1.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "h" + -gMagZoom + "v" + gMagZoom);
p1.setAttribute("stroke", "black");
p1.setAttribute("stroke-width", "1px");
p1.setAttribute("fill", "#aaa");
var p2 = document.createElementNS(SVG_NS, "path");
p2.setAttribute("d", "M" + ((x - gMagWidth / 2) + 1) * gMagZoom + "," + (y - gMagHeight / 2) * gMagZoom + "v" + gMagZoom + "h" + -gMagZoom);
p2.setAttribute("stroke", "black");
p2.setAttribute("stroke-width", "1px");
p2.setAttribute("fill", "#888");
mag.appendChild(p1);
mag.appendChild(p2);
gMagPixPaths[x][y] = [p1, p2];
}
}
var flashedOn = false;
setInterval(function() {
flashedOn = !flashedOn;
flash_pixels(flashedOn);
}, 500);
}
function show_phase(phaseid) {
for (var i in gPhases) {
var phase = gPhases[i];
phase.style.display = (phase.id == phaseid) ? "" : "none";
}
if (phase == "viewer")
ID("images").style.display = "none";
}
function loadFromWeb(url) {
var r = new XMLHttpRequest();
r.open("GET", url);
r.onreadystatechange = function() {
if (r.readyState == 4) {
process_log(r.response);
}
}
r.send(null);
}
function fileentry_changed() {
show_phase("loading");
var input = ID("fileentry");
var files = input.files;
if (files.length > 0) {
// Only handle the first file; don't handle multiple selection.
// The parts of the log we care about are ASCII-only. Since we
// can ignore lines we don't care about, best to read in as
// iso-8859-1, which guarantees we don't get decoding errors.
var fileReader = new FileReader();
fileReader.onload = function(e) {
var log = null;
log = e.target.result;
if (log)
process_log(log);
else
show_phase("entry");
}
fileReader.readAsText(files[0], "iso-8859-1");
}
// So the user can process the same filename again (after
// overwriting the log), clear the value on the form input so we
// will always get an onchange event.
input.value = "";
}
function log_pasted() {
show_phase("loading");
var entry = ID("logentry");
var log = entry.value;
entry.value = "";
process_log(log);
}
var gTestItems;
function process_log(contents) {
var lines = contents.split(/[\r\n]+/);
gTestItems = [];
for (var j in lines) {
var line = lines[j];
var match = line.match(/^(?:NEXT ERROR )?REFTEST (.*)$/);
if (!match)
continue;
line = match[1];
match = line.match(/^(TEST-PASS|TEST-UNEXPECTED-PASS|TEST-KNOWN-FAIL|TEST-UNEXPECTED-FAIL)(\(EXPECTED RANDOM\)|) \| ([^\|]+) \|(.*)/);
if (match) {
var state = match[1];
var random = match[2];
var url = match[3];
var extra = match[4];
gTestItems.push(
{
pass: !state.match(/FAIL$/),
// only one of the following three should ever be true
unexpected: !!state.match(/^TEST-UNEXPECTED/),
random: (random == "(EXPECTED RANDOM)"),
skip: (extra == " (SKIP)"),
url: url,
images: []
});
continue;
}
match = line.match(/^ IMAGE[^:]*: (.*)$/);
if (match) {
var item = gTestItems[gTestItems.length - 1];
item.images.push(match[1]);
}
}
build_viewer();
}
function build_viewer() {
if (gTestItems.length == 0) {
show_phase("entry");
return;
}
var cell = ID("itemlist");
while (cell.childNodes.length > 0)
cell.removeChild(cell.childNodes[cell.childNodes.length - 1]);
var table = document.createElement("table");
var tbody = document.createElement("tbody");
table.appendChild(tbody);
for (var i in gTestItems) {
var item = gTestItems[i];
// XXX skip expected pass items until we have filtering UI
if (item.pass && !item.unexpected)
continue;
var tr = document.createElement("tr");
var rowclass = item.pass ? "pass" : "fail";
var td;
var text;
td = document.createElement("td");
text = "";
if (item.unexpected) { text += "!"; rowclass += " unexpected"; }
if (item.random) { text += "R"; rowclass += " random"; }
if (item.skip) { text += "S"; rowclass += " skip"; }
td.appendChild(document.createTextNode(text));
tr.appendChild(td);
td = document.createElement("td");
td.className = "url";
// Only display part of URL after "/mozilla/".
var match = item.url.match(/\/mozilla\/(.*)/);
text = document.createTextNode(match ? match[1] : item.url);
if (item.images.length > 0) {
var a = document.createElement("a");
a.href = "javascript:show_images(" + i + ")";
a.appendChild(text);
td.appendChild(a);
} else {
td.appendChild(text);
}
tr.appendChild(td);
tbody.appendChild(tr);
}
cell.appendChild(table);
show_phase("viewer");
}
function get_image_data(src, whenReady) {
var img = new Image();
img.onload = function() {
var canvas = document.createElement("canvas");
canvas.width = 800;
canvas.height = 1000;
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0);
whenReady(ctx.getImageData(0, 0, 800, 1000));
};
img.src = src;
}
function show_images(i) {
var item = gTestItems[i];
var cell = ID("images");
ID("image1").style.display = "";
ID("image2").style.display = "none";
ID("diffrect").style.display = "none";
ID("imgcontrols").reset();
ID("image1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
// Making the href be #image1 doesn't seem to work
ID("feimage1").setAttributeNS(XLINK_NS, "xlink:href", item.images[0]);
if (item.images.length == 1) {
ID("imgcontrols").style.display = "none";
} else {
ID("imgcontrols").style.display = "";
ID("image2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
// Making the href be #image2 doesn't seem to work
ID("feimage2").setAttributeNS(XLINK_NS, "xlink:href", item.images[1]);
}
cell.style.display = "";
get_image_data(item.images[0], function(data) { gImage1Data = data });
get_image_data(item.images[1], function(data) { gImage2Data = data });
}
function show_image(i) {
if (i == 1) {
ID("image1").style.display = "";
ID("image2").style.display = "none";
} else {
ID("image1").style.display = "none";
ID("image2").style.display = "";
}
}
function show_differences(cb) {
ID("diffrect").style.display = cb.checked ? "" : "none";
}
function flash_pixels(on) {
var stroke = on ? "red" : "black";
var strokeWidth = on ? "2px" : "1px";
for (var i = 0; i < gFlashingPixels.length; i++) {
gFlashingPixels[i].setAttribute("stroke", stroke);
gFlashingPixels[i].setAttribute("stroke-width", strokeWidth);
}
}
function cursor_point(evt) {
var m = evt.target.getScreenCTM().inverse();
var p = ID("svg").createSVGPoint();
p.x = evt.clientX;
p.y = evt.clientY;
p = p.matrixTransform(m);
return { x: Math.floor(p.x), y: Math.floor(p.y) };
}
function hex2(i) {
return (i < 16 ? "0" : "") + i.toString(16);
}
function canvas_pixel_as_hex(data, x, y) {
var offset = (y * data.width + x) * 4;
var r = data.data[offset];
var g = data.data[offset + 1];
var b = data.data[offset + 2];
return "#" + hex2(r) + hex2(g) + hex2(b);
}
function hex_as_rgb(hex) {
return "rgb(" + [parseInt(hex.substring(1, 3), 16), parseInt(hex.substring(3, 5), 16), parseInt(hex.substring(5, 7), 16)] + ")";
}
function magnify(evt) {
var { x: x, y: y } = cursor_point(evt);
var centerPixelColor1, centerPixelColor2;
var dx_lo = -Math.floor(gMagWidth / 2);
var dx_hi = Math.floor(gMagWidth / 2);
var dy_lo = -Math.floor(gMagHeight / 2);
var dy_hi = Math.floor(gMagHeight / 2);
flash_pixels(false);
gFlashingPixels = [];
for (var j = dy_lo; j <= dy_hi; j++) {
for (var i = dx_lo; i <= dx_hi; i++) {
var px = x + i;
var py = y + j;
var p1 = gMagPixPaths[i + dx_hi][j + dy_hi][0];
var p2 = gMagPixPaths[i + dx_hi][j + dy_hi][1];
if (px < 0 || py < 0 || px >= 800 || py >= 1000) {
p1.setAttribute("fill", "#aaa");
p2.setAttribute("fill", "#888");
} else {
var color1 = canvas_pixel_as_hex(gImage1Data, x + i, y + j);
var color2 = canvas_pixel_as_hex(gImage2Data, x + i, y + j);
p1.setAttribute("fill", color1);
p2.setAttribute("fill", color2);
if (color1 != color2) {
gFlashingPixels.push(p1, p2);
p1.parentNode.appendChild(p1);
p2.parentNode.appendChild(p2);
}
if (i == 0 && j == 0) {
centerPixelColor1 = color1;
centerPixelColor2 = color2;
}
}
}
}
flash_pixels(true);
show_pixelinfo(x, y, centerPixelColor1, hex_as_rgb(centerPixelColor1), centerPixelColor2, hex_as_rgb(centerPixelColor2));
}
function show_pixelinfo(x, y, pix1rgb, pix1hex, pix2rgb, pix2hex) {
var pixelinfo = ID("pixelinfo");
ID("coords").textContent = [x, y];
ID("pix1hex").textContent = pix1hex;
ID("pix1rgb").textContent = pix1rgb;
ID("pix2hex").textContent = pix2hex;
ID("pix2rgb").textContent = pix2rgb;
}
]]></script>
</head>
<body onload="load()">
<div id="entry">
<h1>Reftest analyzer: load reftest log</h1>
<p>Either paste your log into this textarea:<br />
<textarea cols="80" rows="10" id="logentry"/><br/>
<input type="button" value="Process pasted log" onclick="log_pasted()" /></p>
<p>... or load it from a file:<br/>
<input type="file" id="fileentry" onchange="fileentry_changed()" />
</p>
</div>
<div id="loading" style="display:none">Loading log...</div>
<div id="viewer" style="display:none">
<div id="pixelarea">
<div id="pixelinfo">
<table>
<tbody>
<tr><th>Pixel at:</th><td colspan="2" id="coords"/></tr>
<tr><th>Image 1:</th><td id="pix1rgb"></td><td id="pix1hex"></td></tr>
<tr><th>Image 2:</th><td id="pix2rgb"></td><td id="pix2hex"></td></tr>
</tbody>
</table>
<div>
<div id="pixelhint">
<div>
<p>Move the mouse over the reftest image on the right to show
magnified pixels on the left. The color information above is for
the pixel centered in the magnified view.</p>
<p>Image 1 is shown in the upper triangle of each pixel and Image 2
is shown in the lower triangle.</p>
</div>
</div>
</div>
</div>
<div id="magnification">
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84" shape-rendering="optimizeSpeed">
<g id="mag"/>
</svg>
</div>
</div>
<div id="itemlist"></div>
<div id="images" style="display:none">
<form id="imgcontrols">
<label><input type="radio" name="which" value="0" onchange="show_image(1)" checked="checked" />Image 1</label>
<label><input type="radio" name="which" value="1" onchange="show_image(2)" />Image 2</label>
<label><input type="checkbox" onchange="show_differences(this)" />Circle differences</label>
</form>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="800px" height="1000px" viewbox="0 0 800 1000" id="svg">
<defs>
<!-- use sRGB to avoid loss of data -->
<filter id="showDifferences" x="0%" y="0%" width="100%" height="100%"
style="color-interpolation-filters: sRGB">
<feImage id="feimage1" result="img1" xlink:href="#image1" />
<feImage id="feimage2" result="img2" xlink:href="#image2" />
<!-- inv1 and inv2 are the images with RGB inverted -->
<feComponentTransfer result="inv1" in="img1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="inv2" in="img2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- w1 will have non-white pixels anywhere that img2
is brighter than img1, and w2 for the reverse.
It would be nice not to have to go through these
intermediate states, but feComposite
type="arithmetic" can't transform the RGB channels
and leave the alpha channel untouched. -->
<feComposite result="w1" in="img1" in2="inv2" operator="arithmetic" k2="1" k3="1" />
<feComposite result="w2" in="img2" in2="inv1" operator="arithmetic" k2="1" k3="1" />
<!-- c1 will have non-black pixels anywhere that img2
is brighter than img1, and c2 for the reverse -->
<feComponentTransfer result="c1" in="w1">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<feComponentTransfer result="c2" in="w2">
<feFuncR type="linear" slope="-1" intercept="1" />
<feFuncG type="linear" slope="-1" intercept="1" />
<feFuncB type="linear" slope="-1" intercept="1" />
</feComponentTransfer>
<!-- c will be nonblack (and fully on) for every pixel+component where there are differences -->
<feComposite result="c" in="c1" in2="c2" operator="arithmetic" k2="255" k3="255" />
<!-- a will be opaque for every pixel with differences and transparent for all others -->
<feColorMatrix result="a" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0" />
<!-- a, dilated by 4 pixels -->
<feMorphology result="dila4" in="a" operator="dilate" radius="4" />
<!-- a, dilated by 1 pixel -->
<feMorphology result="dila1" in="a" operator="dilate" radius="1" />
<!-- all the pixels in the 3-pixel dilation of a but not in the 1-pixel dilation of a, to highlight the diffs -->
<feComposite result="highlight" in="dila4" in2="dila1" operator="out" />
<feFlood result="red" flood-color="red" />
<feComposite result="redhighlight" in="red" in2="highlight" operator="in" />
<feFlood result="black" flood-color="black" flood-opacity="0.5" />
<feMerge>
<feMergeNode in="black" />
<feMergeNode in="redhighlight" />
</feMerge>
</filter>
</defs>
<g onmousemove="magnify(evt)">
<image x="0" y="0" width="100%" height="100%" id="image1" />
<image x="0" y="0" width="100%" height="100%" id="image2" />
</g>
<rect id="diffrect" filter="url(#showDifferences)" pointer-events="none" x="0" y="0" width="100%" height="100%" />
</svg>
</div>
</div>
</body>
</html>

460
test/test.py Normal file
View File

@ -0,0 +1,460 @@
import json, platform, os, shutil, sys, subprocess, tempfile, threading, time, urllib, urllib2
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import SocketServer
from optparse import OptionParser
from urlparse import urlparse
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'
EQLOG_FILE = 'eq.log'
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.add_option("--reftest", action="store_true", dest="reftest",
help="Automatically start reftest showing comparison test failures, if there are any.",
default=False)
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"
if not options.browser and not options.browserManifestFile:
self.error("No test browsers found. Use --browserManifest or --browser args.")
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',
'.js': 'application/javascript',
'.json': 'application/json',
'.pdf': 'application/pdf',
'.xhtml': 'application/xhtml+xml',
'.ico': 'image/x-icon',
'.log': 'text/plain'
}
class State:
browsers = [ ]
manifest = { }
taskResults = { }
remaining = 0
results = { }
done = False
masterMode = False
numErrors = 0
numEqFailures = 0
numEqNoSnapshot = 0
numFBFFailures = 0
numLoadFailures = 0
eqLog = None
class Result:
def __init__(self, snapshot, failure, page):
self.snapshot = snapshot
self.failure = failure
self.page = page
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:
BaseHTTPRequestHandler.log_request(code, size)
def sendFile(self, path, ext):
self.send_response(200)
self.send_header("Content-Type", MIMEs[ext])
self.send_header("Content-Length", os.path.getsize(path))
self.end_headers()
with open(path) as f:
self.wfile.write(f.read())
def do_GET(self):
url = urlparse(self.path)
# Ignore query string
path, _ = url.path, url.query
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 url.path == "/favicon.ico":
self.sendFile(os.path.join(DOC_ROOT, "test", "resources", "favicon.ico"), ext)
return
if not (prefix == DOC_ROOT
and os.path.isfile(path)
and ext in MIMEs):
print path
self.send_error(404)
return
if 'Range' in self.headers:
# TODO for fetch-as-you-go
self.send_error(501)
return
self.sendFile(path, ext)
def do_POST(self):
numBytes = int(self.headers['Content-Length'])
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
result = json.loads(self.rfile.read(numBytes))
browser, id, failure, round, page, snapshot = result['browser'], result['id'], result['failure'], result['round'], result['page'], result['snapshot']
taskResults = State.taskResults[browser][id]
taskResults[round].append(Result(snapshot, failure, page))
def isTaskDone():
numPages = result["numPages"]
rounds = State.manifest[id]["rounds"]
for round in range(0,rounds):
if len(taskResults[round]) < numPages:
return False
return True
if isTaskDone():
# sort the results since they sometimes come in out of order
for results in taskResults:
results.sort(key=lambda result: result.page)
check(State.manifest[id], taskResults, browser)
# Please oh please GC this ...
del State.taskResults[browser][id]
State.remaining -= 1
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"]
self.tempDir = None
self.process = None
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")
shutil.copytree(os.path.join(DOC_ROOT, "test", "resources", "firefox"),
self.profileDir)
def teardown(self):
# If the browser is still running, wait up to ten seconds for it to quit
if self.process and self.process.poll() is None:
checks = 0
while self.process.poll() is None and checks < 20:
checks += 1
time.sleep(.5)
# If it's still not dead, try to kill it
if self.process.poll() is None:
print "Process %s is still running. Killing." % self.name
self.process.kill()
if self.tempDir is not None and os.path.exists(self.tempDir):
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])
self.process = subprocess.Popen(cmds)
def makeBrowserCommands(browserManifestFile):
with open(browserManifestFile) as bmf:
browsers = [BrowserCommand(browser) for browser in json.load(bmf)]
return browsers
def downloadLinkedPDFs(manifestList):
for item in manifestList:
f, isLink = item['file'], item.get('link', False)
if isLink and not os.access(f, os.R_OK):
linkFile = open(f +'.link')
link = linkFile.read()
linkFile.close()
sys.stdout.write('Downloading '+ link +' to '+ f +' ...')
sys.stdout.flush()
response = urllib2.urlopen(link)
with open(f, 'w') as out:
out.write(response.read())
print 'done'
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')
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'):
subprocess.call(( 'rm', '-rf', 'tmp' ))
assert not os.path.isdir(TMPDIR)
testBrowsers = []
if options.browserManifestFile:
testBrowsers = makeBrowserCommands(options.browserManifestFile)
elif options.browser:
testBrowsers = [BrowserCommand({"path":options.browser, "name":"firefox"})]
assert len(testBrowsers) > 0
with open(options.manifestFile) as mf:
manifestList = json.load(mf)
downloadLinkedPDFs(manifestList)
for b in testBrowsers:
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.name][id] = taskResults
State.remaining = len(testBrowsers) * len(manifestList)
return testBrowsers
def startBrowsers(browsers, options):
for b in browsers:
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)
def teardownBrowsers(browsers):
for b in browsers:
try:
b.teardown()
except:
print "Error cleaning up after browser at ", b.path
print "Temp dir was ", b.tempDir
print "Error:", sys.exc_info()[0]
def check(task, results, browser):
failed = False
for r in xrange(len(results)):
pageResults = results[r]
for p in xrange(len(pageResults)):
pageResult = pageResults[p]
if pageResult is None:
continue
failure = pageResult.failure
if failure:
failed = True
State.numErrors += 1
print 'TEST-UNEXPECTED-FAIL | test failed', task['id'], '| in', browser, '| page', p + 1, 'round', r, '|', failure
if failed:
return
kind = task['type']
if 'eq' == kind:
checkEq(task, results, browser)
elif 'fbf' == kind:
checkFBF(task, results, browser)
elif 'load' == kind:
checkLoad(task, results, browser)
else:
assert 0 and 'Unknown test type'
def checkEq(task, results, browser):
pfx = os.path.join(REFDIR, sys.platform, browser, task['id'])
results = results[0]
taskId = task['id']
passed = True
for page in xrange(len(results)):
snapshot = results[page].snapshot
ref = None
eq = True
path = os.path.join(pfx, str(page + 1))
if not os.access(path, os.R_OK):
print 'WARNING: no reference snapshot', path
State.numEqNoSnapshot += 1
else:
f = open(path)
ref = f.read()
f.close()
eq = (ref == snapshot)
if not eq:
print 'TEST-UNEXPECTED-FAIL | eq', taskId, '| in', browser, '| rendering of page', page + 1, '!= reference rendering'
if not State.eqLog:
State.eqLog = open(EQLOG_FILE, 'w')
eqLog = State.eqLog
# NB: this follows the format of Mozilla reftest
# output so that we can reuse its reftest-analyzer
# script
print >>eqLog, 'REFTEST TEST-UNEXPECTED-FAIL |', browser +'-'+ taskId +'-page'+ str(page + 1), '| image comparison (==)'
print >>eqLog, 'REFTEST IMAGE 1 (TEST):', snapshot
print >>eqLog, 'REFTEST IMAGE 2 (REFERENCE):', ref
passed = False
State.numEqFailures += 1
if State.masterMode and (ref is None or not eq):
tmpTaskDir = os.path.join(TMPDIR, sys.platform, browser, task['id'])
try:
os.makedirs(tmpTaskDir)
except OSError, e:
pass
of = open(os.path.join(tmpTaskDir, str(page + 1)), 'w')
of.write(snapshot)
of.close()
if passed:
print 'TEST-PASS | eq test', task['id'], '| in', browser
def checkFBF(task, results, browser):
round0, round1 = results[0], results[1]
assert len(round0) == len(round1)
passed = True
for page in xrange(len(round1)):
r0Page, r1Page = round0[page], round1[page]
if r0Page is None:
break
if r0Page.snapshot != r1Page.snapshot:
print 'TEST-UNEXPECTED-FAIL | forward-back-forward test', task['id'], '| in', browser, '| first rendering of page', page + 1, '!= second'
passed = False
State.numFBFFailures += 1
if passed:
print 'TEST-PASS | forward-back-forward test', task['id'], '| in', browser
def checkLoad(task, results, browser):
# Load just checks for absence of failure, so if we got here the
# test has passed
print 'TEST-PASS | load test', task['id'], '| in', browser
def processResults():
print ''
numErrors, numEqFailures, numEqNoSnapshot, numFBFFailures = State.numErrors, State.numEqFailures, State.numEqNoSnapshot, State.numFBFFailures
numFatalFailures = (numErrors + numFBFFailures)
if 0 == numEqFailures and 0 == numFatalFailures:
print 'All tests passed.'
else:
print 'OHNOES! Some tests failed!'
if 0 < numErrors:
print ' errors:', numErrors
if 0 < numEqFailures:
print ' different ref/snapshot:', numEqFailures
if 0 < numFBFFailures:
print ' different first/second rendering:', numFBFFailures
if State.masterMode and (0 < numEqFailures or 0 < numEqNoSnapshot):
print "Some eq tests failed or didn't have snapshots."
print 'Checking to see if master references can be updated...'
if 0 < numFatalFailures:
print ' No. Some non-eq tests failed.'
else:
' Yes! The references in tmp/ can be synced with ref/.'
if not prompt('Would you like to update the master copy in ref/?'):
print ' OK, not updating.'
else:
sys.stdout.write(' Updating ... ')
# XXX unclear what to do on errors here ...
# NB: do *NOT* pass --delete to rsync. That breaks this
# entire scheme.
subprocess.check_call(( 'rsync', '-arv', 'tmp/', 'ref/' ))
print 'done'
def startReftest(browser):
url = "http://127.0.0.1:8080/test/resources/reftest-analyzer.xhtml"
url += "#web=/test/eq.log"
try:
browser.setup()
browser.start(url)
print "Waiting for browser..."
browser.process.wait()
finally:
teardownBrowsers([browser])
print "Completed reftest usage."
def main():
t1 = time.time()
optionParser = TestOptions()
options, args = optionParser.parse_args()
options = optionParser.verifyOptions(options)
if options == None:
sys.exit(1)
httpd = TestServer(('127.0.0.1', 8080), PDFTestHandler)
httpd_thread = threading.Thread(target=httpd.serve_forever)
httpd_thread.setDaemon(True)
httpd_thread.start()
browsers = setUp(options)
try:
startBrowsers(browsers, options)
while not State.done:
time.sleep(1)
processResults()
finally:
teardownBrowsers(browsers)
t2 = time.time()
print "Runtime was", int(t2 - t1), "seconds"
if options.reftest and State.numEqFailures > 0:
print "\nStarting reftest harness to examine %d eq test failures." % State.numEqFailures
startReftest(browsers[0])
if __name__ == '__main__':
main()

40
test/test_manifest.json Normal file
View File

@ -0,0 +1,40 @@
[
{ "id": "tracemonkey-eq",
"file": "pdfs/tracemonkey.pdf",
"rounds": 1,
"type": "eq"
},
{ "id": "tracemonkey-fbf",
"file": "pdfs/tracemonkey.pdf",
"rounds": 2,
"type": "fbf"
},
{ "id": "html5-canvas-cheat-sheet-load",
"file": "pdfs/canvas.pdf",
"rounds": 1,
"type": "load"
},
{ "id": "intelisa-load",
"file": "pdfs/intelisa.pdf",
"link": true,
"rounds": 1,
"type": "load"
},
{ "id": "pdfspec-load",
"file": "pdfs/pdf.pdf",
"link": true,
"rounds": 1,
"type": "load"
},
{ "id": "sizes",
"file": "pdfs/sizes.pdf",
"rounds": 1,
"type": "eq"
},
{ "id": "openweb-cover",
"file": "pdfs/openweb_tm-PRINT.pdf",
"link": true,
"rounds": 1,
"type": "eq"
}
]

228
test/test_slave.html Normal file
View File

@ -0,0 +1,228 @@
<html>
<head>
<title>pdf.js test slave</title>
<style type="text/css"></style>
<script type="text/javascript" src="/pdf.js"></script>
<script type="text/javascript" src="/fonts.js"></script>
<script type="text/javascript" src="/glyphlist.js"></script>
<script type="application/javascript">
var browser, canvas, currentTask, currentTaskIdx, failure, manifest, numPages, pdfDoc, stdout;
function queryParams() {
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;
}
function load() {
var params = queryParams();
browser = params.browser;
manifestFile = params.manifestFile;
canvas = document.createElement("canvas");
canvas.mozOpaque = true;
stdout = document.getElementById("stdout");
log("Harness thinks this browser is '"+ browser +"'\n");
log("Fetching manifest "+ manifestFile +"...");
var r = new XMLHttpRequest();
r.open("GET", manifestFile, false);
r.onreadystatechange = function(e) {
if (r.readyState == 4) {
log("done\n");
manifest = JSON.parse(r.responseText);
currentTaskIdx = 0, nextTask();
}
};
r.send(null);
}
function nextTask() {
if (currentTaskIdx == manifest.length) {
return done();
}
currentTask = manifest[currentTaskIdx];
currentTask.round = 0;
log("Loading file "+ currentTask.file +"\n");
var r = new XMLHttpRequest();
r.open("GET", currentTask.file);
r.mozResponseType = r.responseType = "arraybuffer";
r.onreadystatechange = function() {
if (r.readyState == 4) {
var data = r.mozResponseArrayBuffer || r.mozResponse ||
r.responseArrayBuffer || r.response;
try {
pdfDoc = new PDFDoc(new Stream(data));
numPages = pdfDoc.numPages;
} catch(e) {
numPages = 1;
failure = 'load PDF doc: '+ e.toString();
}
currentTask.pageNum = 1, nextPage();
}
};
r.send(null);
}
function nextPage() {
if (currentTask.pageNum > numPages) {
if (++currentTask.round < currentTask.rounds) {
log(" Round "+ (1 + currentTask.round) +"\n");
currentTask.pageNum = 1;
} else {
++currentTaskIdx, nextTask();
return;
}
}
failure = '';
log(" loading page "+ currentTask.pageNum +"... ");
var ctx = canvas.getContext("2d");
var fonts = [];
var gfx = null;
try {
gfx = new CanvasGraphics(ctx);
currentPage = pdfDoc.getPage(currentTask.pageNum);
currentPage.compile(gfx, fonts);
} catch(e) {
failure = 'compile: '+ e.toString();
}
try {
// using mediaBox for the canvas size
var wTwips = (currentPage.mediaBox[2] - currentPage.mediaBox[0]);
var hTwips = (currentPage.mediaBox[3] - currentPage.mediaBox[1]);
canvas.width = wTwips * 96.0 / 72.0;
canvas.height = hTwips * 96.0 / 72.0;
clear(ctx);
} catch(e) {
failure = 'page setup: '+ e.toString();
}
var fontLoaderTimer = null;
function checkFontsLoaded() {
try {
if (!FontLoader.bind(fonts)) {
fontLoaderTimer = window.setTimeout(checkFontsLoaded, 10);
return;
}
} catch(e) {
failure = 'fonts: '+ e.toString();
}
snapshotCurrentPage(gfx);
}
if (failure) {
// Skip font loading if there was a failure, since the fonts might
// be in an inconsistent state.
snapshotCurrentPage(gfx);
} else {
checkFontsLoaded();
}
}
function snapshotCurrentPage(gfx) {
log("done, snapshotting... ");
if (!failure) {
try {
currentPage.display(gfx);
} catch(e) {
failure = 'render: '+ e.toString();
}
}
sendTaskResult(canvas.toDataURL("image/png"));
log("done"+ (failure ? " (failed!)" : "") +"\n");
// Set up the next request
backoff = (inFlightRequests > 0) ? inFlightRequests * 10 : 0;
setTimeout(function() {
++currentTask.pageNum, nextPage();
},
backoff
);
}
function quitApp() {
log("Done!");
document.body.innerHTML = "Tests are finished. <h1>CLOSE ME!</h1>";
if (window.SpecialPowers)
SpecialPowers.quitApplication();
else
window.close();
}
function done() {
if (inFlightRequests > 0) {
document.getElementById("inFlightCount").innerHTML = inFlightRequests;
setTimeout(done, 100);
} else {
setTimeout(quitApp, 100);
}
}
var inFlightRequests = 0;
function sendTaskResult(snapshot) {
var result = { browser: browser,
id: currentTask.id,
numPages: numPages,
failure: failure,
file: currentTask.file,
round: currentTask.round,
page: currentTask.pageNum,
snapshot: snapshot };
var r = new XMLHttpRequest();
// (The POST URI is ignored atm.)
r.open("POST", "/submit_task_results", true);
r.setRequestHeader("Content-Type", "application/json");
r.onreadystatechange = function(e) {
if (r.readyState == 4) {
inFlightRequests--;
}
}
document.getElementById("inFlightCount").innerHTML = inFlightRequests++;
r.send(JSON.stringify(result));
}
function clear(ctx) {
ctx.save();
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
/* Auto-scroll if the scrollbar is near the bottom, otherwise do nothing. */
function checkScrolling() {
if ((stdout.scrollHeight - stdout.scrollTop) <= stdout.offsetHeight) {
stdout.scrollTop = stdout.scrollHeight;
}
}
function log(str) {
stdout.innerHTML += str;
checkScrolling();
}
</script>
</head>
<body onload="load();">
<pre style="width:800; height:800; overflow: scroll;"id="stdout"></pre>
<p>Inflight requests: <span id="inFlightCount"></span></p>
</body>
</html>

View File

@ -1,23 +0,0 @@
[
{ "id": "tracemonkey-eq",
"file": "tests/tracemonkey.pdf",
"rounds": 1,
"type": "eq"
},
{ "id": "tracemonkey-fbf",
"file": "tests/tracemonkey.pdf",
"rounds": 2,
"type": "fbf"
},
{ "id": "html5-canvas-cheat-sheet-load",
"file": "tests/canvas.pdf",
"rounds": 1,
"type": "load"
},
{ "id": "pdfspec-load",
"file": "tests/pdf.pdf",
"link": true,
"rounds": 1,
"type": "load"
}
]

View File

@ -1,165 +0,0 @@
<html>
<head>
<title>pdf.js test slave</title>
<script type="text/javascript" src="pdf.js"></script>
<script type="text/javascript" src="fonts.js"></script>
<script type="text/javascript" src="glyphlist.js"></script>
<script type="application/javascript">
var browser, canvas, currentTask, currentTaskIdx, failure, manifest, pdfDoc, stdout;
function queryParams() {
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;
}
function load() {
var params = queryParams();
browser = params.browser;
manifestFile = params.manifestFile;
canvas = document.createElement("canvas");
// 8.5x11in @ 100% ... XXX need something better here
canvas.width = 816;
canvas.height = 1056;
canvas.mozOpaque = true;
stdout = document.getElementById("stdout");
log("Harness thinks this browser is '"+ browser +"'\n");
log("Fetching manifest ...");
var r = new XMLHttpRequest();
r.open("GET", manifestFile, false);
r.onreadystatechange = function(e) {
if (r.readyState == 4) {
log("done\n");
manifest = JSON.parse(r.responseText);
currentTaskIdx = 0, nextTask();
}
};
r.send(null);
}
function nextTask() {
if (currentTaskIdx == manifest.length) {
return done();
}
currentTask = manifest[currentTaskIdx];
currentTask.round = 0;
log("Loading file "+ currentTask.file +"\n");
var r = new XMLHttpRequest();
r.open("GET", currentTask.file);
r.mozResponseType = r.responseType = "arraybuffer";
r.onreadystatechange = function() {
if (r.readyState == 4) {
var data = r.mozResponseArrayBuffer || r.mozResponse ||
r.responseArrayBuffer || r.response;
pdfDoc = new PDFDoc(new Stream(data));
currentTask.pageNum = 1, nextPage();
}
};
r.send(null);
}
function nextPage() {
if (currentTask.pageNum > pdfDoc.numPages) {
if (++currentTask.round < currentTask.rounds) {
log(" Round "+ (1 + currentTask.round) +"\n");
currentTask.pageNum = 1;
} else {
++currentTaskIdx, nextTask();
return;
}
}
failure = '';
log(" drawing page "+ currentTask.pageNum +"...");
var ctx = canvas.getContext("2d");
clear(ctx);
var fonts = [];
var gfx = new CanvasGraphics(ctx);
try {
currentPage = pdfDoc.getPage(currentTask.pageNum);
currentPage.compile(gfx, fonts);
} catch(e) {
failure = 'compile: '+ e.toString();
}
// TODO load fonts
setTimeout(function() {
if (!failure) {
try {
currentPage.display(gfx);
} catch(e) {
failure = 'render: '+ e.toString();
}
}
currentTask.taskDone = (currentTask.pageNum == pdfDoc.numPages
&& (1 + currentTask.round) == currentTask.rounds);
sendTaskResult(canvas.toDataURL("image/png"));
log("done"+ (failure ? " (failed!)" : "") +"\n");
++currentTask.pageNum, nextPage();
},
0
);
}
function done() {
log("Done!\n");
setTimeout(function() {
document.body.innerHTML = "Tests are finished. <h1>CLOSE ME!</h1>";
window.close();
},
100
);
}
function sendTaskResult(snapshot) {
var result = { browser: browser,
id: currentTask.id,
taskDone: currentTask.taskDone,
failure: failure,
file: currentTask.file,
round: currentTask.round,
page: currentTask.pageNum,
snapshot: snapshot };
var r = new XMLHttpRequest();
// (The POST URI is ignored atm.)
r.open("POST", "submit_task_results", false);
r.setRequestHeader("Content-Type", "application/json");
// XXX async
r.send(JSON.stringify(result));
}
function clear(ctx) {
var ctx = canvas.getContext("2d");
ctx.save();
ctx.fillStyle = "rgb(255, 255, 255)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
function log(str) {
stdout.innerHTML += str;
window.scrollTo(0, stdout.getBoundingClientRect().bottom);
}
</script>
</head>
<body onload="load();">
<pre id="stdout"></pre>
</body>
</html>

View File

@ -194,6 +194,8 @@ function readFontIndexData(aStream, aIsByte) {
return aStream.getByte() << 24 | aStream.getByte() << 16 |
aStream.getByte() << 8 | aStream.getByte();
}
error(offsize + " is not a valid offset size");
return null;
};
var offsets = [];
@ -320,12 +322,12 @@ var Type2Parser = function(aFilePath) {
dump(subrs);
// Reading Private Dict
var private = font.get("Private");
log("Reading Private Dict (offset: " + private.offset + " size: " + private.size + ")");
aStream.pos = private.offset;
var priv = font.get("Private");
log("Reading Private Dict (offset: " + priv.offset + " size: " + priv.size + ")");
aStream.pos = priv.offset;
var privateDict = [];
for (var i = 0; i < private.size; i++)
for (var i = 0; i < priv.size; i++)
privateDict.push(aStream.getByte());
dump("private:" + privateDict);
parseAsToken(privateDict, CFFDictPrivateDataMap);
@ -386,7 +388,7 @@ function writeToFile(aBytes, aFilePath) {
var stream = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
stream.init(file, 0x04 | 0x08 | 0x20, 0600, 0);
stream.init(file, 0x04 | 0x08 | 0x20, 600, 0);
var bos = Cc["@mozilla.org/binaryoutputstream;1"]
.createInstance(Ci.nsIBinaryOutputStream);

View File

@ -5,7 +5,9 @@
<script type="text/javascript" src="viewer.js"></script>
<script type="text/javascript" src="pdf.js"></script>
<script type="text/javascript" src="utils/fonts_utils.js"></script>
<script type="text/javascript" src="fonts.js"></script>
<script type="text/javascript" src="crypto.js"></script>
<script type="text/javascript" src="glyphlist.js"></script>
</head>
@ -25,8 +27,7 @@
<div id="viewer">
<!-- Canvas dimensions must be specified in CSS pixels. CSS pixels
are always 96 dpi. 816x1056 is 8.5x11in at 96dpi. -->
<!-- We're rendering here at 1.5x scale. -->
<canvas id="canvas" width="1224" height="1584"></canvas>
<canvas id="canvas" width="816" height="1056" defaultwidth="816" defaultheight="1056"></canvas>
</div>
</body>
</html>

View File

@ -3,11 +3,12 @@
"use strict";
var pdfDocument, canvas, pageDisplay, pageNum, numPages, pageInterval;
var pdfDocument, canvas, pageScale, pageDisplay, pageNum, numPages, pageTimeout;
function load(userInput) {
canvas = document.getElementById("canvas");
canvas.mozOpaque = true;
pageNum = parseInt(queryParams().page) || 1;
pageNum = ("page" in queryParams()) ? parseInt(queryParams().page) : 1;
pageScale = ("scale" in queryParams()) ? parseInt(queryParams().scale) : 1.5;
var fileName = userInput;
if (!userInput) {
fileName = queryParams().file || "compressed.tracemonkey-pldi-09.pdf";
@ -52,13 +53,19 @@ function gotoPage(num) {
}
function displayPage(num) {
window.clearInterval(pageInterval);
window.clearTimeout(pageTimeout);
document.getElementById("pageNumber").value = num;
var t0 = Date.now();
var page = pdfDocument.getPage(pageNum = num);
canvas.width = parseInt(canvas.getAttribute("defaultwidth")) * pageScale;
canvas.height = parseInt(canvas.getAttribute("defaultheight")) * pageScale;
// scale canvas by 2
canvas.width = 2 * page.mediaBox[2];
canvas.hieght = 2 * page.mediaBox[3];
var t1 = Date.now();
var ctx = canvas.getContext("2d");
@ -75,27 +82,11 @@ function displayPage(num) {
page.compile(gfx, fonts);
var t2 = Date.now();
var fontsReady = true;
// 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;
}
new Font(font.name, font.file, font.properties);
fontsReady = false;
}
function delayLoadFont() {
for (var i = 0; i < count; i++) {
if (Fonts[font.name].loading)
function loadFont() {
if (!FontLoader.bind(fonts)) {
pageTimeout = window.setTimeout(loadFont, 10);
return;
}
window.clearInterval(pageInterval);
var t3 = Date.now();
@ -106,12 +97,7 @@ function displayPage(num) {
var infoDisplay = document.getElementById("info");
infoDisplay.innerHTML = "Time to load/compile/fonts/render: "+ (t1 - t0) + "/" + (t2 - t1) + "/" + (t3 - t2) + "/" + (t4 - t3) + " ms";
};
if (fontsReady) {
delayLoadFont();
} else {
pageInterval = setInterval(delayLoadFont, 10);
}
loadFont();
}
function nextPage() {

46
viewer_worker.html Normal file
View File

@ -0,0 +1,46 @@
<html>
<head>
<title>Simple pdf.js page worker viewer</title>
<script type="text/javascript" src="worker_client.js"></script>
<script>
var pdfDoc;
window.onload = function() {
window.canvas = document.getElementById("canvas");
window.ctx = canvas.getContext("2d");
pdfDoc = new WorkerPDFDoc(window.canvas);
pdfDoc.onChangePage = function(numPage) {
document.getElementById("pageNumber").value = numPage;
}
// pdfDoc.open("canvas.pdf", function() {
pdfDoc.open("compressed.tracemonkey-pldi-09.pdf", function() {
document.getElementById("numPages").innerHTML = "/" + pdfDoc.numPages;
})
}
</script>
<link rel="stylesheet" href="viewer.css"></link>
</head>
<body>
<div id="controls">
<input type="file" style="float: right; margin: auto 32px;" onChange="load(this.value.toString());"></input>
<!-- This only opens supported PDFs from the source path...
-- Can we use JSONP to overcome the same-origin restrictions? -->
<button onclick="pdfDoc.prevPage();">Previous</button>
<button onclick="pdfDoc.nextPage();">Next</button>
<input type="text" id="pageNumber" onchange="pdfDoc.showPage(this.value);"
value="1" size="4"></input>
<span id="numPages">--</span>
<span id="info"></span>
</div>
<div id="viewer">
<!-- Canvas dimensions must be specified in CSS pixels. CSS pixels
are always 96 dpi. 816x1056 is 8.5x11in at 96dpi. -->
<!-- We're rendering here at 1.5x scale. -->
<canvas id="canvas" width="1224" height="1584"></canvas>
</div>
</body>
</html>

272
worker_client.js Normal file
View File

@ -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);
}