diff --git a/canvas_proxy.js b/canvas_proxy.js new file mode 100644 index 000000000..d6f5a0a25 --- /dev/null +++ b/canvas_proxy.js @@ -0,0 +1,250 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +var JpegStreamProxyCounter = 0; +// WebWorker Proxy for JpegStream. +var JpegStreamProxy = (function() { + function constructor(bytes, dict) { + this.id = JpegStreamProxyCounter++; + this.dict = dict; + + // Tell the main thread to create an image. + postMessage({ + action: "jpeg_stream", + data: { + id: this.id, + raw: bytesToString(bytes) + } + }); + } + + constructor.prototype = { + getImage: function() { + return this; + }, + getChar: function() { + error("internal error: getChar is not valid on JpegStream"); + } + }; + + return constructor; +})(); + +// Really simple GradientProxy. There is currently only one active gradient at +// the time, meaning you can't create a gradient, create a second one and then +// use the first one again. As this isn't used in pdf.js right now, it's okay. +function GradientProxy(cmdQueue, x0, y0, x1, y1) { + cmdQueue.push(["$createLinearGradient", [x0, y0, x1, y1]]); + this.addColorStop = function(i, rgba) { + cmdQueue.push(["$addColorStop", [i, rgba]]); + } +} + +// Really simple PatternProxy. +var patternProxyCounter = 0; +function PatternProxy(cmdQueue, object, kind) { + this.id = patternProxyCounter++; + + if (!(object instanceof CanvasProxy) ) { + throw "unkown type to createPattern"; + } + + // Flush the object here to ensure it's available on the main thread. + // TODO: Make some kind of dependency management, such that the object + // gets flushed only if needed. + object.flush(); + cmdQueue.push(["$createPatternFromCanvas", [this.id, object.id, kind]]); +} + +var canvasProxyCounter = 0; +function CanvasProxy(width, height) { + this.id = canvasProxyCounter++; + + // The `stack` holds the rendering calls and gets flushed to the main thead. + var cmdQueue = this.cmdQueue = []; + + // Dummy context that gets exposed. + var ctx = {}; + this.getContext = function(type) { + if (type != "2d") { + throw "CanvasProxy can only provide a 2d context."; + } + return ctx; + } + + // Expose only the minimum of the canvas object - there is no dom to do + // more here. + this.width = width; + this.height = height; + ctx.canvas = this; + + // Setup function calls to `ctx`. + var ctxFunc = [ + "createRadialGradient", + "arcTo", + "arc", + "fillText", + "strokeText", + "createImageData", + "drawWindow", + "save", + "restore", + "scale", + "rotate", + "translate", + "transform", + "setTransform", + "clearRect", + "fillRect", + "strokeRect", + "beginPath", + "closePath", + "moveTo", + "lineTo", + "quadraticCurveTo", + "bezierCurveTo", + "rect", + "fill", + "stroke", + "clip", + "measureText", + "isPointInPath", + + // These functions are necessary to track the rendering currentX state. + // The exact values can be computed on the main thread only, as the + // worker has no idea about text width. + "$setCurrentX", + "$addCurrentX", + "$saveCurrentX", + "$restoreCurrentX", + "$showText" + ]; + + function buildFuncCall(name) { + return function() { + // console.log("funcCall", name) + cmdQueue.push([name, Array.prototype.slice.call(arguments)]); + } + } + var name; + for (var i = 0; i < ctxFunc.length; i++) { + name = ctxFunc[i]; + ctx[name] = buildFuncCall(name); + } + + // Some function calls that need more work. + + ctx.createPattern = function(object, kind) { + return new PatternProxy(cmdQueue, object, kind); + } + + ctx.createLinearGradient = function(x0, y0, x1, y1) { + return new GradientProxy(cmdQueue, x0, y0, x1, y1); + } + + ctx.getImageData = function(x, y, w, h) { + return { + width: w, + height: h, + data: Uint8ClampedArray(w * h * 4) + }; + } + + ctx.putImageData = function(data, x, y, width, height) { + cmdQueue.push(["$putImageData", [data, x, y, width, height]]); + } + + ctx.drawImage = function(image, x, y, width, height, sx, sy, swidth, sheight) { + if (image instanceof CanvasProxy) { + // Send the image/CanvasProxy to the main thread. + image.flush(); + cmdQueue.push(["$drawCanvas", [image.id, x, y, sx, sy, swidth, sheight]]); + } else if(image instanceof JpegStreamProxy) { + cmdQueue.push(["$drawImage", [image.id, x, y, sx, sy, swidth, sheight]]) + } else { + throw "unkown type to drawImage"; + } + } + + // Setup property access to `ctx`. + var ctxProp = { + // "canvas" + "globalAlpha": "1", + "globalCompositeOperation": "source-over", + "strokeStyle": "#000000", + "fillStyle": "#000000", + "lineWidth": "1", + "lineCap": "butt", + "lineJoin": "miter", + "miterLimit": "10", + "shadowOffsetX": "0", + "shadowOffsetY": "0", + "shadowBlur": "0", + "shadowColor": "rgba(0, 0, 0, 0)", + "font": "10px sans-serif", + "textAlign": "start", + "textBaseline": "alphabetic", + "mozTextStyle": "10px sans-serif", + "mozImageSmoothingEnabled": "true" + } + + function buildGetter(name) { + return function() { + return ctx["$" + name]; + } + } + + function buildSetter(name) { + return function(value) { + cmdQueue.push(["$", name, value]); + return ctx["$" + name] = value; + } + } + + // Setting the value to `stroke|fillStyle` needs special handling, as it + // might gets an gradient/pattern. + function buildSetterStyle(name) { + return function(value) { + if (value instanceof GradientProxy) { + cmdQueue.push(["$" + name + "Gradient"]); + } else if (value instanceof PatternProxy) { + cmdQueue.push(["$" + name + "Pattern", [value.id]]); + } else { + cmdQueue.push(["$", name, value]); + return ctx["$" + name] = value; + } + } + } + + for (var name in ctxProp) { + ctx["$" + name] = ctxProp[name]; + ctx.__defineGetter__(name, buildGetter(name)); + + // Special treatment for `fillStyle` and `strokeStyle`: The passed style + // might be a gradient. Need to check for that. + if (name == "fillStyle" || name == "strokeStyle") { + ctx.__defineSetter__(name, buildSetterStyle(name)); + } else { + ctx.__defineSetter__(name, buildSetter(name)); + } + } +} + +/** +* Sends the current cmdQueue of the CanvasProxy over to the main thread and +* resets the cmdQueue. +*/ +CanvasProxy.prototype.flush = function() { + postMessage({ + action: "canvas_proxy_cmd_queue", + data: { + id: this.id, + cmdQueue: this.cmdQueue, + width: this.width, + height: this.height + } + }); + this.cmdQueue.length = 0; +} diff --git a/fonts.js b/fonts.js index ad3d4fd35..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). @@ -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)); @@ -274,7 +302,7 @@ var Font = (function () { var bias = 0; for (var i = 0; i < segCount - 1; i++) { var range = ranges[i]; - var start = range[0]; + var start = range[0]; var end = range[1]; var delta = (((start - 1) - bias) ^ 0xffff) + 1; bias += (end - start + 1); @@ -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,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, @@ -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,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 @@ -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,54 +767,28 @@ 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); - } + ctx.font = "bold italic 20px " + fontName + ", Symbol"; + var testString = " "; // Periodicaly check for the width of the testString, it will be // different once the real font has loaded @@ -750,37 +796,26 @@ var Font = (function () { 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 */ - // 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) { @@ -1088,7 +1126,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 +1184,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 +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; @@ -1200,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++) { @@ -1224,7 +1262,7 @@ CFF.prototype = { charstrings.push({ glyph: glyph, unicode: unicode, - charstring: glyphs[i].data.slice() + charstring: glyphs[i].data }); } }; @@ -1257,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]; @@ -1267,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 { @@ -1359,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 @@ -1410,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 = [ @@ -1446,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 @@ -1454,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, @@ -1484,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/images/buttons.png b/images/buttons.png index 682212660..3357b47d6 100644 Binary files a/images/buttons.png and b/images/buttons.png differ diff --git a/images/source/FileButton.psd.zip b/images/source/FileButton.psd.zip new file mode 100644 index 000000000..1f2b51cee Binary files /dev/null and b/images/source/FileButton.psd.zip differ diff --git a/multi-page-viewer.css b/multi-page-viewer.css deleted file mode 100644 index f9a7837b1..000000000 --- a/multi-page-viewer.css +++ /dev/null @@ -1,146 +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; -} - -#pageNumber { - text-align: right; -} - -#viewer { - margin: 44px 0px 0px; - padding: 8px 0px; -} diff --git a/multi-page-viewer.html b/multi-page-viewer.html deleted file mode 100644 index 4e15cf4f8..000000000 --- a/multi-page-viewer.html +++ /dev/null @@ -1,39 +0,0 @@ - - - -pdf.js Multi-Page Viewer - - - - - - - - -
- - - - Previous/Next - - - - / - -- - Page Number - - - - Zoom - -
-
- - diff --git a/multi-page-viewer.js b/multi-page-viewer.js deleted file mode 100644 index 9d9cec702..000000000 --- a/multi-page-viewer.js +++ /dev/null @@ -1,404 +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, - - 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); - } - }, - - open: 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.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); - } - - PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? - 'disabled' : ''; - PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? - 'disabled' : ''; - } - }; - - req.send(null); - } -}; - -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)); - }; - - PDFViewer.pageNumber = parseInt(PDFViewer.queryParams.page) || PDFViewer.pageNumber; - PDFViewer.scale = parseInt(PDFViewer.scaleSelect.value) / 100 || 1.0; - - PDFViewer.open(PDFViewer.queryParams.file || PDFViewer.url); - - window.onscroll = function(evt) { - var lastPagesDrawn = PDFViewer.lastPagesDrawn; - var visiblePages = PDFViewer.visiblePages(); - - var pagesToDraw = []; - var pagesToKeep = []; - var pagesToRemove = []; - - var i; - - // Determine which visible pages were not previously drawn. - for (i = 0; i < visiblePages.length; i++) { - if (lastPagesDrawn.indexOf(visiblePages[i]) === -1) { - pagesToDraw.push(visiblePages[i]); - PDFViewer.drawPage(visiblePages[i]); - } else { - pagesToKeep.push(visiblePages[i]); - } - } - - // Determine which previously drawn pages are no longer visible. - for (i = 0; i < lastPagesDrawn.length; i++) { - if (visiblePages.indexOf(lastPagesDrawn[i]) === -1) { - pagesToRemove.push(lastPagesDrawn[i]); - PDFViewer.removePage(lastPagesDrawn[i]); - } - } - - PDFViewer.lastPagesDrawn = pagesToDraw.concat(pagesToKeep); - - // Update the page number input with the current page number. - if (!PDFViewer.willJumpToPage && visiblePages.length > 0) { - PDFViewer.pageNumber = PDFViewer.pageNumberInput.value = visiblePages[0]; - PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? - 'disabled' : ''; - PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? - 'disabled' : ''; - } else { - PDFViewer.willJumpToPage = false; - } - }; -}; diff --git a/multi_page_viewer.css b/multi_page_viewer.css new file mode 100644 index 000000000..b3eaab792 --- /dev/null +++ b/multi_page_viewer.css @@ -0,0 +1,197 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +body { + background-color: #929292; + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Helvetica, Arial, Verdana, sans-serif; + margin: 0px; + padding: 0px; +} + +canvas { + box-shadow: 0px 4px 10px #000; + -moz-box-shadow: 0px 4px 10px #000; + -webkit-box-shadow: 0px 4px 10px #000; +} + +span { + font-size: 0.8em; +} + +.control { + display: inline-block; + float: left; + margin: 0px 20px 0px 0px; + padding: 0px 4px 0px 0px; +} + +.control > input { + float: left; + border: 1px solid #4d4d4d; + height: 20px; + padding: 0px; + margin: 0px 2px 0px 0px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); +} + +.control > select { + float: left; + border: 1px solid #4d4d4d; + height: 22px; + padding: 2px 0px 0px; + margin: 0px 0px 1px; + border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -moz-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); + -webkit-box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.25); +} + +.control > span { + cursor: default; + float: left; + height: 18px; + margin: 5px 2px 0px; + padding: 0px; + user-select: none; + -moz-user-select: none; + -webkit-user-select: none; +} + +.control .label { + clear: both; + float: left; + font-size: 0.65em; + margin: 2px 0px 0px; + position: relative; + text-align: center; + width: 100%; +} + +.page { + width: 816px; + height: 1056px; + margin: 10px auto; +} + +#controls { + background-color: #eee; + border-bottom: 1px solid #666; + padding: 4px 0px 0px 8px; + position: fixed; + left: 0px; + top: 0px; + height: 40px; + width: 100%; + box-shadow: 0px 2px 8px #000; + -moz-box-shadow: 0px 2px 8px #000; + -webkit-box-shadow: 0px 2px 8px #000; +} + +#controls input { + user-select: text; + -moz-user-select: text; + -webkit-user-select: text; +} + +#previousPageButton { + background: url('images/buttons.png') no-repeat 0px -23px; + cursor: default; + display: inline-block; + float: left; + margin: 0px; + width: 28px; + height: 23px; +} + +#previousPageButton.down { + background: url('images/buttons.png') no-repeat 0px -46px; +} + +#previousPageButton.disabled { + background: url('images/buttons.png') no-repeat 0px 0px; +} + +#nextPageButton { + background: url('images/buttons.png') no-repeat -28px -23px; + cursor: default; + display: inline-block; + float: left; + margin: 0px; + width: 28px; + height: 23px; +} + +#nextPageButton.down { + background: url('images/buttons.png') no-repeat -28px -46px; +} + +#nextPageButton.disabled { + background: url('images/buttons.png') no-repeat -28px 0px; +} + +#openFileButton { + background: url('images/buttons.png') no-repeat -56px -23px; + cursor: default; + display: inline-block; + float: left; + margin: 0px 0px 0px 3px; + width: 29px; + height: 23px; +} + +#openFileButton.down { + background: url('images/buttons.png') no-repeat -56px -46px; +} + +#openFileButton.disabled { + background: url('images/buttons.png') no-repeat -56px 0px; +} + +#fileInput { + display: none; +} + +#pageNumber { + text-align: right; +} + +#sidebar { + background-color: rgba(0, 0, 0, 0.8); + position: fixed; + width: 150px; + top: 62px; + bottom: 18px; + border-top-right-radius: 8px; + border-bottom-right-radius: 8px; + -moz-border-radius-topright: 8px; + -moz-border-radius-bottomright: 8px; + -webkit-border-top-right-radius: 8px; + -webkit-border-bottom-right-radius: 8px; +} + +#sidebarScrollView { + position: absolute; + overflow: hidden; + overflow-y: auto; + top: 40px; + right: 10px; + bottom: 10px; + left: 10px; +} + +#sidebarContentView { + height: auto; + width: 100px; +} + +#viewer { + margin: 44px 0px 0px; + padding: 8px 0px; +} diff --git a/multi_page_viewer.html b/multi_page_viewer.html new file mode 100644 index 000000000..47234686d --- /dev/null +++ b/multi_page_viewer.html @@ -0,0 +1,51 @@ + + + +pdf.js Multi-Page Viewer + + + + + + + + +
+ + + + Previous/Next + + + + / + -- + Page Number + + + + Zoom + + + + + Open File + +
+ +
+ + diff --git a/multi_page_viewer.js b/multi_page_viewer.js new file mode 100644 index 000000000..f262734d3 --- /dev/null +++ b/multi_page_viewer.js @@ -0,0 +1,423 @@ +/* -*- 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, + + 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()) { + 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); + + // 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.createPage(i); + } + + if (PDFViewer.numberOfPages > 0) { + PDFViewer.drawPage(1); + } + } + + for (i = 0; i < PDFViewer.scaleSelect.childNodes; i++) { + var option = PDFViewer.scaleSelect.childNodes[i]; + + if (option.value == num) { + if (!option.selected) { + option.selected = 'selected'; + } + } else { + if (option.selected) { + option.removeAttribute('selected'); + } + } + } + + PDFViewer.scaleSelect.value = Math.floor(PDFViewer.scale * 100) + '%'; + }, + + goToPage: function(num) { + if (1 <= num && num <= PDFViewer.numberOfPages) { + PDFViewer.pageNumber = num; + PDFViewer.pageNumberInput.value = PDFViewer.pageNumber; + PDFViewer.willJumpToPage = true; + + document.location.hash = PDFViewer.pageNumber; + + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? 'disabled' : ''; + } + }, + + goToPreviousPage: function() { + if (PDFViewer.pageNumber > 1) { + PDFViewer.goToPage(--PDFViewer.pageNumber); + } + }, + + goToNextPage: function() { + if (PDFViewer.pageNumber < PDFViewer.numberOfPages) { + PDFViewer.goToPage(++PDFViewer.pageNumber); + } + }, + + openURL: function(url) { + PDFViewer.url = url; + document.title = url; + + var req = new XMLHttpRequest(); + req.open('GET', url); + req.mozResponseType = req.responseType = 'arraybuffer'; + req.expected = (document.URL.indexOf('file:') === 0) ? 0 : 200; + + req.onreadystatechange = function() { + if (req.readyState === 4 && req.status === req.expected) { + var data = req.mozResponseArrayBuffer || req.mozResponse || req.responseArrayBuffer || req.response; + + PDFViewer.readPDF(data); + } + }; + + req.send(null); + }, + + readPDF: function(data) { + while (PDFViewer.element.hasChildNodes()) { + PDFViewer.element.removeChild(PDFViewer.element.firstChild); + } + + PDFViewer.pdf = new PDFDoc(new Stream(data)); + PDFViewer.numberOfPages = PDFViewer.pdf.numPages; + document.getElementById('numPages').innerHTML = PDFViewer.numberOfPages.toString(); + + for (var i = 1; i <= PDFViewer.numberOfPages; i++) { + PDFViewer.createPage(i); + } + + if (PDFViewer.numberOfPages > 0) { + PDFViewer.drawPage(1); + document.location.hash = 1; + } + + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? 'disabled' : ''; + } +}; + +window.onload = function() { + // Parse the URL query parameters into a cached object. + PDFViewer.queryParams = function() { + var qs = window.location.search.substring(1); + var kvs = qs.split('&'); + var params = {}; + + for (var i = 0; i < kvs.length; ++i) { + var kv = kvs[i].split('='); + params[unescape(kv[0])] = unescape(kv[1]); + } + + return params; + }(); + + PDFViewer.element = document.getElementById('viewer'); + + PDFViewer.pageNumberInput = document.getElementById('pageNumber'); + PDFViewer.pageNumberInput.onkeydown = function(evt) { + var charCode = evt.charCode || evt.keyCode; + + // Up arrow key. + if (charCode === 38) { + PDFViewer.goToNextPage(); + this.select(); + } + + // Down arrow key. + else if (charCode === 40) { + PDFViewer.goToPreviousPage(); + this.select(); + } + + // All other non-numeric keys (excluding Left arrow, Right arrow, + // Backspace, and Delete keys). + else if ((charCode < 48 || charCode > 57) && + charCode !== 8 && // Backspace + charCode !== 46 && // Delete + charCode !== 37 && // Left arrow + charCode !== 39 // Right arrow + ) { + return false; + } + + return true; + }; + PDFViewer.pageNumberInput.onkeyup = function(evt) { + var charCode = evt.charCode || evt.keyCode; + + // All numeric keys, Backspace, and Delete. + if ((charCode >= 48 && charCode <= 57) || + charCode === 8 || // Backspace + charCode === 46 // Delete + ) { + PDFViewer.goToPage(this.value); + } + + this.focus(); + }; + + PDFViewer.previousPageButton = document.getElementById('previousPageButton'); + PDFViewer.previousPageButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.goToPreviousPage(); + } + }; + PDFViewer.previousPageButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + PDFViewer.previousPageButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + PDFViewer.previousPageButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.nextPageButton = document.getElementById('nextPageButton'); + PDFViewer.nextPageButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.goToNextPage(); + } + }; + PDFViewer.nextPageButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + PDFViewer.nextPageButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + PDFViewer.nextPageButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.scaleSelect = document.getElementById('scaleSelect'); + PDFViewer.scaleSelect.onchange = function(evt) { + PDFViewer.changeScale(parseInt(this.value)); + }; + + if (window.File && window.FileReader && window.FileList && window.Blob) { + var openFileButton = document.getElementById('openFileButton'); + openFileButton.onclick = function(evt) { + if (this.className.indexOf('disabled') === -1) { + PDFViewer.fileInput.click(); + } + }; + openFileButton.onmousedown = function(evt) { + if (this.className.indexOf('disabled') === -1) { + this.className = 'down'; + } + }; + openFileButton.onmouseup = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + openFileButton.onmouseout = function(evt) { + this.className = (this.className.indexOf('disabled') !== -1) ? 'disabled' : ''; + }; + + PDFViewer.fileInput = document.getElementById('fileInput'); + PDFViewer.fileInput.onchange = function(evt) { + var files = evt.target.files; + + if (files.length > 0) { + var file = files[0]; + var fileReader = new FileReader(); + + document.title = file.name; + + // Read the local file into a Uint8Array. + fileReader.onload = function(evt) { + var data = evt.target.result; + var buffer = new ArrayBuffer(data.length); + var uint8Array = new Uint8Array(buffer); + + for (var i = 0; i < data.length; i++) { + uint8Array[i] = data.charCodeAt(i); + } + + PDFViewer.readPDF(uint8Array); + }; + + // Read as a binary string since "readAsArrayBuffer" is not yet + // implemented in Firefox. + fileReader.readAsBinaryString(file); + } + }; + PDFViewer.fileInput.value = null; + } else { + document.getElementById('fileWrapper').style.display = 'none'; + } + + PDFViewer.pageNumber = parseInt(PDFViewer.queryParams.page) || PDFViewer.pageNumber; + PDFViewer.scale = parseInt(PDFViewer.scaleSelect.value) / 100 || 1.0; + + PDFViewer.openURL(PDFViewer.queryParams.file || PDFViewer.url); + + window.onscroll = function(evt) { + var lastPagesDrawn = PDFViewer.lastPagesDrawn; + var visiblePages = PDFViewer.visiblePages(); + + var pagesToDraw = []; + var pagesToKeep = []; + var pagesToRemove = []; + + var i; + + // Determine which visible pages were not previously drawn. + for (i = 0; i < visiblePages.length; i++) { + if (lastPagesDrawn.indexOf(visiblePages[i]) === -1) { + pagesToDraw.push(visiblePages[i]); + PDFViewer.drawPage(visiblePages[i]); + } else { + pagesToKeep.push(visiblePages[i]); + } + } + + // Determine which previously drawn pages are no longer visible. + for (i = 0; i < lastPagesDrawn.length; i++) { + if (visiblePages.indexOf(lastPagesDrawn[i]) === -1) { + pagesToRemove.push(lastPagesDrawn[i]); + PDFViewer.removePage(lastPagesDrawn[i]); + } + } + + PDFViewer.lastPagesDrawn = pagesToDraw.concat(pagesToKeep); + + // Update the page number input with the current page number. + if (!PDFViewer.willJumpToPage && visiblePages.length > 0) { + PDFViewer.pageNumber = PDFViewer.pageNumberInput.value = visiblePages[0]; + PDFViewer.previousPageButton.className = (PDFViewer.pageNumber === 1) ? 'disabled' : ''; + PDFViewer.nextPageButton.className = (PDFViewer.pageNumber === PDFViewer.numberOfPages) ? 'disabled' : ''; + } else { + PDFViewer.willJumpToPage = false; + } + }; +}; diff --git a/pdf.js b/pdf.js index 320fc6913..3ebedd9e1 100644 --- a/pdf.js +++ b/pdf.js @@ -66,62 +66,63 @@ function stringToBytes(str) { var Stream = (function() { function constructor(arrayBuffer, start, length, dict) { - this.bytes = Uint8Array(arrayBuffer); + this.bytes = new Uint8Array(arrayBuffer); this.start = start || 0; this.pos = this.start; - this.end = (start + length) || this.bytes.byteLength; + this.end = (start + length) || this.bytes.length; this.dict = dict; } + // required methods for a stream. if a particular stream does not + // implement these, an error should be thrown constructor.prototype = { get length() { return this.end - this.start; }, - getByte: function() { - var bytes = this.bytes; + getByte: function stream_getByte() { if (this.pos >= this.end) - return -1; - return bytes[this.pos++]; + 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; + + if (!length) + return bytes.subarray(pos, strEnd); var end = pos + length; - var strEnd = this.end; - if (!end || end > strEnd) + if (end > strEnd) end = strEnd; this.pos = end; return bytes.subarray(pos, end); }, - lookChar: function() { - var bytes = this.bytes; + lookChar: function stream_lookChar() { if (this.pos >= this.end) - return; - return String.fromCharCode(bytes[this.pos]); + return null; + return String.fromCharCode(this.bytes[this.pos]); }, - getChar: function() { - var ch = this.lookChar(); - if (!ch) - return ch; - this.pos++; - return ch; + getChar: function stream_getChar() { + if (this.pos >= this.end) + return null; + return String.fromCharCode(this.bytes[this.pos++]); }, - skip: function(n) { - if (!n && !IsNum(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); } }; @@ -132,7 +133,7 @@ var Stream = (function() { var StringStream = (function() { function constructor(str) { var length = str.length; - var bytes = Uint8Array(length); + var bytes = new Uint8Array(length); for (var n = 0; n < length; ++n) bytes[n] = str.charCodeAt(n); Stream.call(this, bytes); @@ -143,12 +144,134 @@ var StringStream = (function() { return constructor; })(); +// super class for the decoding streams +var DecodeStream = (function() { + function constructor() { + this.pos = 0; + this.bufferLength = 0; + this.eof = false; + this.buffer = null; + } + + constructor.prototype = { + ensureBuffer: function decodestream_ensureBuffer(requested) { + var buffer = this.buffer; + var current = buffer ? buffer.byteLength : 0; + if (requested < current) + return buffer; + var size = 512; + while (size < requested) + size <<= 1; + var buffer2 = new Uint8Array(size); + for (var i = 0; i < current; ++i) + buffer2[i] = buffer[i]; + return this.buffer = buffer2; + }, + getByte: function decodestream_getByte() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return null; + this.readBlock(); + } + return this.buffer[this.pos++]; + }, + getBytes: function decodestream_getBytes(length) { + var pos = this.pos; + + if (length) { + this.ensureBuffer(pos + length); + var end = pos + length; + + while (!this.eof && this.bufferLength < end) + this.readBlock(); + + var bufEnd = this.bufferLength; + if (end > bufEnd) + end = bufEnd; + } else { + while (!this.eof) + this.readBlock(); + + var end = this.bufferLength; + } + + this.pos = end; + return this.buffer.subarray(pos, end) + }, + lookChar: function decodestream_lookChar() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return null; + this.readBlock(); + } + return String.fromCharCode(this.buffer[this.pos]); + }, + getChar: function decodestream_getChar() { + var pos = this.pos; + while (this.bufferLength <= pos) { + if (this.eof) + return null; + this.readBlock(); + } + return String.fromCharCode(this.buffer[this.pos++]); + }, + skip: function decodestream_skip(n) { + if (!n) + n = 1; + this.pos += n; + } + }; + + return constructor; +})(); + + +var FakeStream = (function() { + function constructor(stream) { + this.dict = stream.dict; + DecodeStream.call(this); + }; + + constructor.prototype = Object.create(DecodeStream.prototype); + constructor.prototype.readBlock = function() { + var bufferLength = this.bufferLength; + bufferLength += 1024; + var buffer = this.ensureBuffer(bufferLength); + this.bufferLength = bufferLength; + }; + constructor.prototype.getBytes = function(length) { + var pos = this.pos; + + if (length) { + this.ensureBuffer(pos + length); + var end = pos + length; + + while (!this.eof && this.bufferLength < end) + this.readBlock(); + + var bufEnd = this.bufferLength; + if (end > bufEnd) + end = bufEnd; + } else { + this.eof = true; + var end = this.bufferLength; + } + + this.pos = end; + return this.buffer.subarray(pos, end) + }; + + return constructor; +})(); + var FlateStream = (function() { - const codeLenCodeMap = Uint32Array([ + var codeLenCodeMap = new Uint32Array([ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 ]); - const lengthDecode = Uint32Array([ + var lengthDecode = new Uint32Array([ 0x00003, 0x00004, 0x00005, 0x00006, 0x00007, 0x00008, 0x00009, 0x0000a, 0x1000b, 0x1000d, 0x1000f, 0x10011, 0x20013, 0x20017, 0x2001b, 0x2001f, 0x30023, 0x3002b, 0x30033, 0x3003b, 0x40043, @@ -156,7 +279,7 @@ var FlateStream = (function() { 0x00102, 0x00102, 0x00102 ]); - const distDecode = Uint32Array([ + var distDecode = new Uint32Array([ 0x00001, 0x00002, 0x00003, 0x00004, 0x10005, 0x10007, 0x20009, 0x2000d, 0x30011, 0x30019, 0x40021, 0x40031, 0x50041, 0x50061, 0x60081, 0x600c1, 0x70101, 0x70181, 0x80201, 0x80301, 0x90401, @@ -164,7 +287,7 @@ var FlateStream = (function() { 0xd4001, 0xd6001 ]); - const fixedLitCodeTab = [Uint32Array([ + var fixedLitCodeTab = [new Uint32Array([ 0x70100, 0x80050, 0x80010, 0x80118, 0x70110, 0x80070, 0x80030, 0x900c0, 0x70108, 0x80060, 0x80020, 0x900a0, 0x80000, 0x80080, 0x80040, 0x900e0, 0x70104, 0x80058, 0x80018, 0x90090, 0x70114, @@ -241,7 +364,7 @@ var FlateStream = (function() { 0x900ff ]), 9]; - const fixedDistCodeTab = [Uint32Array([ + var fixedDistCodeTab = [new Uint32Array([ 0x50000, 0x50010, 0x50008, 0x50018, 0x50004, 0x50014, 0x5000c, 0x5001c, 0x50002, 0x50012, 0x5000a, 0x5001a, 0x50006, 0x50016, 0x5000e, 0x00000, 0x50001, 0x50011, 0x50009, 0x50019, 0x50005, @@ -267,273 +390,399 @@ var FlateStream = (function() { this.bytes = bytes; this.bytesPos = bytesPos; - this.eof = false; + this.codeSize = 0; this.codeBuf = 0; - - this.pos = 0; - this.bufferLength = 0; + + DecodeStream.call(this); } - constructor.prototype = { - getBits: function(bits) { - var codeSize = this.codeSize; - var codeBuf = this.codeBuf; - var bytes = this.bytes; - var bytesPos = this.bytesPos; + constructor.prototype = Object.create(DecodeStream.prototype); - var b; - while (codeSize < bits) { - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad encoding in flate stream"); - codeBuf |= b << codeSize; - codeSize += 8; - } - b = codeBuf & ((1 << bits) - 1); - this.codeBuf = codeBuf >> bits; - this.codeSize = codeSize -= bits; - this.bytesPos = bytesPos; - return b; - }, - getCode: function(table) { - var codes = table[0]; - var maxLen = table[1]; - var codeSize = this.codeSize; - var codeBuf = this.codeBuf; - var bytes = this.bytes; - var bytesPos = this.bytesPos; + constructor.prototype.getBits = function(bits) { + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + var bytes = this.bytes; + var bytesPos = this.bytesPos; - while (codeSize < maxLen) { - var b; - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad encoding in flate stream"); - codeBuf |= (b << codeSize); - codeSize += 8; - } - var code = codes[codeBuf & ((1 << maxLen) - 1)]; - var codeLen = code >> 16; - var codeVal = code & 0xffff; - if (codeSize == 0|| codeSize < codeLen || codeLen == 0) + var b; + while (codeSize < bits) { + if (typeof (b = bytes[bytesPos++]) == "undefined") error("Bad encoding in flate stream"); - this.codeBuf = (codeBuf >> codeLen); - this.codeSize = (codeSize - codeLen); - this.bytesPos = bytesPos; - return codeVal; - }, - ensureBuffer: function(requested) { - var buffer = this.buffer; - var current = buffer ? buffer.byteLength : 0; - if (requested < current) - return buffer; - var size = 512; - while (size < requested) - size <<= 1; - var buffer2 = Uint8Array(size); - for (var i = 0; i < current; ++i) - buffer2[i] = buffer[i]; - return this.buffer = buffer2; - }, - getByte: function() { - var pos = this.pos; - while (this.bufferLength <= pos) { - if (this.eof) - return; - this.readBlock(); - } - return this.buffer[this.pos++]; - }, - getBytes: function(length) { - var pos = this.pos; + codeBuf |= b << codeSize; + codeSize += 8; + } + b = codeBuf & ((1 << bits) - 1); + this.codeBuf = codeBuf >> bits; + this.codeSize = codeSize -= bits; + this.bytesPos = bytesPos; + return b; + }; + constructor.prototype.getCode = function(table) { + var codes = table[0]; + var maxLen = table[1]; + var codeSize = this.codeSize; + var codeBuf = this.codeBuf; + var bytes = this.bytes; + var bytesPos = this.bytesPos; - while (!this.eof && this.bufferLength < pos + length) - this.readBlock(); + while (codeSize < maxLen) { + var b; + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad encoding in flate stream"); + codeBuf |= (b << codeSize); + codeSize += 8; + } + var code = codes[codeBuf & ((1 << maxLen) - 1)]; + var codeLen = code >> 16; + var codeVal = code & 0xffff; + if (codeSize == 0|| codeSize < codeLen || codeLen == 0) + error("Bad encoding in flate stream"); + this.codeBuf = (codeBuf >> codeLen); + this.codeSize = (codeSize - codeLen); + this.bytesPos = bytesPos; + return codeVal; + }; + constructor.prototype.generateHuffmanTable = function(lengths) { + var n = lengths.length; - var end = pos + length; - var bufEnd = this.bufferLength; + // find max code length + var maxLen = 0; + for (var i = 0; i < n; ++i) { + if (lengths[i] > maxLen) + maxLen = lengths[i]; + } - if (end > bufEnd) - end = bufEnd; - - this.pos = end; - return this.buffer.subarray(pos, end) - }, - lookChar: function() { - var pos = this.pos; - while (this.bufferLength <= pos) { - if (this.eof) - return; - this.readBlock(); - } - return String.fromCharCode(this.buffer[pos]); - }, - getChar: function() { - var ch = this.lookChar(); - // shouldnt matter what the position is if we get past the eof - // so no need to check if ch is undefined - this.pos++; - return ch; - }, - skip: function(n) { - if (!n) - n = 1; - this.pos += n; - }, - generateHuffmanTable: function(lengths) { - var n = lengths.length; - - // find max code length - var maxLen = 0; - for (var i = 0; i < n; ++i) { - if (lengths[i] > maxLen) - maxLen = lengths[i]; - } - - // build the table - var size = 1 << maxLen; - var codes = Uint32Array(size); - for (var len = 1, code = 0, skip = 2; - len <= maxLen; - ++len, code <<= 1, skip <<= 1) { - for (var val = 0; val < n; ++val) { - if (lengths[val] == len) { - // bit-reverse the code - var code2 = 0; - var t = code; - for (var i = 0; i < len; ++i) { - code2 = (code2 << 1) | (t & 1); - t >>= 1; - } - - // fill the table entries - for (var i = code2; i < size; i += skip) - codes[i] = (len << 16) | val; - - ++code; + // build the table + var size = 1 << maxLen; + var codes = new Uint32Array(size); + for (var len = 1, code = 0, skip = 2; + len <= maxLen; + ++len, code <<= 1, skip <<= 1) { + for (var val = 0; val < n; ++val) { + if (lengths[val] == len) { + // bit-reverse the code + var code2 = 0; + var t = code; + for (var i = 0; i < len; ++i) { + code2 = (code2 << 1) | (t & 1); + t >>= 1; } + + // fill the table entries + for (var i = code2; i < size; i += skip) + codes[i] = (len << 16) | val; + + ++code; } } + } - return [codes, maxLen]; - }, - readBlock: function() { - function repeat(stream, array, len, offset, what) { - var repeat = stream.getBits(len) + offset; - while (repeat-- > 0) - array[i++] = what; - } + return [codes, maxLen]; + }; + constructor.prototype.readBlock = function() { + function repeat(stream, array, len, offset, what) { + var repeat = stream.getBits(len) + offset; + while (repeat-- > 0) + array[i++] = what; + } + // read block header + var hdr = this.getBits(3); + if (hdr & 1) + this.eof = true; + hdr >>= 1; + + if (hdr == 0) { // uncompressed block var bytes = this.bytes; var bytesPos = this.bytesPos; - - // read block header - var hdr = this.getBits(3); - if (hdr & 1) - this.eof = true; - hdr >>= 1; - var b; - if (hdr == 0) { // uncompressed block - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - var blockLen = b; - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - blockLen |= (b << 8); - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - var check = b; - if (typeof (b = bytes[bytesPos++]) == "undefined") - error("Bad block header in flate stream"); - check |= (b << 8); - if (check != (~this.blockLen & 0xffff)) - error("Bad uncompressed block length in flate stream"); - var bufferLength = this.bufferLength; - var buffer = this.ensureBuffer(bufferLength + blockLen); - this.bufferLength = bufferLength + blockLen; - for (var n = bufferLength; n < blockLen; ++n) { - if (typeof (b = bytes[bytesPos++]) == "undefined") { - this.eof = true; - break; - } - buffer[n] = b; + + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + var blockLen = b; + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + blockLen |= (b << 8); + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + var check = b; + if (typeof (b = bytes[bytesPos++]) == "undefined") + error("Bad block header in flate stream"); + check |= (b << 8); + if (check != (~blockLen & 0xffff)) + error("Bad uncompressed block length in flate stream"); + + this.codeBuf = 0; + this.codeSize = 0; + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + blockLen); + var end = bufferLength + blockLen; + this.bufferLength = end; + for (var n = bufferLength; n < end; ++n) { + if (typeof (b = bytes[bytesPos++]) == "undefined") { + this.eof = true; + break; } + buffer[n] = b; + } + this.bytesPos = bytesPos; + return; + } + + var litCodeTable; + var distCodeTable; + if (hdr == 1) { // compressed block, fixed codes + litCodeTable = fixedLitCodeTab; + distCodeTable = fixedDistCodeTab; + } else if (hdr == 2) { // compressed block, dynamic codes + var numLitCodes = this.getBits(5) + 257; + var numDistCodes = this.getBits(5) + 1; + var numCodeLenCodes = this.getBits(4) + 4; + + // build the code lengths code table + var codeLenCodeLengths = Array(codeLenCodeMap.length); + var i = 0; + while (i < numCodeLenCodes) + codeLenCodeLengths[codeLenCodeMap[i++]] = this.getBits(3); + var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths); + + // build the literal and distance code tables + var len = 0; + var i = 0; + var codes = numLitCodes + numDistCodes; + var codeLengths = new Array(codes); + while (i < codes) { + var code = this.getCode(codeLenCodeTab); + if (code == 16) { + repeat(this, codeLengths, 2, 3, len); + } else if (code == 17) { + repeat(this, codeLengths, 3, 3, len = 0); + } else if (code == 18) { + repeat(this, codeLengths, 7, 11, len = 0); + } else { + codeLengths[i++] = len = code; + } + } + + litCodeTable = + this.generateHuffmanTable(codeLengths.slice(0, numLitCodes)); + distCodeTable = + this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes)); + } else { + error("Unknown block type in flate stream"); + } + + var buffer = this.buffer; + var limit = buffer ? buffer.length : 0; + var pos = this.bufferLength; + while (true) { + var code1 = this.getCode(litCodeTable); + if (code1 < 256) { + if (pos + 1 >= limit) { + buffer = this.ensureBuffer(pos + 1); + limit = buffer.length; + } + buffer[pos++] = code1; + continue; + } + if (code1 == 256) { + this.bufferLength = pos; return; } - - var litCodeTable; - var distCodeTable; - if (hdr == 1) { // compressed block, fixed codes - litCodeTable = fixedLitCodeTab; - distCodeTable = fixedDistCodeTab; - } else if (hdr == 2) { // compressed block, dynamic codes - var numLitCodes = this.getBits(5) + 257; - var numDistCodes = this.getBits(5) + 1; - var numCodeLenCodes = this.getBits(4) + 4; - - // build the code lengths code table - var codeLenCodeLengths = Array(codeLenCodeMap.length); - var i = 0; - while (i < numCodeLenCodes) - codeLenCodeLengths[codeLenCodeMap[i++]] = this.getBits(3); - var codeLenCodeTab = this.generateHuffmanTable(codeLenCodeLengths); - - // build the literal and distance code tables - var len = 0; - var i = 0; - var codes = numLitCodes + numDistCodes; - var codeLengths = new Array(codes); - while (i < codes) { - var code = this.getCode(codeLenCodeTab); - if (code == 16) { - repeat(this, codeLengths, 2, 3, len); - } else if (code == 17) { - repeat(this, codeLengths, 3, 3, len = 0); - } else if (code == 18) { - repeat(this, codeLengths, 7, 11, len = 0); - } else { - codeLengths[i++] = len = code; - } - } - - litCodeTable = this.generateHuffmanTable(codeLengths.slice(0, numLitCodes)); - distCodeTable = this.generateHuffmanTable(codeLengths.slice(numLitCodes, codes)); - } else { - error("Unknown block type in flate stream"); - } - - var pos = this.bufferLength; - while (true) { - var code1 = this.getCode(litCodeTable); - if (code1 == 256) { - this.bufferLength = pos; - return; - } - if (code1 < 256) { - var buffer = this.ensureBuffer(pos + 1); - buffer[pos++] = code1; - } else { - code1 -= 257; - code1 = lengthDecode[code1]; - var code2 = code1 >> 16; - if (code2 > 0) - code2 = this.getBits(code2); - var len = (code1 & 0xffff) + code2; - code1 = this.getCode(distCodeTable); - code1 = distDecode[code1]; - code2 = code1 >> 16; - if (code2 > 0) - code2 = this.getBits(code2); - var dist = (code1 & 0xffff) + code2; - var buffer = this.ensureBuffer(pos + len); - for (var k = 0; k < len; ++k, ++pos) - buffer[pos] = buffer[pos - dist]; - } + code1 -= 257; + code1 = lengthDecode[code1]; + var code2 = code1 >> 16; + if (code2 > 0) + code2 = this.getBits(code2); + var len = (code1 & 0xffff) + code2; + code1 = this.getCode(distCodeTable); + code1 = distDecode[code1]; + code2 = code1 >> 16; + if (code2 > 0) + code2 = this.getBits(code2); + var dist = (code1 & 0xffff) + code2; + if (pos + len >= limit) { + buffer = this.ensureBuffer(pos + len); + limit = buffer.length; } + for (var k = 0; k < len; ++k, ++pos) + buffer[pos] = buffer[pos - dist]; } }; return constructor; })(); + +var PredictorStream = (function() { + function constructor(stream, params) { + var predictor = this.predictor = params.get("Predictor") || 1; + + if (predictor <= 1) + return stream; // no prediction + if (predictor !== 2 && (predictor < 10 || predictor > 15)) + error("Unsupported predictor"); + + if (predictor === 2) + this.readBlock = this.readBlockTiff; + else + this.readBlock = this.readBlockPng; + + this.stream = stream; + this.dict = stream.dict; + if (params.has("EarlyChange")) { + error("EarlyChange predictor parameter is not supported"); + } + var colors = this.colors = params.get("Colors") || 1; + var bits = this.bits = params.get("BitsPerComponent") || 8; + var columns = this.columns = params.get("Columns") || 1; + + var pixBytes = this.pixBytes = (colors * bits + 7) >> 3; + // add an extra pixByte to represent the pixel left of column 0 + var rowBytes = this.rowBytes = (columns * colors * bits + 7) >> 3; + + DecodeStream.call(this); + return this; + } + + constructor.prototype = Object.create(DecodeStream.prototype); + + constructor.prototype.readBlockTiff = function() { + var rowBytes = this.rowBytes; + var pixBytes = this.pixBytes; + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + rowBytes); + var currentRow = buffer.subarray(bufferLength, bufferLength + rowBytes); + + var bits = this.bits; + var colors = this.colors; + + var rawBytes = this.stream.getBytes(rowBytes); + + if (bits === 1) { + var inbuf = 0; + for (var i = 0; i < rowBytes; ++i) { + var c = rawBytes[i]; + inBuf = (inBuf << 8) | c; + // bitwise addition is exclusive or + // first shift inBuf and then add + currentRow[i] = (c ^ (inBuf >> colors)) & 0xFF; + // truncate inBuf (assumes colors < 16) + inBuf &= 0xFFFF; + } + } else if (bits === 8) { + for (var i = 0; i < colors; ++i) + currentRow[i] = rawBytes[i]; + for (; i < rowBytes; ++i) + currentRow[i] = currentRow[i - colors] + rawBytes[i]; + } else { + var compArray = new Uint8Array(colors + 1); + var bitMask = (1 << bits) - 1; + var inbuf = 0, outbut = 0; + var inbits = 0, outbits = 0; + var j = 0, k = 0; + var columns = this.columns; + for (var i = 0; i < columns; ++i) { + for (var kk = 0; kk < colors; ++kk) { + if (inbits < bits) { + inbuf = (inbuf << 8) | (rawBytes[j++] & 0xFF); + inbits += 8; + } + compArray[kk] = (compArray[kk] + + (inbuf >> (inbits - bits))) & bitMask; + inbits -= bits; + outbuf = (outbuf << bits) | compArray[kk]; + outbits += bits; + if (outbits >= 8) { + currentRow[k++] = (outbuf >> (outbits - 8)) & 0xFF; + outbits -= 8; + } + } + } + if (outbits > 0) { + currentRow[k++] = (outbuf << (8 - outbits)) + + (inbuf & ((1 << (8 - outbits)) - 1)) + } + } + this.bufferLength += rowBytes; + }; + constructor.prototype.readBlockPng = function() { + var rowBytes = this.rowBytes; + var pixBytes = this.pixBytes; + + var predictor = this.stream.getByte(); + var rawBytes = this.stream.getBytes(rowBytes); + + var bufferLength = this.bufferLength; + var buffer = this.ensureBuffer(bufferLength + pixBytes); + + var currentRow = buffer.subarray(bufferLength, bufferLength + rowBytes); + var prevRow = buffer.subarray(bufferLength - rowBytes, bufferLength); + if (prevRow.length == 0) + prevRow = currentRow; + + switch (predictor) { + case 0: + break; + case 1: + for (var i = 0; i < pixBytes; ++i) + currentRow[i] = rawBytes[i]; + for (; i < rowBytes; ++i) + currentRow[i] = (currentRow[i - pixBytes] + rawBytes[i]) & 0xFF; + break; + case 2: + for (var i = 0; i < rowBytes; ++i) + currentRow[i] = (prevRow[i] + rawBytes[i]) & 0xFF; + break; + case 3: + for (var i = 0; i < pixBytes; ++i) + currentRow[i] = (prevRow[i] >> 1) + rawBytes[i]; + for (; i < rowBytes; ++i) + currentRow[i] = (((prevRow[i] + currentRow[i - pixBytes]) + >> 1) + rawBytes[i]) & 0xFF; + break; + case 4: + // we need to save the up left pixels values. the simplest way + // is to create a new buffer + for (var i = 0; i < pixBytes; ++i) + currentRow[i] = rawBytes[i]; + for (; i < rowBytes; ++i) { + var up = prevRow[i]; + var upLeft = lastRow[i - pixBytes]; + var left = currentRow[i - pixBytes]; + var p = left + up - upLeft; + + var pa = p - left; + if (pa < 0) + pa = -pa; + var pb = p - up; + if (pb < 0) + pb = -pb; + var pc = p - upLeft; + if (pc < 0) + pc = -pc; + + var c = rawBytes[i]; + if (pa <= pb && pa <= pc) + currentRow[i] = left + c; + else if (pb <= pc) + currentRow[i] = up + c; + else + currentRow[i] = upLeft + c; + break; + } + default: + error("Unsupported predictor"); + break; + } + this.bufferLength += rowBytes; + }; + + return constructor; +})(); + // A JpegStream can't be read directly. We use the platform to render the underlying // JPEG data for us. var JpegStream = (function() { @@ -558,196 +807,6 @@ var JpegStream = (function() { return constructor; })(); - -var PredictorStream = (function() { - function constructor(stream, params) { - var predictor = this.predictor = params.get("Predictor") || 1; - - if (predictor <= 1) - return stream; // no prediction - if (predictor !== 2 && (predictor < 10 || predictor > 15)) - error("Unsupported predictor"); - - if (predictor === 2) - this.readRow = this.readRowTiff; - else - this.readRow = this.readRowPng; - - this.stream = stream; - this.dict = stream.dict; - if (params.has("EarlyChange")) { - error("EarlyChange predictor parameter is not supported"); - } - var colors = this.colors = params.get("Colors") || 1; - var bits = this.bits = params.get("BitsPerComponent") || 8; - var columns = this.columns = params.get("Columns") || 1; - - var pixBytes = this.pixBytes = (colors * bits + 7) >> 3; - // add an extra pixByte to represent the pixel left of column 0 - var rowBytes = this.rowBytes = ((columns * colors * bits + 7) >> 3) + pixBytes; - - this.currentRow = new Uint8Array(rowBytes); - this.bufferLength = rowBytes; - this.pos = rowBytes; - } - - constructor.prototype = { - readRowTiff : function() { - var currentRow = this.currentRow; - var rowBytes = this.rowBytes; - var pixBytes = this.pixBytes; - var bits = this.bits; - var colors = this.colors; - - var rawBytes = this.stream.getBytes(rowBytes - pixBytes); - - if (bits === 1) { - var inbuf = 0; - for (var i = pixBytes, j = 0; i < rowBytes; ++i, ++j) { - var c = rawBytes[j]; - inBuf = (inBuf << 8) | c; - // bitwise addition is exclusive or - // first shift inBuf and then add - currentRow[i] = (c ^ (inBuf >> colors)) & 0xFF; - // truncate inBuf (assumes colors < 16) - inBuf &= 0xFFFF; - } - } else if (bits === 8) { - for (var i = pixBytes, j = 0; i < rowBytes; ++i, ++j) - currentRow[i] = currentRow[i - colors] + rawBytes[j]; - } else { - var compArray = new Uint8Array(colors + 1); - var bitMask = (1 << bits) - 1; - var inbuf = 0, outbut = 0; - var inbits = 0, outbits = 0; - var j = 0, k = pixBytes; - var columns = this.columns; - for (var i = 0; i < columns; ++i) { - for (var kk = 0; kk < colors; ++kk) { - if (inbits < bits) { - inbuf = (inbuf << 8) | (rawBytes[j++] & 0xFF); - inbits += 8; - } - compArray[kk] = (compArray[kk] + - (inbuf >> (inbits - bits))) & bitMask; - inbits -= bits; - outbuf = (outbuf << bits) | compArray[kk]; - outbits += bits; - if (outbits >= 8) { - currentRow[k++] = (outbuf >> (outbits - 8)) & 0xFF; - outbits -= 8; - } - } - } - if (outbits > 0) { - currentRow[k++] = (outbuf << (8 - outbits)) + - (inbuf & ((1 << (8 - outbits)) - 1)) - } - } - this.pos = pixBytes; - }, - readRowPng : function() { - var currentRow = this.currentRow; - - var rowBytes = this.rowBytes; - var pixBytes = this.pixBytes; - - var predictor = this.stream.getByte(); - var rawBytes = this.stream.getBytes(rowBytes - pixBytes); - - switch (predictor) { - case 0: - break; - case 1: - for (var i = pixBytes, j = 0; i < rowBytes; ++i, ++j) - currentRow[i] = (currentRow[i - pixBytes] + rawBytes[j]) & 0xFF; - break; - case 2: - for (var i = pixBytes, j = 0; i < rowBytes; ++i, ++j) - currentRow[i] = (currentRow[i] + rawBytes[j]) & 0xFF; - break; - case 3: - for (var i = pixBytes, j = 0; i < rowBytes; ++i, ++j) - currentRow[i] = (((currentRow[i] + currentRow[i - pixBytes]) - >> 1) + rawBytes[j]) & 0xFF; - break; - case 4: - // we need to save the up left pixels values. the simplest way - // is to create a new buffer - var lastRow = currentRow; - var currentRow = new Uint8Array(rowBytes); - for (var i = pixBytes, j = 0; i < rowBytes; ++i, ++j) { - var up = lastRow[i]; - var upLeft = lastRow[i - pixBytes]; - var left = currentRow[i - pixBytes]; - var p = left + up - upLeft; - - var pa = p - left; - if (pa < 0) - pa = -pa; - var pb = p - up; - if (pb < 0) - pb = -pb; - var pc = p - upLeft; - if (pc < 0) - pc = -pc; - - var c = rawBytes[j]; - if (pa <= pb && pa <= pc) - currentRow[i] = left + c; - else if (pb <= pc) - currentRow[i] = up + c; - else - currentRow[i] = upLeft + c; - break; - this.currentRow = currentRow; - } - default: - error("Unsupported predictor"); - break; - } - this.pos = pixBytes; - }, - getByte : function() { - if (this.pos >= this.bufferLength) - this.readRow(); - return this.currentRow[this.pos++]; - }, - getBytes : function(n) { - var i, bytes; - bytes = new Uint8Array(n); - for (i = 0; i < n; ++i) { - if (this.pos >= this.bufferLength) - this.readRow(); - bytes[i] = this.currentRow[this.pos++]; - } - return bytes; - }, - getChar : function() { - return String.formCharCode(this.getByte()); - }, - lookChar : function() { - if (this.pos >= this.bufferLength) - this.readRow(); - return String.formCharCode(this.currentRow[this.pos]); - }, - skip : function(n) { - var i; - if (!n) { - n = 1; - } - while (n > this.bufferLength - this.pos) { - n -= this.bufferLength - this.pos; - this.readRow(); - if (this.bufferLength === 0) break; - } - this.pos += n; - } - }; - - return constructor; -})(); - var DecryptStream = (function() { function constructor(str, decrypt) { this.str = str; @@ -781,6 +840,74 @@ var DecryptStream = (function() { return constructor; })(); +var Ascii85Stream = (function() { + function constructor(str) { + this.str = str; + this.dict = str.dict; + this.input = new Uint8Array(5); + + DecodeStream.call(this); + } + + constructor.prototype = Object.create(DecodeStream.prototype); + constructor.prototype.readBlock = function() { + var tildaCode = "~".charCodeAt(0); + var zCode = "z".charCodeAt(0); + var str = this.str; + + var c = str.getByte(); + while (Lexer.isSpace(String.fromCharCode(c))) + c = str.getByte(); + + if (!c || c === tildaCode) { + this.eof = true; + return; + } + + var bufferLength = this.bufferLength; + + // special code for z + if (c == zCode) { + var buffer = this.ensureBuffer(bufferLength + 4); + for (var i = 0; i < 4; ++i) + buffer[bufferLength + i] = 0; + this.bufferLength += 4; + } else { + var input = this.input; + input[0] = c; + for (var i = 1; i < 5; ++i){ + c = str.getByte(); + while (Lexer.isSpace(String.fromCharCode(c))) + c = str.getByte(); + + input[i] = c; + + if (!c || c == tildaCode) + break; + } + var buffer = this.ensureBuffer(bufferLength + i - 1); + this.bufferLength += i - 1; + + // partial ending; + if (i < 5) { + for (; i < 5; ++i) + input[i] = 0x21 + 84; + this.eof = true; + } + var t = 0; + for (var i = 0; i < 5; ++i) + t = t * 85 + (input[i] - 0x21); + + for (var i = 3; i >= 0; --i){ + buffer[bufferLength + i] = t & 0xFF; + t >>= 8; + } + } + }; + + return constructor; +})(); + var Name = (function() { function constructor(name) { this.name = name; @@ -810,7 +937,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); @@ -943,10 +1072,10 @@ var Lexer = (function() { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // fx ]; - const MIN_INT = (1<<31) | 0; - const MAX_INT = (MIN_INT - 1) | 0; - const MIN_UINT = 0; - const MAX_UINT = ((1<<30) * 4) - 1; + var MIN_INT = (1<<31) | 0; + var MAX_INT = (MIN_INT - 1) | 0; + var MIN_UINT = 0; + var MAX_UINT = ((1<<30) * 4) - 1; function ToHexDigit(ch) { if (ch >= "0" && ch <= "9") @@ -1352,8 +1481,8 @@ var Parser = (function() { if (IsArray(filter)) { var filterArray = filter; var paramsArray = params; - for (var i = 0, ii = filter.length; i < ii; ++i) { - filter = filter[i]; + for (var i = 0, ii = filterArray.length; i < ii; ++i) { + filter = filterArray[i]; if (!IsName(filter)) error("Bad filter name"); else { @@ -1375,6 +1504,11 @@ var Parser = (function() { } else if (name == "DCTDecode") { var bytes = stream.getBytes(length); return new JpegStream(bytes, stream.dict); + } else if (name == "ASCII85Decode") { + return new Ascii85Stream(stream); + } else if (name == "CCITTFaxDecode") { + TODO("implement fax stream"); + return new FakeStream(stream); } else { error("filter '" + name + "' not supported yet"); } @@ -1483,7 +1617,7 @@ var XRef = (function() { } constructor.prototype = { - readXRefTable: function(parser) { + readXRefTable: function readXRefTable(parser) { var obj; while (true) { if (IsCmd(obj = parser.getObj(), "trailer")) @@ -1554,7 +1688,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"); @@ -1606,7 +1740,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); @@ -1624,6 +1758,7 @@ var XRef = (function() { return this.readXRefStream(obj); } error("Invalid XRef"); + return null; }, getEntry: function(i) { var e = this.entries[i]; @@ -1964,7 +2099,7 @@ var PDFDoc = (function() { return constructor; })(); -const IDENTITY_MATRIX = [ 1, 0, 0, 1, 0, 0 ]; +var IDENTITY_MATRIX = [ 1, 0, 0, 1, 0, 0 ]; // contexts store most of the state we need natively. // However, PDF needs a bit more state, which we store here. @@ -1988,7 +2123,7 @@ var CanvasExtraState = (function() { return constructor; })(); -const Encodings = { +var Encodings = { get ExpertEncoding() { return shadow(this, "ExpertEncoding", [ ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, "space","exclamsmall","Hungarumlautsmall",,"dollaroldstyle","dollarsuperior", @@ -2162,15 +2297,34 @@ const Encodings = { } }; +function ScratchCanvas(width, height) { + var canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + return canvas; +} + var CanvasGraphics = (function() { - function constructor(canvasCtx) { + function constructor(canvasCtx, imageCanvas) { this.ctx = canvasCtx; this.current = new CanvasExtraState(); this.stateStack = [ ]; this.pendingClip = null; this.res = null; this.xobjs = null; - this.map = { + this.ScratchCanvas = imageCanvas || ScratchCanvas; + } + + var LINE_CAP_STYLES = [ "butt", "round", "square" ]; + var LINE_JOIN_STYLES = [ "miter", "round", "bevel" ]; + var NORMAL_CLIP = {}; + var EO_CLIP = {}; + + // Used for tiling patterns + var PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; + + constructor.prototype = { + map: { // Graphics state w: "setLineWidth", J: "setLineCap", @@ -2263,20 +2417,15 @@ var CanvasGraphics = (function() { // Compatibility BX: "beginCompat", EX: "endCompat", - }; - } - - const LINE_CAP_STYLES = [ "butt", "round", "square" ]; - const LINE_JOIN_STYLES = [ "miter", "round", "bevel" ]; - const NORMAL_CLIP = {}; - const EO_CLIP = {}; - - // Used for tiling patterns - const PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; - - constructor.prototype = { + }, + translateFont: function(fontDict, xref, resources) { - var descriptor = xref.fetch(fontDict.get("FontDescriptor")); + var fd = fontDict.get("FontDescriptor"); + if (!fd) + // XXX deprecated "special treatment" for standard + // fonts? What do we need to do here? + return null; + var descriptor = xref.fetch(fd); var fontName = descriptor.get("FontName"); assertWellFormed(IsName(fontName), "invalid font name"); @@ -2287,16 +2436,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 = true; - 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 @@ -2319,9 +2461,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")); @@ -2334,20 +2475,18 @@ var CanvasGraphics = (function() { } } } else if (fontDict.has("ToUnicode")) { + encodingMap = {empty: true}; var cmapObj = xref.fetchIfRef(fontDict.get("ToUnicode")); if (IsName(cmapObj)) { error("ToUnicode file cmap translation not implemented"); } else if (IsStream(cmapObj)) { var encoding = Encodings["WinAnsiEncoding"]; var firstChar = xref.fetchIfRef(fontDict.get("FirstChar")); - for (var i = firstChar; i < encoding.length; i++) - encodingMap[i] = new Name(encoding[i]); var tokens = []; var token = ""; - var buffer = cmapObj.ensureBuffer ? cmapObj.ensureBuffer() : cmapObj; - var cmap = cmapObj.getBytes(buffer.byteLength); + 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) { @@ -2357,7 +2496,6 @@ var CanvasGraphics = (function() { break; case "beginbfrange": - ignoreFont = false; case "begincodespacerange": token = ""; tokens = []; @@ -2374,8 +2512,10 @@ var CanvasGraphics = (function() { var code = parseInt("0x" + tokens[j+2]); for (var k = startRange; k <= endRange; k++) { - encodingMap[k] = GlyphsUnicode[encoding[code]]; - charset.push(encoding[code++]); + // The encoding mapping table will be filled + // later during the building phase + //encodingMap[k] = GlyphsUnicode[encoding[code]]; + charset.push(encoding[code++] || ".notdef"); } } break; @@ -2402,16 +2542,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 { @@ -2551,12 +2694,18 @@ var CanvasGraphics = (function() { }, save: function() { this.ctx.save(); + if (this.ctx.$saveCurrentX) { + this.ctx.$saveCurrentX(); + } this.stateStack.push(this.current); this.current = new CanvasExtraState(); }, restore: function() { var prev = this.stateStack.pop(); if (prev) { + if (this.ctx.$restoreCurrentX) { + this.ctx.$restoreCurrentX(); + } this.current = prev; this.ctx.restore(); } @@ -2637,6 +2786,9 @@ var CanvasGraphics = (function() { // Text beginText: function() { this.current.textMatrix = IDENTITY_MATRIX; + if (this.ctx.$setCurrentX) { + this.ctx.$setCurrentX(0) + } this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, @@ -2680,7 +2832,7 @@ var CanvasGraphics = (function() { } this.current.fontSize = size; - this.ctx.font = this.current.fontSize +'px "' + fontName + '"'; + this.ctx.font = this.current.fontSize +'px "' + fontName + '", Symbol'; }, setTextRenderingMode: function(mode) { TODO("text rendering mode"); @@ -2691,6 +2843,9 @@ var CanvasGraphics = (function() { moveText: function (x, y) { this.current.x = this.current.lineX += x; this.current.y = this.current.lineY += y; + if (this.ctx.$setCurrentX) { + this.ctx.$setCurrentX(this.current.x) + } }, setLeadingMoveText: function(x, y) { this.setLeading(-y); @@ -2698,6 +2853,10 @@ var CanvasGraphics = (function() { }, setTextMatrix: function(a, b, c, d, e, f) { this.current.textMatrix = [ a, b, c, d, e, f ]; + + if (this.ctx.$setCurrentX) { + this.ctx.$setCurrentX(0) + } this.current.x = this.current.lineX = 0; this.current.y = this.current.lineY = 0; }, @@ -2708,9 +2867,15 @@ var CanvasGraphics = (function() { this.ctx.save(); this.ctx.transform.apply(this.ctx, this.current.textMatrix); this.ctx.scale(1, -1); - this.ctx.translate(0, -2 * this.current.y); - this.ctx.fillText(Fonts.charsToUnicode(text), this.current.x, this.current.y); - this.current.x += this.ctx.measureText(text).width; + + if (this.ctx.$showText) { + this.ctx.$showText(this.current.y, Fonts.charsToUnicode(text)); + } else { + text = Fonts.charsToUnicode(text); + this.ctx.translate(this.current.x, -1 * this.current.y); + this.ctx.fillText(text, 0, 0); + this.current.x += this.ctx.measureText(text).width; + } this.ctx.restore(); }, @@ -2718,7 +2883,11 @@ var CanvasGraphics = (function() { for (var i = 0; i < arr.length; ++i) { var e = arr[i]; if (IsNum(e)) { - this.current.x -= e * 0.001 * this.current.fontSize; + if (this.ctx.$addCurrentX) { + this.ctx.$addCurrentX(-e * 0.001 * this.current.fontSize) + } else { + this.current.x -= e * 0.001 * this.current.fontSize; + } } else if (IsString(e)) { this.showText(e); } else { @@ -2795,13 +2964,14 @@ var CanvasGraphics = (function() { error("Unable to find pattern resource"); var pattern = xref.fetchIfRef(patternRes.get(patternName.name)); - - const types = [null, this.tilingFill]; - var typeNum = pattern.dict.get("PatternType"); + var patternDict = IsStream(pattern) ? pattern.dict : pattern; + var types = [null, this.tilingFill, + function() { TODO("Shading Patterns"); }]; + var typeNum = patternDict.get("PatternType"); var patternFn = types[typeNum]; if (!patternFn) error("Unhandled pattern type"); - patternFn.call(this, pattern); + patternFn.call(this, pattern, patternDict); } } else { // TODO real impl @@ -2856,9 +3026,10 @@ var CanvasGraphics = (function() { // we want the canvas to be as large as the step size var botRight = applyMatrix([x0 + xstep, y0 + ystep], matrix); - var tmpCanvas = document.createElement("canvas"); - tmpCanvas.width = Math.ceil(botRight[0] - topLeft[0]); - tmpCanvas.height = Math.ceil(botRight[1] - topLeft[1]); + var tmpCanvas = new this.ScratchCanvas( + Math.ceil(botRight[0] - topLeft[0]), // width + Math.ceil(botRight[1] - topLeft[1]) // height + ); // set the new canvas element context as the graphics context var tmpCtx = tmpCanvas.getContext("2d"); @@ -2894,7 +3065,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) { @@ -2920,6 +3091,7 @@ var CanvasGraphics = (function() { shadingFill: function(entryRef) { var xref = this.xref; var res = this.res; + var shadingRes = xref.fetchIfRef(res.get("Shading")); if (!shadingRes) error("No shading resource found"); @@ -2944,7 +3116,7 @@ var CanvasGraphics = (function() { if (background) TODO("handle background colors"); - const types = [null, + var types = [null, this.fillFunctionShading, this.fillAxialShading, this.fillRadialShading]; @@ -2952,7 +3124,7 @@ var CanvasGraphics = (function() { var typeNum = shading.get("ShadingType"); var fillFn = types[typeNum]; if (!fillFn) - error("Unknown type of shading"); + error("Unknown or NYI type of shading '"+ typeNum +"'"); fillFn.apply(this, [shading]); this.restore(); @@ -3005,6 +3177,10 @@ var CanvasGraphics = (function() { this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10); }, + fillRadialShading: function(sh) { + TODO("radial shading"); + }, + // Images beginInlineImage: function() { TODO("inline images"); @@ -3115,8 +3291,11 @@ var CanvasGraphics = (function() { } } - if (bitsPerComponent !== 8) - error("Unsupported bpc"); + if (bitsPerComponent !== 8) { + TODO("Support bpc="+ bitsPerComponent); + this.restore(); + return; + } var xref = this.xref; var colorSpaces = this.colorSpaces; @@ -3173,9 +3352,7 @@ var CanvasGraphics = (function() { // handle matte object } - var tmpCanvas = document.createElement("canvas"); - tmpCanvas.width = w; - tmpCanvas.height = h; + var tmpCanvas = new this.ScratchCanvas(w, h); var tmpCtx = tmpCanvas.getContext("2d"); var imgData = tmpCtx.getImageData(0, 0, w, h); var pixels = imgData.data; @@ -3214,7 +3391,7 @@ var CanvasGraphics = (function() { } break; default: - error("unhandled amount of components per pixel: " + numComps); + TODO("Images with "+ numComps + " components per pixel"); } } else { var numComps = colorSpace.numComps; @@ -3241,7 +3418,7 @@ var CanvasGraphics = (function() { } break; default: - error("unhandled amount of components per pixel: " + numComps); + TODO("Images with "+ numComps + " components per pixel"); } } tmpCtx.putImageData(imgData, 0, 0); @@ -3343,12 +3520,23 @@ var ColorSpace = (function() { break; case "ICCBased": var dict = stream.dict; + this.stream = stream; this.dict = dict; this.numComps = dict.get("N"); break; + case "Indexed": + this.stream = stream; + this.dict = stream.dict; + var base = cs[1]; + var hival = cs[2]; + assertWellFormed(0 <= hival && hival <= 255, "hival in range"); + var lookupTable = cs[3]; + TODO("implement 'Indexed' color space"); + this.numComps = 3; // HACK + break; default: - error("unrecognized color space object"); + error("unrecognized color space object '"+ mode +"'"); } } else { error("unrecognized color space object"); @@ -3367,7 +3555,7 @@ var PDFFunction = (function() { if (!dict) dict = fn; - const types = [this.constructSampled, + var types = [this.constructSampled, null, this.constructInterpolated, this.constructStiched, @@ -3439,6 +3627,7 @@ var PDFFunction = (function() { v = encode[i2] + ((v - domain[i2]) * (encode[i2 + 1] - encode[i2]) / (domain[i2 + 1] - domain[i2])); + // clip to the size args[i] = clip(v, 0, size[i] - 1); } @@ -3466,6 +3655,7 @@ var PDFFunction = (function() { // decode v = decode[i2] + (v * (decode[i2 + 1] - decode[i2]) / ((1 << bps) - 1)); + // clip to the domain output.push(clip(v, range[i2], range[i2 + 1])); } diff --git a/pdf_worker.js b/pdf_worker.js new file mode 100644 index 000000000..fa29428c7 --- /dev/null +++ b/pdf_worker.js @@ -0,0 +1,88 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +var consoleTimer = {}; +var console = { + log: function log() { + var args = Array.prototype.slice.call(arguments); + postMessage({ + action: "log", + data: args + }); + }, + + time: function(name) { + consoleTimer[name] = Date.now(); + }, + + timeEnd: function(name) { + var time = consoleTimer[name]; + if (time == null) { + throw "Unkown timer name " + name; + } + this.log("Timer:", name, Date.now() - time); + } +} + +// +importScripts("canvas_proxy.js"); +importScripts("pdf.js"); +importScripts("fonts.js"); +importScripts("glyphlist.js") + +// Use the JpegStreamProxy proxy. +JpegStream = JpegStreamProxy; + +// Create the WebWorkerProxyCanvas. +var canvas = new CanvasProxy(1224, 1584); + +// Listen for messages from the main thread. +var pdfDocument = null; +onmessage = function(event) { + var data = event.data; + // If there is no pdfDocument yet, then the sent data is the PDFDocument. + if (!pdfDocument) { + pdfDocument = new PDFDoc(new Stream(data)); + postMessage({ + action: "pdf_num_pages", + data: pdfDocument.numPages + }); + return; + } + // User requested to render a certain page. + else { + console.time("compile"); + + // Let's try to render the first page... + var page = pdfDocument.getPage(parseInt(data)); + + // page.compile will collect all fonts for us, once we have loaded them + // we can trigger the actual page rendering with page.display + var fonts = []; + var gfx = new CanvasGraphics(canvas.getContext("2d"), CanvasProxy); + page.compile(gfx, fonts); + console.timeEnd("compile"); + + console.time("fonts"); + // Inspect fonts and translate the missing one. + var count = fonts.length; + for (var i = 0; i < count; i++) { + var font = fonts[i]; + if (Fonts[font.name]) { + fontsReady = fontsReady && !Fonts[font.name].loading; + continue; + } + + // This "builds" the font and sents it over to the main thread. + new Font(font.name, font.file, font.properties); + } + console.timeEnd("fonts"); + + console.time("display"); + page.display(gfx); + canvas.flush(); + console.timeEnd("display"); + } +} diff --git a/test.py b/test.py deleted file mode 100644 index dae723b8a..000000000 --- a/test.py +++ /dev/null @@ -1,221 +0,0 @@ -import json, os, sys, subprocess, urllib2 -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from urlparse import urlparse - -ANAL = True -DEFAULT_MANIFEST_FILE = 'test_manifest.json' -REFDIR = 'ref' -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 - -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 set_up(manifestFile): - # Only serve files from a pdf.js clone - assert not ANAL or os.path.isfile('pdf.js') and os.path.isdir('.git') - - testBrowsers = [ b for b in - ( 'firefox4', ) -#'chrome12', 'chrome13', 'firefox5', '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 - 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)): - ref = None - try: - path = os.path.join(pfx, str(page + 1)) - f = open(path) - ref = f.read() - f.close() - except IOError, ioe: - continue - - snapshot = results[page] - if ref != snapshot: - print 'TEST-UNEXPECTED-FAIL | eq', task['id'], '| in', browser, '| rendering of page', page + 1, '!= reference rendering' - passed = False - 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 - 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 main(args): - manifestFile = args[0] if len(args) == 1 else DEFAULT_MANIFEST_FILE - set_up(manifestFile) - server = HTTPServer(('127.0.0.1', 8080), PDFTestHandler) - while not State.done: - server.handle_request() - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore new file mode 100644 index 000000000..ef853ef61 --- /dev/null +++ b/test/pdfs/.gitignore @@ -0,0 +1 @@ +pdf.pdf diff --git a/tests/canvas.pdf b/test/pdfs/canvas.pdf similarity index 100% rename from tests/canvas.pdf rename to test/pdfs/canvas.pdf diff --git a/tests/pdf.pdf.link b/test/pdfs/pdf.pdf.link similarity index 100% rename from tests/pdf.pdf.link rename to test/pdfs/pdf.pdf.link diff --git a/tests/tracemonkey.pdf b/test/pdfs/tracemonkey.pdf similarity index 100% rename from tests/tracemonkey.pdf rename to test/pdfs/tracemonkey.pdf diff --git a/test/resources/browser_manifests/browser_manifest.json.mac b/test/resources/browser_manifests/browser_manifest.json.mac new file mode 100644 index 000000000..7c9dda943 --- /dev/null +++ b/test/resources/browser_manifests/browser_manifest.json.mac @@ -0,0 +1,10 @@ +[ + { + "name":"firefox5", + "path":"/Applications/Firefox.app" + }, + { + "name":"firefox6", + "path":"/Users/sayrer/firefoxen/Aurora.app" + } +] diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest new file mode 100644 index 000000000..614f31c3a --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome.manifest @@ -0,0 +1,4 @@ +content specialpowers chrome/specialpowers/content/ +component {59a52458-13e0-4d93-9d85-a637344f29a1} components/SpecialPowersObserver.js +contract @mozilla.org/special-powers-observer;1 {59a52458-13e0-4d93-9d85-a637344f29a1} +category profile-after-change @mozilla.org/special-powers-observer;1 @mozilla.org/special-powers-observer;1 diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js new file mode 100644 index 000000000..538b104eb --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/chrome/specialpowers/content/specialpowers.js @@ -0,0 +1,372 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Special Powers code + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Clint Talbert cmtalbert@gmail.com + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK *****/ +/* This code is loaded in every child process that is started by mochitest in + * order to be used as a replacement for UniversalXPConnect + */ + +var Ci = Components.interfaces; +var Cc = Components.classes; + +function SpecialPowers(window) { + this.window = window; + bindDOMWindowUtils(this, window); + this._encounteredCrashDumpFiles = []; + this._unexpectedCrashDumpFiles = { }; + this._crashDumpDir = null; + this._pongHandlers = []; + this._messageListener = this._messageReceived.bind(this); + addMessageListener("SPPingService", this._messageListener); +} + +function bindDOMWindowUtils(sp, window) { + var util = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + // This bit of magic brought to you by the letters + // B Z, and E, S and the number 5. + // + // Take all of the properties on the nsIDOMWindowUtils-implementing + // object, and rebind them onto a new object with a stub that uses + // apply to call them from this privileged scope. This way we don't + // have to explicitly stub out new methods that appear on + // nsIDOMWindowUtils. + var proto = Object.getPrototypeOf(util); + var target = {}; + function rebind(desc, prop) { + if (prop in desc && typeof(desc[prop]) == "function") { + var oldval = desc[prop]; + desc[prop] = function() { return oldval.apply(util, arguments); }; + } + } + for (var i in proto) { + var desc = Object.getOwnPropertyDescriptor(proto, i); + rebind(desc, "get"); + rebind(desc, "set"); + rebind(desc, "value"); + Object.defineProperty(target, i, desc); + } + sp.DOMWindowUtils = target; +} + +SpecialPowers.prototype = { + toString: function() { return "[SpecialPowers]"; }, + sanityCheck: function() { return "foo"; }, + + // This gets filled in in the constructor. + DOMWindowUtils: undefined, + + // Mimic the get*Pref API + getBoolPref: function(aPrefName) { + return (this._getPref(aPrefName, 'BOOL')); + }, + getIntPref: function(aPrefName) { + return (this._getPref(aPrefName, 'INT')); + }, + getCharPref: function(aPrefName) { + return (this._getPref(aPrefName, 'CHAR')); + }, + getComplexValue: function(aPrefName, aIid) { + return (this._getPref(aPrefName, 'COMPLEX', aIid)); + }, + + // Mimic the set*Pref API + setBoolPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'BOOL', aValue)); + }, + setIntPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'INT', aValue)); + }, + setCharPref: function(aPrefName, aValue) { + return (this._setPref(aPrefName, 'CHAR', aValue)); + }, + setComplexValue: function(aPrefName, aIid, aValue) { + return (this._setPref(aPrefName, 'COMPLEX', aValue, aIid)); + }, + + // Mimic the clearUserPref API + clearUserPref: function(aPrefName) { + var msg = {'op':'clear', 'prefName': aPrefName, 'prefType': ""}; + sendSyncMessage('SPPrefService', msg); + }, + + // Private pref functions to communicate to chrome + _getPref: function(aPrefName, aPrefType, aIid) { + var msg = {}; + if (aIid) { + // Overloading prefValue to handle complex prefs + msg = {'op':'get', 'prefName': aPrefName, 'prefType':aPrefType, 'prefValue':[aIid]}; + } else { + msg = {'op':'get', 'prefName': aPrefName,'prefType': aPrefType}; + } + return(sendSyncMessage('SPPrefService', msg)[0]); + }, + _setPref: function(aPrefName, aPrefType, aValue, aIid) { + var msg = {}; + if (aIid) { + msg = {'op':'set','prefName':aPrefName, 'prefType': aPrefType, 'prefValue': [aIid,aValue]}; + } else { + msg = {'op':'set', 'prefName': aPrefName, 'prefType': aPrefType, 'prefValue': aValue}; + } + return(sendSyncMessage('SPPrefService', msg)[0]); + }, + + //XXX: these APIs really ought to be removed, they're not e10s-safe. + // (also they're pretty Firefox-specific) + _getTopChromeWindow: function(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + }, + _getDocShell: function(window) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + }, + _getMUDV: function(window) { + return this._getDocShell(window).contentViewer + .QueryInterface(Ci.nsIMarkupDocumentViewer); + }, + _getAutoCompletePopup: function(window) { + return this._getTopChromeWindow(window).document + .getElementById("PopupAutoComplete"); + }, + addAutoCompletePopupEventListener: function(window, listener) { + this._getAutoCompletePopup(window).addEventListener("popupshowing", + listener, + false); + }, + removeAutoCompletePopupEventListener: function(window, listener) { + this._getAutoCompletePopup(window).removeEventListener("popupshowing", + listener, + false); + }, + isBackButtonEnabled: function(window) { + return !this._getTopChromeWindow(window).document + .getElementById("Browser:Back") + .hasAttribute("disabled"); + }, + + addChromeEventListener: function(type, listener, capture, allowUntrusted) { + addEventListener(type, listener, capture, allowUntrusted); + }, + removeChromeEventListener: function(type, listener, capture) { + removeEventListener(type, listener, capture); + }, + + getFullZoom: function(window) { + return this._getMUDV(window).fullZoom; + }, + setFullZoom: function(window, zoom) { + this._getMUDV(window).fullZoom = zoom; + }, + getTextZoom: function(window) { + return this._getMUDV(window).textZoom; + }, + setTextZoom: function(window, zoom) { + this._getMUDV(window).textZoom = zoom; + }, + + createSystemXHR: function() { + return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + }, + + gc: function() { + this.DOMWindowUtils.garbageCollect(); + }, + + hasContentProcesses: function() { + try { + var rt = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); + return rt.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + } catch (e) { + return true; + } + }, + + registerProcessCrashObservers: function() { + addMessageListener("SPProcessCrashService", this._messageListener); + sendSyncMessage("SPProcessCrashService", { op: "register-observer" }); + }, + + _messageReceived: function(aMessage) { + switch (aMessage.name) { + case "SPProcessCrashService": + if (aMessage.json.type == "crash-observed") { + var self = this; + aMessage.json.dumpIDs.forEach(function(id) { + self._encounteredCrashDumpFiles.push(id + ".dmp"); + self._encounteredCrashDumpFiles.push(id + ".extra"); + }); + } + break; + + case "SPPingService": + if (aMessage.json.op == "pong") { + var handler = this._pongHandlers.shift(); + if (handler) { + handler(); + } + } + break; + } + return true; + }, + + removeExpectedCrashDumpFiles: function(aExpectingProcessCrash) { + var success = true; + if (aExpectingProcessCrash) { + var message = { + op: "delete-crash-dump-files", + filenames: this._encounteredCrashDumpFiles + }; + if (!sendSyncMessage("SPProcessCrashService", message)[0]) { + success = false; + } + } + this._encounteredCrashDumpFiles.length = 0; + return success; + }, + + findUnexpectedCrashDumpFiles: function() { + var self = this; + var message = { + op: "find-crash-dump-files", + crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles + }; + var crashDumpFiles = sendSyncMessage("SPProcessCrashService", message)[0]; + crashDumpFiles.forEach(function(aFilename) { + self._unexpectedCrashDumpFiles[aFilename] = true; + }); + return crashDumpFiles; + }, + + executeAfterFlushingMessageQueue: function(aCallback) { + this._pongHandlers.push(aCallback); + sendAsyncMessage("SPPingService", { op: "ping" }); + }, + + executeSoon: function(aFunc) { + var tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager); + tm.mainThread.dispatch({ + run: function() { + aFunc(); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }, + + /* from http://mxr.mozilla.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/quit.js + * by Bob Clary, Jeff Walden, and Robert Sayre. + */ + quitApplication: function() { + function canQuitApplication() + { + var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + if (!os) + return true; + + try { + var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); + os.notifyObservers(cancelQuit, "quit-application-requested", null); + + // Something aborted the quit process. + if (cancelQuit.data) + return false; + } catch (ex) {} + return true; + } + + if (!canQuitApplication()) + return false; + + var appService = Cc['@mozilla.org/toolkit/app-startup;1'].getService(Ci.nsIAppStartup); + appService.quit(Ci.nsIAppStartup.eForceQuit); + return true; + } +}; + +// Expose everything but internal APIs (starting with underscores) to +// web content. +SpecialPowers.prototype.__exposedProps__ = {}; +for each (i in Object.keys(SpecialPowers.prototype).filter(function(v) {return v.charAt(0) != "_";})) { + SpecialPowers.prototype.__exposedProps__[i] = "r"; +} + +// Attach our API to the window. +function attachSpecialPowersToWindow(aWindow) { + try { + if ((aWindow !== null) && + (aWindow !== undefined) && + (aWindow.wrappedJSObject) && + !(aWindow.wrappedJSObject.SpecialPowers)) { + aWindow.wrappedJSObject.SpecialPowers = new SpecialPowers(aWindow); + } + } catch(ex) { + dump("TEST-INFO | specialpowers.js | Failed to attach specialpowers to window exception: " + ex + "\n"); + } +} + +// This is a frame script, so it may be running in a content process. +// In any event, it is targeted at a specific "tab", so we listen for +// the DOMWindowCreated event to be notified about content windows +// being created in this context. + +function SpecialPowersManager() { + addEventListener("DOMWindowCreated", this, false); +} + +SpecialPowersManager.prototype = { + handleEvent: function handleEvent(aEvent) { + var window = aEvent.target.defaultView; + + // Need to make sure we are called on what we care about - + // content windows. DOMWindowCreated is called on *all* HTMLDocuments, + // some of which belong to chrome windows or other special content. + // + var uri = window.document.documentURIObject; + if (uri.scheme === "chrome" || uri.spec.split(":")[0] == "about") { + return; + } + + attachSpecialPowersToWindow(window); + } +}; + +var specialpowersmanager = new SpecialPowersManager(); diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js b/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js new file mode 100755 index 000000000..90655e2e7 --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/components/SpecialPowersObserver.js @@ -0,0 +1,293 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Special Powers code + * + * The Initial Developer of the Original Code is + * Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jesse Ruderman + * Robert Sayre + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK *****/ + +// Based on: +// https://bugzilla.mozilla.org/show_bug.cgi?id=549539 +// https://bug549539.bugzilla.mozilla.org/attachment.cgi?id=429661 +// https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_1.9.3 +// http://mxr.mozilla.org/mozilla-central/source/toolkit/components/console/hudservice/HUDService.jsm#3240 +// https://developer.mozilla.org/en/how_to_build_an_xpcom_component_in_javascript + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const Cc = Components.classes; +const Ci = Components.interfaces; + +const CHILD_SCRIPT = "chrome://specialpowers/content/specialpowers.js" + +/** + * Special Powers Exception - used to throw exceptions nicely + **/ +function SpecialPowersException(aMsg) { + this.message = aMsg; + this.name = "SpecialPowersException"; +} + +SpecialPowersException.prototype.toString = function() { + return this.name + ': "' + this.message + '"'; +}; + +/* XPCOM gunk */ +function SpecialPowersObserver() { + this._isFrameScriptLoaded = false; + this._messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. + getService(Ci.nsIChromeFrameMessageManager); +} + +SpecialPowersObserver.prototype = { + classDescription: "Special powers Observer for use in testing.", + classID: Components.ID("{59a52458-13e0-4d93-9d85-a637344f29a1}"), + contractID: "@mozilla.org/special-powers-observer;1", + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver]), + _xpcom_categories: [{category: "profile-after-change", service: true }], + + observe: function(aSubject, aTopic, aData) + { + switch (aTopic) { + case "profile-after-change": + this.init(); + break; + + case "chrome-document-global-created": + if (!this._isFrameScriptLoaded) { + // Register for any messages our API needs us to handle + this._messageManager.addMessageListener("SPPrefService", this); + this._messageManager.addMessageListener("SPProcessCrashService", this); + this._messageManager.addMessageListener("SPPingService", this); + + this._messageManager.loadFrameScript(CHILD_SCRIPT, true); + this._isFrameScriptLoaded = true; + } + break; + + case "xpcom-shutdown": + this.uninit(); + break; + + case "plugin-crashed": + case "ipc:content-shutdown": + function addDumpIDToMessage(propertyName) { + var id = aSubject.getPropertyAsAString(propertyName); + if (id) { + message.dumpIDs.push(id); + } + } + + var message = { type: "crash-observed", dumpIDs: [] }; + aSubject = aSubject.QueryInterface(Ci.nsIPropertyBag2); + if (aTopic == "plugin-crashed") { + addDumpIDToMessage("pluginDumpID"); + addDumpIDToMessage("browserDumpID"); + } else { // ipc:content-shutdown + addDumpIDToMessage("dumpID"); + } + this._messageManager.sendAsyncMessage("SPProcessCrashService", message); + break; + } + }, + + init: function() + { + var obs = Services.obs; + obs.addObserver(this, "xpcom-shutdown", false); + obs.addObserver(this, "chrome-document-global-created", false); + }, + + uninit: function() + { + var obs = Services.obs; + obs.removeObserver(this, "chrome-document-global-created", false); + this.removeProcessCrashObservers(); + }, + + addProcessCrashObservers: function() { + if (this._processCrashObserversRegistered) { + return; + } + + Services.obs.addObserver(this, "plugin-crashed", false); + Services.obs.addObserver(this, "ipc:content-shutdown", false); + this._processCrashObserversRegistered = true; + }, + + removeProcessCrashObservers: function() { + if (!this._processCrashObserversRegistered) { + return; + } + + Services.obs.removeObserver(this, "plugin-crashed"); + Services.obs.removeObserver(this, "ipc:content-shutdown"); + this._processCrashObserversRegistered = false; + }, + + getCrashDumpDir: function() { + if (!this._crashDumpDir) { + var directoryService = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + this._crashDumpDir = directoryService.get("ProfD", Ci.nsIFile); + this._crashDumpDir.append("minidumps"); + } + return this._crashDumpDir; + }, + + deleteCrashDumpFiles: function(aFilenames) { + var crashDumpDir = this.getCrashDumpDir(); + if (!crashDumpDir.exists()) { + return false; + } + + var success = aFilenames.length != 0; + aFilenames.forEach(function(crashFilename) { + var file = crashDumpDir.clone(); + file.append(crashFilename); + if (file.exists()) { + file.remove(false); + } else { + success = false; + } + }); + return success; + }, + + findCrashDumpFiles: function(aToIgnore) { + var crashDumpDir = this.getCrashDumpDir(); + var entries = crashDumpDir.exists() && crashDumpDir.directoryEntries; + if (!entries) { + return []; + } + + var crashDumpFiles = []; + while (entries.hasMoreElements()) { + var file = entries.getNext().QueryInterface(Ci.nsIFile); + var path = String(file.path); + if (path.match(/\.(dmp|extra)$/) && !aToIgnore[path]) { + crashDumpFiles.push(path); + } + } + return crashDumpFiles.concat(); + }, + + /** + * messageManager callback function + * This will get requests from our API in the window and process them in chrome for it + **/ + receiveMessage: function(aMessage) { + switch(aMessage.name) { + case "SPPrefService": + var prefs = Services.prefs; + var prefType = aMessage.json.prefType.toUpperCase(); + var prefName = aMessage.json.prefName; + var prefValue = "prefValue" in aMessage.json ? aMessage.json.prefValue : null; + + if (aMessage.json.op == "get") { + if (!prefName || !prefType) + throw new SpecialPowersException("Invalid parameters for get in SPPrefService"); + } else if (aMessage.json.op == "set") { + if (!prefName || !prefType || prefValue === null) + throw new SpecialPowersException("Invalid parameters for set in SPPrefService"); + } else if (aMessage.json.op == "clear") { + if (!prefName) + throw new SpecialPowersException("Invalid parameters for clear in SPPrefService"); + } else { + throw new SpecialPowersException("Invalid operation for SPPrefService"); + } + // Now we make the call + switch(prefType) { + case "BOOL": + if (aMessage.json.op == "get") + return(prefs.getBoolPref(prefName)); + else + return(prefs.setBoolPref(prefName, prefValue)); + case "INT": + if (aMessage.json.op == "get") + return(prefs.getIntPref(prefName)); + else + return(prefs.setIntPref(prefName, prefValue)); + case "CHAR": + if (aMessage.json.op == "get") + return(prefs.getCharPref(prefName)); + else + return(prefs.setCharPref(prefName, prefValue)); + case "COMPLEX": + if (aMessage.json.op == "get") + return(prefs.getComplexValue(prefName, prefValue[0])); + else + return(prefs.setComplexValue(prefName, prefValue[0], prefValue[1])); + case "": + if (aMessage.json.op == "clear") { + prefs.clearUserPref(prefName); + return; + } + } + break; + + case "SPProcessCrashService": + switch (aMessage.json.op) { + case "register-observer": + this.addProcessCrashObservers(); + break; + case "unregister-observer": + this.removeProcessCrashObservers(); + break; + case "delete-crash-dump-files": + return this.deleteCrashDumpFiles(aMessage.json.filenames); + case "find-crash-dump-files": + return this.findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore); + default: + throw new SpecialPowersException("Invalid operation for SPProcessCrashService"); + } + break; + + case "SPPingService": + if (aMessage.json.op == "ping") { + aMessage.target + .QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader + .messageManager + .sendAsyncMessage("SPPingService", { op: "pong" }); + } + break; + + default: + throw new SpecialPowersException("Unrecognized Special Powers API"); + } + } +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([SpecialPowersObserver]); diff --git a/test/resources/firefox/extensions/special-powers@mozilla.org/install.rdf b/test/resources/firefox/extensions/special-powers@mozilla.org/install.rdf new file mode 100644 index 000000000..db8de988e --- /dev/null +++ b/test/resources/firefox/extensions/special-powers@mozilla.org/install.rdf @@ -0,0 +1,26 @@ + + + + + + special-powers@mozilla.org + 2010.07.23 + 2 + + + + + toolkit@mozilla.org + 3.0 + 7.0a1 + + + + + Special Powers + Special powers for use in testing. + Mozilla + + diff --git a/test/resources/firefox/user.js b/test/resources/firefox/user.js new file mode 100644 index 000000000..7ca293923 --- /dev/null +++ b/test/resources/firefox/user.js @@ -0,0 +1,35 @@ +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 diff --git a/test/test.py b/test/test.py new file mode 100644 index 000000000..a89aa0a7b --- /dev/null +++ b/test/test.py @@ -0,0 +1,431 @@ +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.set_usage(USAGE_EXAMPLE) + + def verifyOptions(self, options): + if options.masterMode and options.manifestFile: + self.error("--masterMode and --manifestFile must not be specified at the same time.") + if not options.manifestFile: + options.manifestFile = DEFAULT_MANIFEST_FILE + if options.browser and options.browserManifestFile: + print "Warning: ignoring browser argument since manifest file was also supplied" + return options + +def prompt(question): + '''Return True iff the user answered "yes" to |question|.''' + inp = raw_input(question +' [yes/no] > ') + return inp == 'yes' + +MIMEs = { + '.css': 'text/css', + '.html': 'text/html', + '.js': 'application/javascript', + '.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 + 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 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 not (prefix == DOC_ROOT + 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, 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 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"})] + else: + print "No test browsers found. Use --browserManifest or --browser args." + + with open(options.manifestFile) as mf: + manifestList = json.load(mf) + + for item in manifestList: + f, isLink = item['file'], item.get('link', False) + 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.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' + # XXX need to dump this always, somehow, when we have + # the reference repository + if State.masterMode: + 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 main(): + 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) + +if __name__ == '__main__': + main() diff --git a/test_manifest.json b/test/test_manifest.json similarity index 70% rename from test_manifest.json rename to test/test_manifest.json index 036b7aafc..e4a7ada81 100644 --- a/test_manifest.json +++ b/test/test_manifest.json @@ -1,21 +1,21 @@ [ { "id": "tracemonkey-eq", - "file": "tests/tracemonkey.pdf", + "file": "pdfs/tracemonkey.pdf", "rounds": 1, "type": "eq" }, { "id": "tracemonkey-fbf", - "file": "tests/tracemonkey.pdf", + "file": "pdfs/tracemonkey.pdf", "rounds": 2, "type": "fbf" }, { "id": "html5-canvas-cheat-sheet-load", - "file": "tests/canvas.pdf", + "file": "pdfs/canvas.pdf", "rounds": 1, "type": "load" }, { "id": "pdfspec-load", - "file": "tests/pdf.pdf", + "file": "pdfs/pdf.pdf", "link": true, "rounds": 1, "type": "load" diff --git a/test_slave.html b/test/test_slave.html similarity index 61% rename from test_slave.html rename to test/test_slave.html index cff9b3f7d..08a3804f3 100644 --- a/test_slave.html +++ b/test/test_slave.html @@ -1,9 +1,10 @@ pdf.js test slave - - - + + + + + + + + +
+ + + + + + -- + +
+ +
+ + + +
+ + + diff --git a/worker_client.js b/worker_client.js new file mode 100644 index 000000000..359a5f7c1 --- /dev/null +++ b/worker_client.js @@ -0,0 +1,272 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +if (typeof console.time == "undefined") { + var consoleTimer = {}; + console.time = function(name) { + consoleTimer[name] = Date.now(); + }; + + console.timeEnd = function(name) { + var time = consoleTimer[name]; + if (time == null) { + throw "Unkown timer name " + name; + } + this.log("Timer:", name, Date.now() - time); + }; +} + +function WorkerPDFDoc(canvas) { + var timer = null + + this.ctx = canvas.getContext("2d"); + this.canvas = canvas; + this.worker = new Worker('pdf_worker.js'); + + this.numPage = 1; + this.numPages = null; + + var imagesList = {}; + var canvasList = { + 0: canvas + }; + var patternList = {}; + var gradient; + + var currentX = 0; + var currentXStack = []; + + var ctxSpecial = { + "$setCurrentX": function(value) { + currentX = value; + }, + + "$addCurrentX": function(value) { + currentX += value; + }, + + "$saveCurrentX": function() { + currentXStack.push(currentX); + }, + + "$restoreCurrentX": function() { + currentX = currentXStack.pop(); + }, + + "$showText": function(y, text) { + this.translate(currentX, -1 * y); + this.fillText(text, 0, 0); + currentX += this.measureText(text).width; + }, + + "$putImageData": function(imageData, x, y) { + var imgData = this.getImageData(0, 0, imageData.width, imageData.height); + + // Store the .data property to avaid property lookups. + var imageRealData = imageData.data; + var imgRealData = imgData.data; + + // Copy over the imageData. + var len = imageRealData.length; + while (len--) + imgRealData[len] = imageRealData[len] + + this.putImageData(imgData, x, y); + }, + + "$drawImage": function(id, x, y, sx, sy, swidth, sheight) { + var image = imagesList[id]; + if (!image) { + throw "Image not found: " + id; + } + this.drawImage(image, x, y, image.width, image.height, + sx, sy, swidth, sheight); + }, + + "$drawCanvas": function(id, x, y, sx, sy, swidth, sheight) { + var canvas = canvasList[id]; + if (!canvas) { + throw "Canvas not found"; + } + if (sheight != null) { + this.drawImage(canvas, x, y, canvas.width, canvas.height, + sx, sy, swidth, sheight); + } else { + this.drawImage(canvas, x, y, canvas.width, canvas.height); + } + }, + + "$createLinearGradient": function(x0, y0, x1, y1) { + gradient = this.createLinearGradient(x0, y0, x1, y1); + }, + + "$createPatternFromCanvas": function(patternId, canvasId, kind) { + var canvas = canvasList[canvasId]; + if (!canvas) { + throw "Canvas not found"; + } + patternList[patternId] = this.createPattern(canvas, kind); + }, + + "$addColorStop": function(i, rgba) { + gradient.addColorStop(i, rgba); + }, + + "$fillStyleGradient": function() { + this.fillStyle = gradient; + }, + + "$fillStylePattern": function(id) { + var pattern = patternList[id]; + if (!pattern) { + throw "Pattern not found"; + } + this.fillStyle = pattern; + }, + + "$strokeStyleGradient": function() { + this.strokeStyle = gradient; + }, + + "$strokeStylePattern": function(id) { + var pattern = patternList[id]; + if (!pattern) { + throw "Pattern not found"; + } + this.strokeStyle = pattern; + } + } + + function renderProxyCanvas(canvas, cmdQueue) { + var ctx = canvas.getContext("2d"); + var cmdQueueLength = cmdQueue.length; + for (var i = 0; i < cmdQueueLength; i++) { + var opp = cmdQueue[i]; + if (opp[0] == "$") { + ctx[opp[1]] = opp[2]; + } else if (opp[0] in ctxSpecial) { + ctxSpecial[opp[0]].apply(ctx, opp[1]); + } else { + ctx[opp[0]].apply(ctx, opp[1]); + } + } + } + + /** + * Functions to handle data sent by the WebWorker. + */ + var actionHandler = { + "log": function(data) { + console.log.apply(console, data); + }, + + "pdf_num_pages": function(data) { + this.numPages = parseInt(data); + if (this.loadCallback) { + this.loadCallback(); + } + }, + + "font": function(data) { + var base64 = window.btoa(data.raw); + + // Add the @font-face rule to the document + var url = "url(data:" + data.mimetype + ";base64," + base64 + ");"; + var rule = "@font-face { font-family:'" + data.fontName + "';src:" + url + "}"; + var styleSheet = document.styleSheets[0]; + styleSheet.insertRule(rule, styleSheet.length); + + // Just adding the font-face to the DOM doesn't make it load. It + // seems it's loaded once Gecko notices it's used. Therefore, + // add a div on the page using the loaded font. + var div = document.createElement("div"); + var style = 'font-family:"' + data.fontName + + '";position: absolute;top:-99999;left:-99999;z-index:-99999'; + div.setAttribute("style", style); + document.body.appendChild(div); + }, + + "jpeg_stream": function(data) { + var img = new Image(); + img.src = "data:image/jpeg;base64," + window.btoa(data.raw); + imagesList[data.id] = img; + }, + + "canvas_proxy_cmd_queue": function(data) { + var id = data.id; + var cmdQueue = data.cmdQueue; + + // Check if there is already a canvas with the given id. If not, + // create a new canvas. + if (!canvasList[id]) { + var newCanvas = document.createElement("canvas"); + newCanvas.width = data.width; + newCanvas.height = data.height; + canvasList[id] = newCanvas; + } + + // There might be fonts that need to get loaded. Shedule the + // rendering at the end of the event queue ensures this. + setTimeout(function() { + if (id == 0) { + console.time("canvas rendering"); + var ctx = this.ctx; + ctx.save(); + ctx.fillStyle = "rgb(255, 255, 255)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + } + renderProxyCanvas(canvasList[id], cmdQueue); + if (id == 0) console.timeEnd("canvas rendering") + }, 0, this); + } + } + + // List to the WebWorker for data and call actionHandler on it. + this.worker.onmessage = function(event) { + var data = event.data; + if (data.action in actionHandler) { + actionHandler[data.action].call(this, data.data); + } else { + throw "Unkown action from worker: " + data.action; + } + } +} + +WorkerPDFDoc.prototype.open = function(url, callback) { + var req = new XMLHttpRequest(); + req.open("GET", url); + req.mozResponseType = req.responseType = "arraybuffer"; + req.expected = (document.URL.indexOf("file:") == 0) ? 0 : 200; + req.onreadystatechange = function() { + if (req.readyState == 4 && req.status == req.expected) { + var data = req.mozResponseArrayBuffer || req.mozResponse || + req.responseArrayBuffer || req.response; + + this.loadCallback = callback; + this.worker.postMessage(data); + this.showPage(this.numPage); + } + }.bind(this); + req.send(null); +} + +WorkerPDFDoc.prototype.showPage = function(numPage) { + this.numPage = parseInt(numPage); + this.worker.postMessage(numPage); + if (this.onChangePage) { + this.onChangePage(numPage); + } +} + +WorkerPDFDoc.prototype.nextPage = function() { + if (this.numPage == this.numPages) return; + this.showPage(++this.numPage); +} + +WorkerPDFDoc.prototype.prevPage = function() { + if (this.numPage == 1) return; + this.showPage(--this.numPage); +}