diff --git a/crypto.js b/crypto.js new file mode 100644 index 000000000..14cc21902 --- /dev/null +++ b/crypto.js @@ -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; +})(); diff --git a/fonts.js b/fonts.js index f00f5c75f..ac06b76ae 100644 --- a/fonts.js +++ b/fonts.js @@ -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). @@ -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: @@ -135,28 +166,12 @@ var Font = (function () { break; } - var data = this.font; Fonts[name] = { data: data, properties: properties, loading: true, cache: Object.create(null) - } - - // Convert data to a string. - var dataStr = ""; - var length = data.length; - for (var i = 0; i < length; ++i) - dataStr += String.fromCharCode(data[i]); - - // Attach the font to the document. If this script is runnig in a worker, - // call `bindWorker`, which sends stuff over to the main thread. - if (typeof window != "undefined") { - this.bindDOM(dataStr); - } else { - this.bindWorker(dataStr); - } - + }; }; function stringToArray(str) { @@ -310,57 +325,60 @@ var Font = (function () { idDeltas + idRangeOffsets + glyphsIds); }; - function createOS2Table() { - var OS2 = stringToArray( - "\x00\x03" + // version - "\x02\x24" + // xAvgCharWidth - "\x01\xF4" + // usWeightClass - "\x00\x05" + // usWidthClass - "\x00\x00" + // fstype - "\x02\x8A" + // ySubscriptXSize - "\x02\xBB" + // ySubscriptYSize - "\x00\x00" + // ySubscriptXOffset - "\x00\x8C" + // ySubscriptYOffset - "\x02\x8A" + // ySuperScriptXSize - "\x02\xBB" + // ySuperScriptYSize - "\x00\x00" + // ySuperScriptXOffset - "\x01\xDF" + // ySuperScriptYOffset - "\x00\x31" + // yStrikeOutSize - "\x01\x02" + // yStrikeOutPosition - "\x00\x00" + // sFamilyClass - "\x02\x00\x06\x03\x00\x00\x00\x00\x00\x00" + // Panose - "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 0-31) - "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 32-63) - "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 64-95) - "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 96-127) - "\x2A\x32\x31\x2A" + // achVendID - "\x00\x20" + // fsSelection - "\x00\x2D" + // usFirstCharIndex - "\x00\x7A" + // usLastCharIndex - "\x00\x03" + // sTypoAscender - "\x00\x20" + // sTypeDescender - "\x00\x38" + // sTypoLineGap - "\x00\x5A" + // usWinAscent - "\x02\xB4" + // usWinDescent - "\x00\xCE\x00\x00" + // ulCodePageRange1 (Bits 0-31) - "\x00\x01\x00\x00" + // ulCodePageRange2 (Bits 32-63) - "\x00\x00" + // sxHeight - "\x00\x00" + // sCapHeight - "\x00\x01" + // usDefaultChar - "\x00\xCD" + // usBreakChar - "\x00\x02" // usMaxContext - ); - return OS2; + function createOS2Table(properties) { + return "\x00\x03" + // version + "\x02\x24" + // xAvgCharWidth + "\x01\xF4" + // usWeightClass + "\x00\x05" + // usWidthClass + "\x00\x00" + // fstype + "\x02\x8A" + // ySubscriptXSize + "\x02\xBB" + // ySubscriptYSize + "\x00\x00" + // ySubscriptXOffset + "\x00\x8C" + // ySubscriptYOffset + "\x02\x8A" + // ySuperScriptXSize + "\x02\xBB" + // ySuperScriptYSize + "\x00\x00" + // ySuperScriptXOffset + "\x01\xDF" + // ySuperScriptYOffset + "\x00\x31" + // yStrikeOutSize + "\x01\x02" + // yStrikeOutPosition + "\x00\x00" + // sFamilyClass + "\x02\x00\x06\x03\x00\x00\x00\x00\x00\x00" + // Panose + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 0-31) + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 32-63) + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 64-95) + "\xFF\xFF\xFF\xFF" + // ulUnicodeRange1 (Bits 96-127) + "\x2A\x32\x31\x2A" + // achVendID + "\x00\x20" + // fsSelection + "\x00\x2D" + // usFirstCharIndex + "\x00\x7A" + // usLastCharIndex + "\x00\x03" + // sTypoAscender + "\x00\x20" + // sTypeDescender + "\x00\x38" + // sTypoLineGap + string16(properties.ascent) + // usWinAscent + string16(properties.descent) + // usWinDescent + "\x00\xCE\x00\x00" + // ulCodePageRange1 (Bits 0-31) + "\x00\x01\x00\x00" + // ulCodePageRange2 (Bits 32-63) + string16(properties.xHeight) + // sxHeight + string16(properties.capHeight) + // sCapHeight + "\x00\x01" + // usDefaultChar + "\x00\xCD" + // usBreakChar + "\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, @@ -418,10 +436,11 @@ var Font = (function () { var length = FontsUtils.bytesToInteger(font.getBytes(2)); var language = FontsUtils.bytesToInteger(font.getBytes(2)); - if ((format == 0 && numTables == 1) || + 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++) { @@ -525,10 +544,9 @@ 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)) }); // Replace the old CMAP table with a shiny new one @@ -536,20 +554,9 @@ var Font = (function () { // 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)) }); } @@ -593,16 +600,16 @@ var Font = (function () { return font.getBytes(); }, - convert: function font_convert(name, font, properties) { + 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 @@ -610,7 +617,7 @@ var Font = (function () { "undefined" // Designer ]; - var name = + var nameTable = "\x00\x00" + // format "\x00\x0A" + // Number of names Record "\x00\x7E"; // Storage @@ -627,21 +634,21 @@ 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 var CFF = - font.data, // PostScript Font Program + 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 @@ -665,13 +672,11 @@ 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 */ + var charstrings = font.charstrings; cmap = createCMapTable(charstrings); createTableEntry(otf, offsets, "cmap", cmap); @@ -720,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); @@ -741,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! @@ -768,96 +767,55 @@ var Font = (function () { return fontData; }, - bindWorker: function font_bind_worker(dataStr) { + bindWorker: function font_bindWorker(data) { postMessage({ action: "font", data: { - raw: dataStr, + raw: data, fontName: this.name, mimetype: this.mimetype } }); }, - bindDOM: function font_bind_dom(dataStr) { + 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"; + ctx.font = "bold italic 20px " + fontName + ", Symbol"; 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; var interval = window.setInterval(function canvasInterval(self) { this.start = this.start || Date.now(); - ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; + ctx.font = "bold italic 20px " + fontName + ", Symbol"; // For some reasons the font has not loaded, so mark it loaded for the // page to proceed but cry 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 */ - - // Convert the data string and add it to the page. - var base64 = window.btoa(dataStr); // Add the @font-face rule to the document - var url = "url(data:" + this.mimetype + ";base64," + base64 + ");"; + 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); @@ -887,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) { @@ -1223,6 +1184,8 @@ var 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"); @@ -1230,17 +1193,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; @@ -1277,18 +1238,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++) { @@ -1301,7 +1262,7 @@ CFF.prototype = { charstrings.push({ glyph: glyph, unicode: unicode, - charstring: glyphs[i].data.slice() + charstring: glyphs[i].data }); } }; @@ -1334,7 +1295,7 @@ CFF.prototype = { "hvcurveto": 31, }, - flattenCharstring: function flattenCharstring(glyph, charstring, subrs) { + flattenCharstring: function flattenCharstring(charstring, subrs) { var i = 0; while (true) { var obj = charstring[i]; @@ -1344,9 +1305,9 @@ CFF.prototype = { if (obj.charAt) { switch (obj) { case "callsubr": - var subr = subrs[charstring[i - 1]].slice(); + var subr = subrs[charstring[i - 1]]; if (subr.length > 1) { - subr = this.flattenCharstring(glyph, subr, subrs); + subr = this.flattenCharstring(subr, subrs); subr.pop(); charstring.splice(i - 1, 2, subr); } else { @@ -1436,23 +1397,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(), subrs)); } // Create a CFF font data @@ -1487,17 +1441,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 = [ @@ -1523,7 +1476,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 @@ -1531,7 +1484,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, @@ -1561,7 +1513,6 @@ CFF.prototype = { 139, 12, 14, 28, 0, 55, 19 ]); - privateData = privateData.join(" ").split(" "); cff.set(privateData, currentOffset); currentOffset += privateData.length; diff --git a/multi_page_viewer.html b/multi_page_viewer.html index 47234686d..649e3a7cc 100644 --- a/multi_page_viewer.html +++ b/multi_page_viewer.html @@ -6,6 +6,7 @@ + diff --git a/multi_page_viewer.js b/multi_page_viewer.js index 3a02ea332..f262734d3 100644 --- a/multi_page_viewer.js +++ b/multi_page_viewer.js @@ -3,6 +3,8 @@ "use strict"; +var pageTimeout; + var PDFViewer = { queryParams: {}, @@ -75,81 +77,45 @@ var PDFViewer = { }, drawPage: function(num) { - if (!PDFViewer.pdf) { + 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(); - + 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); - 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 + var fonts = []; 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; + + var loadFont = function() { + if (!FontLoader.bind(fonts)) { + pageTimeout = window.setTimeout(loadFont, 10); + return; } - - new Font(font.name, font.file, font.properties); - - areFontsReady = false; + page.display(gfx); } - - 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); + loadFont(); } }, @@ -258,7 +224,6 @@ var PDFViewer = { }; window.onload = function() { - // Parse the URL query parameters into a cached object. PDFViewer.queryParams = function() { var qs = window.location.search.substring(1); diff --git a/pdf.js b/pdf.js index 341d8fe06..180d96588 100644 --- a/pdf.js +++ b/pdf.js @@ -56,6 +56,14 @@ function bytesToString(bytes) { return str; } +function stringToBytes(str) { + var length = str.length; + var bytes = new Uint8Array(length); + for (var n = 0; n < length; ++n) + bytes[n] = str.charCodeAt(n) & 0xFF; + return bytes; +} + var Stream = (function() { function constructor(arrayBuffer, start, length, dict) { this.bytes = new Uint8Array(arrayBuffer); @@ -71,14 +79,14 @@ var Stream = (function() { get length() { return this.end - this.start; }, - getByte: function() { + getByte: function stream_getByte() { if (this.pos >= this.end) - return; + return null; return this.bytes[this.pos++]; }, // returns subarray of original buffer // should only be read - getBytes: function(length) { + getBytes: function stream_getBytes(length) { var bytes = this.bytes; var pos = this.pos; var strEnd = this.end; @@ -93,28 +101,28 @@ var Stream = (function() { this.pos = end; return bytes.subarray(pos, end); }, - lookChar: function() { + lookChar: function stream_lookChar() { if (this.pos >= this.end) - return; + return null; return String.fromCharCode(this.bytes[this.pos]); }, - getChar: function() { + getChar: function stream_getChar() { if (this.pos >= this.end) - return; + return null; return String.fromCharCode(this.bytes[this.pos++]); }, - skip: function(n) { + skip: function stream_skip(n) { if (!n) n = 1; this.pos += n; }, - reset: function() { + reset: function stream_reset() { this.pos = this.start; }, - moveStart: function() { + moveStart: function stream_moveStart() { this.start = this.pos; }, - makeSubStream: function(start, length, dict) { + makeSubStream: function stream_makeSubstream(start, length, dict) { return new Stream(this.bytes.buffer, start, length, dict); } }; @@ -146,7 +154,7 @@ var DecodeStream = (function() { } constructor.prototype = { - ensureBuffer: function(requested) { + ensureBuffer: function decodestream_ensureBuffer(requested) { var buffer = this.buffer; var current = buffer ? buffer.byteLength : 0; if (requested < current) @@ -159,16 +167,16 @@ var DecodeStream = (function() { buffer2[i] = buffer[i]; return this.buffer = buffer2; }, - getByte: function() { + getByte: function decodestream_getByte() { var pos = this.pos; while (this.bufferLength <= pos) { if (this.eof) - return; + return null; this.readBlock(); } return this.buffer[this.pos++]; }, - getBytes: function(length) { + getBytes: function decodestream_getBytes(length) { var pos = this.pos; if (length) { @@ -191,25 +199,25 @@ var DecodeStream = (function() { this.pos = end; return this.buffer.subarray(pos, end) }, - lookChar: function() { + lookChar: function decodestream_lookChar() { var pos = this.pos; while (this.bufferLength <= pos) { if (this.eof) - return; + return null; this.readBlock(); } return String.fromCharCode(this.buffer[this.pos]); }, - getChar: function() { + getChar: function decodestream_getChar() { var pos = this.pos; while (this.bufferLength <= pos) { if (this.eof) - return; + return null; this.readBlock(); } return String.fromCharCode(this.buffer[this.pos++]); }, - skip: function(n) { + skip: function decodestream_skip(n) { if (!n) n = 1; this.pos += n; @@ -635,6 +643,7 @@ var PredictorStream = (function() { var rowBytes = this.rowBytes = (columns * colors * bits + 7) >> 3; DecodeStream.call(this); + return this; } constructor.prototype = Object.create(DecodeStream.prototype); @@ -707,7 +716,7 @@ var PredictorStream = (function() { var rawBytes = this.stream.getBytes(rowBytes); var bufferLength = this.bufferLength; - var buffer = this.ensureBuffer(bufferLength + pixBytes); + var buffer = this.ensureBuffer(bufferLength + rowBytes); var currentRow = buffer.subarray(bufferLength, bufferLength + rowBytes); var prevRow = buffer.subarray(bufferLength - rowBytes, bufferLength); @@ -799,11 +808,34 @@ var JpegStream = (function() { return constructor; })(); var DecryptStream = (function() { - function constructor(str, fileKey, encAlgorithm, keyLength) { - TODO("decrypt stream is not implemented"); + function constructor(str, decrypt) { + this.str = str; + this.dict = str.dict; + this.decrypt = decrypt; + + DecodeStream.call(this); } - constructor.prototype = Stream.prototype; + const chunkSize = 512; + + constructor.prototype = Object.create(DecodeStream.prototype); + constructor.prototype.readBlock = function() { + var chunk = this.str.getBytes(chunkSize); + if (!chunk || chunk.length == 0) { + this.eof = true; + return; + } + var decrypt = this.decrypt; + chunk = decrypt(chunk); + + var bufferLength = this.bufferLength; + var i, n = chunk.length; + var buffer = this.ensureBuffer(bufferLength + n); + for (i = 0; i < n; i++) + buffer[bufferLength++] = chunk[i]; + this.bufferLength = bufferLength; + this.eof = n < chunkSize; + }; return constructor; })(); @@ -1883,7 +1915,9 @@ var Dict = (function() { constructor.prototype = { get: function(key) { - return this.map[key]; + if (key in this.map) + return this.map[key]; + return null; }, get2: function(key1, key2) { return this.get(key1) || this.get(key2); @@ -1954,7 +1988,7 @@ function IsArray(v) { } function IsStream(v) { - return typeof v == "object" && "getChar" in v; + return typeof v == "object" && v != null && ("getChar" in v); } function IsRef(v) { @@ -2023,10 +2057,10 @@ var Lexer = (function() { function ToHexDigit(ch) { if (ch >= "0" && ch <= "9") - return ch - "0"; - ch = ch.toLowerCase(); - if (ch >= "a" && ch <= "f") - return ch - "a"; + return ch.charCodeAt(0) - 48; + ch = ch.toUpperCase(); + if (ch >= "A" && ch <= "F") + return ch.charCodeAt(0) - 55; return -1; } @@ -2320,7 +2354,7 @@ var Parser = (function() { // don't buffer inline image data this.buf2 = (this.inlineImg > 0) ? null : this.lexer.getObj(); }, - getObj: function() { + getObj: function(cipherTransform) { // refill buffer after inline image data if (this.inlineImg == 2) this.refill(); @@ -2346,7 +2380,7 @@ var Parser = (function() { this.shift(); if (IsEOF(this.buf1)) break; - dict.set(key, this.getObj()); + dict.set(key, this.getObj(cipherTransform)); } } if (IsEOF(this.buf1)) @@ -2355,7 +2389,7 @@ var Parser = (function() { // stream objects are not allowed inside content streams or // object streams if (this.allowStreams && IsCmd(this.buf2, "stream")) { - return this.makeStream(dict); + return this.makeStream(dict, cipherTransform); } else { this.shift(); } @@ -2374,17 +2408,8 @@ var Parser = (function() { } else if (IsString(this.buf1)) { // string var str = this.buf1; this.shift(); - if (this.fileKey) { - var decrypt = new DecryptStream(new StringStream(str), - this.fileKey, - this.encAlgorithm, - this.keyLength); - var str = ""; - var pos = decrypt.pos; - var length = decrypt.length; - while (pos++ > length) - str += decrypt.getChar(); - } + if (cipherTransform) + str = cipherTransform.decryptString(str); return str; } @@ -2393,7 +2418,7 @@ var Parser = (function() { this.shift(); return obj; }, - makeStream: function(dict) { + makeStream: function(dict, cipherTransform) { var lexer = this.lexer; var stream = lexer.stream; @@ -2420,12 +2445,8 @@ var Parser = (function() { this.shift(); stream = stream.makeSubStream(pos, length, dict); - if (this.fileKey) { - stream = new DecryptStream(stream, - this.fileKey, - this.encAlgorithm, - this.keyLength); - } + if (cipherTransform) + stream = cipherTransform.createStream(stream); stream = this.filter(stream, dict, length); stream.parameters = dict; return stream; @@ -2559,16 +2580,22 @@ var XRef = (function() { this.xrefstms = {}; var trailerDict = this.readXRef(startXRef); + // prepare the XRef cache + this.cache = []; + + var encrypt = trailerDict.get("Encrypt"); + if (encrypt) { + var fileId = trailerDict.get("ID"); + this.encrypt = new CipherTransformFactory(this.fetch(encrypt), fileId[0] /*, password */); + } + // get the root dictionary (catalog) object if (!IsRef(this.root = trailerDict.get("Root"))) error("Invalid root reference"); - - // prepare the XRef cache - this.cache = []; } constructor.prototype = { - readXRefTable: function(parser) { + readXRefTable: function readXRefTable(parser) { var obj; while (true) { if (IsCmd(obj = parser.getObj(), "trailer")) @@ -2639,7 +2666,7 @@ var XRef = (function() { return dict; }, - readXRefStream: function(stream) { + readXRefStream: function readXRefStream(stream) { var streamParameters = stream.parameters; var length = streamParameters.get("Length"); var byteWidths = streamParameters.get("W"); @@ -2691,7 +2718,7 @@ var XRef = (function() { this.readXRef(prev); return streamParameters; }, - readXRef: function(startXRef) { + readXRef: function readXref(startXRef) { var stream = this.stream; stream.pos = startXRef; var parser = new Parser(new Lexer(stream), true); @@ -2709,6 +2736,7 @@ var XRef = (function() { return this.readXRefStream(obj); } error("Invalid XRef"); + return null; }, getEntry: function(i) { var e = this.entries[i]; @@ -2752,7 +2780,11 @@ var XRef = (function() { } error("bad XRef entry"); } - e = parser.getObj(); + if (this.encrypt) { + e = parser.getObj(this.encrypt.createCipherTransform(num, gen)); + } else { + e = parser.getObj(); + } // Don't cache streams since they are mutable. if (!IsStream(e)) this.cache[num] = e; @@ -3374,7 +3406,7 @@ var CanvasGraphics = (function() { if (!fd) // XXX deprecated "special treatment" for standard // fonts? What do we need to do here? - return; + return null; var descriptor = xref.fetch(fd); var fontName = descriptor.get("FontName"); @@ -3386,16 +3418,9 @@ var CanvasGraphics = (function() { error("FontFile not found for font: " + fontName); fontFile = xref.fetchIfRef(fontFile); - // Fonts with an embedded cmap but without any assignment in - // it are not yet supported, so ask the fonts loader to ignore - // them to not pay a stupid one sec latence. - var ignoreFont = false; - var encodingMap = {}; var charset = []; if (fontDict.has("Encoding")) { - ignoreFont = false; - var encoding = xref.fetchIfRef(fontDict.get("Encoding")); if (IsDict(encoding)) { // Build a map between codes and glyphs @@ -3418,9 +3443,8 @@ var CanvasGraphics = (function() { error("Unknown font encoding"); var index = 0; - for (var j = 0; j < encoding.length; j++) { + for (var j = 0; j < encoding.length; j++) encodingMap[index++] = GlyphsUnicode[encoding[j]]; - } var firstChar = xref.fetchIfRef(fontDict.get("FirstChar")); var widths = xref.fetchIfRef(fontDict.get("Widths")); @@ -3444,13 +3468,7 @@ var CanvasGraphics = (function() { var tokens = []; var token = ""; - var length = cmapObj.length; - if (cmapObj instanceof FlateStream) { - cmapObj.readBlock(); - length = cmapObj.bufferLength; - } - - var cmap = cmapObj.getBytes(length); + var cmap = cmapObj.getBytes(cmapObj.length); for (var i =0; i < cmap.length; i++) { var byte = cmap[i]; if (byte == 0x20 || byte == 0x0A || byte == 0x3C || byte == 0x3E) { @@ -3460,7 +3478,6 @@ var CanvasGraphics = (function() { break; case "beginbfrange": - ignoreFont = false; case "begincodespacerange": token = ""; tokens = []; @@ -3507,16 +3524,19 @@ var CanvasGraphics = (function() { } var subType = fontDict.get("Subtype"); - var bbox = descriptor.get("FontBBox"); - assertWellFormed(IsName(subType) && IsArray(bbox), - "invalid font Subtype or FontBBox"); + assertWellFormed(IsName(subType), "invalid font Subtype"); var properties = { type: subType.name, encoding: encodingMap, charset: charset, - bbox: bbox, - ignore: ignoreFont + bbox: descriptor.get("FontBBox"), + ascent: descriptor.get("Ascent"), + descent: descriptor.get("Descent"), + xHeight: descriptor.get("XHeight"), + capHeight: descriptor.get("CapHeight"), + flags: descriptor.get("Flags"), + italicAngle: descriptor.get("ItalicAngle") }; return { @@ -4027,7 +4047,7 @@ var CanvasGraphics = (function() { this.restore(); TODO("Inverse pattern is painted"); - var pattern = this.ctx.createPattern(tmpCanvas, "repeat"); + pattern = this.ctx.createPattern(tmpCanvas, "repeat"); this.ctx.fillStyle = pattern; }, setStrokeGray: function(gray) { diff --git a/test/pdfs/intelisa.pdf.link b/test/pdfs/intelisa.pdf.link new file mode 100644 index 000000000..371cdf947 --- /dev/null +++ b/test/pdfs/intelisa.pdf.link @@ -0,0 +1 @@ +http://www.intel.com/Assets/PDF/manual/253665.pdf \ No newline at end of file diff --git a/test/resources/favicon.ico b/test/resources/favicon.ico new file mode 100644 index 000000000..d44438903 Binary files /dev/null and b/test/resources/favicon.ico differ diff --git a/test/resources/firefox/user.js b/test/resources/firefox/user.js index d4b9d4130..b01e2eb76 100644 --- a/test/resources/firefox/user.js +++ b/test/resources/firefox/user.js @@ -32,3 +32,5 @@ 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 diff --git a/test/test.py b/test/test.py index 53f65f78b..5aece2c24 100644 --- a/test/test.py +++ b/test/test.py @@ -1,4 +1,4 @@ -import json, platform, os, shutil, sys, subprocess, tempfile, threading, urllib, urllib2 +import json, platform, os, shutil, sys, subprocess, tempfile, threading, time, urllib, urllib2 from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import SocketServer from optparse import OptionParser @@ -51,6 +51,7 @@ MIMEs = { '.json': 'application/json', '.pdf': 'application/pdf', '.xhtml': 'application/xhtml+xml', + '.ico': 'image/x-icon' } class State: @@ -69,9 +70,10 @@ class State: eqLog = None class Result: - def __init__(self, snapshot, failure): + def __init__(self, snapshot, failure, page): self.snapshot = snapshot self.failure = failure + self.page = page class TestServer(SocketServer.TCPServer): allow_reuse_address = True @@ -83,6 +85,14 @@ class PDFTestHandler(BaseHTTPRequestHandler): 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 @@ -91,9 +101,14 @@ class PDFTestHandler(BaseHTTPRequestHandler): 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 @@ -102,14 +117,8 @@ class PDFTestHandler(BaseHTTPRequestHandler): 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() + self.sendFile(path, ext) + def do_POST(self): @@ -122,10 +131,20 @@ class PDFTestHandler(BaseHTTPRequestHandler): 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 + taskResults[round].append(Result(snapshot, failure, page)) - if result['taskDone']: + 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] @@ -138,6 +157,8 @@ 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() @@ -151,19 +172,30 @@ class BrowserCommand(): def setup(self): self.tempDir = tempfile.mkdtemp() self.profileDir = os.path.join(self.tempDir, "profile") - print self.profileDir shutil.copytree(os.path.join(DOC_ROOT, "test", "resources", "firefox"), self.profileDir) def teardown(self): - shutil.rmtree(self.tempDir) + # 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]) - subprocess.call(cmds) + self.process = subprocess.Popen(cmds) def makeBrowserCommands(browserManifestFile): with open(browserManifestFile) as bmf: @@ -223,14 +255,23 @@ def setUp(options): State.remaining = len(testBrowsers) * len(manifestList) - for b in testBrowsers: + 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.setup() - print 'Launching', b.name - qs = 'browser='+ urllib.quote(b.name) +'&manifestFile='+ urllib.quote(options.manifestFile) - b.start('http://localhost:8080/test/test_slave.html?'+ qs) - finally: b.teardown() + 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 @@ -385,8 +426,14 @@ def main(): httpd_thread.setDaemon(True) httpd_thread.start() - setUp(options) - processResults() + browsers = setUp(options) + try: + startBrowsers(browsers, options) + while not State.done: + time.sleep(1) + processResults() + finally: + teardownBrowsers(browsers) if __name__ == '__main__': main() diff --git a/test/test_manifest.json b/test/test_manifest.json index e4a7ada81..9b9d5e333 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -14,6 +14,12 @@ "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, diff --git a/test/test_slave.html b/test/test_slave.html index 1053025e1..32076d075 100644 --- a/test/test_slave.html +++ b/test/test_slave.html @@ -6,7 +6,7 @@ + diff --git a/viewer.js b/viewer.js index 41aaf354c..c7be739bc 100644 --- a/viewer.js +++ b/viewer.js @@ -3,7 +3,7 @@ "use strict"; -var pdfDocument, canvas, pageDisplay, pageNum, numPages, pageInterval; +var pdfDocument, canvas, pageDisplay, pageNum, numPages, pageTimeout; function load(userInput) { canvas = document.getElementById("canvas"); canvas.mozOpaque = true; @@ -52,7 +52,7 @@ function gotoPage(num) { } function displayPage(num) { - window.clearInterval(pageInterval); + window.clearTimeout(pageTimeout); document.getElementById("pageNumber").value = num; @@ -75,28 +75,12 @@ 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; + function loadFont() { + if (!FontLoader.bind(fonts)) { + pageTimeout = window.setTimeout(loadFont, 10); + return; } - new Font(font.name, font.file, font.properties); - fontsReady = false; - } - - function delayLoadFont() { - for (var i = 0; i < count; i++) { - if (Fonts[font.name].loading) - return; - } - window.clearInterval(pageInterval); - var t3 = Date.now(); page.display(gfx); @@ -106,12 +90,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() {