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 971d8d257..1169e21c6 100644 --- a/fonts.js +++ b/fonts.js @@ -135,15 +135,28 @@ var Font = (function () { break; } + var data = this.font; Fonts[name] = { - data: this.font, + data: data, properties: properties, loading: true, cache: Object.create(null) } - // Attach the font to the document - this.bind(); + // Convert data to a string. + var dataStr = ""; + var length = data.length; + for (var i = 0; i < length; ++i) + dataStr += String.fromCharCode(data[i]); + + // Attach the font to the document. If this script is runnig in a worker, + // call `bindWorker`, which sends stuff over to the main thread. + if (typeof window != "undefined") { + this.bindDOM(dataStr); + } else { + this.bindWorker(dataStr); + } + }; function stringToArray(str) { @@ -735,12 +748,21 @@ var Font = (function () { return fontData; }, - bind: function font_bind() { - var data = this.font; + bindWorker: function font_bind_worker(dataStr) { + postMessage({ + action: "font", + data: { + raw: dataStr, + fontName: this.name, + mimetype: this.mimetype + } + }); + }, + + bindDOM: function font_bind_dom(dataStr) { var fontName = this.name; /** Hack begin */ - // Actually there is not event when a font has finished downloading so // the following code are a dirty hack to 'guess' when a font is ready var canvas = document.createElement("canvas"); @@ -754,7 +776,7 @@ var Font = (function () { // Get the font size canvas think it will be for 'spaces' var ctx = canvas.getContext("2d"); ctx.font = "bold italic 20px " + fontName + ", Symbol, Arial"; - var testString = " "; + var testString = " "; // When debugging use the characters provided by the charsets to visually // see what's happening instead of 'spaces' @@ -810,14 +832,9 @@ var Font = (function () { }, 30, this); /** Hack end */ - - // Get the base64 encoding of the binary font data - var str = ""; - var length = data.length; - for (var i = 0; i < length; ++i) - str += String.fromCharCode(data[i]); - - var base64 = window.btoa(str); + + // Convert the data string and add it to the page. + var base64 = window.btoa(dataStr); // Add the @font-face rule to the document var url = "url(data:" + this.mimetype + ";base64," + base64 + ");"; diff --git a/pdf.js b/pdf.js index be124c994..a7de0bb35 100644 --- a/pdf.js +++ b/pdf.js @@ -2269,15 +2269,34 @@ var Encodings = { } }; +function ScratchCanvas(width, height) { + var canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + return canvas; +} + var CanvasGraphics = (function() { - function constructor(canvasCtx) { + function constructor(canvasCtx, imageCanvas) { this.ctx = canvasCtx; this.current = new CanvasExtraState(); this.stateStack = [ ]; this.pendingClip = null; this.res = null; this.xobjs = null; - this.map = { + this.ScratchCanvas = imageCanvas || ScratchCanvas; + } + + var LINE_CAP_STYLES = [ "butt", "round", "square" ]; + var LINE_JOIN_STYLES = [ "miter", "round", "bevel" ]; + var NORMAL_CLIP = {}; + var EO_CLIP = {}; + + // Used for tiling patterns + var PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; + + constructor.prototype = { + map: { // Graphics state w: "setLineWidth", J: "setLineCap", @@ -2370,18 +2389,8 @@ var CanvasGraphics = (function() { // Compatibility BX: "beginCompat", EX: "endCompat", - }; - } - - var LINE_CAP_STYLES = [ "butt", "round", "square" ]; - var LINE_JOIN_STYLES = [ "miter", "round", "bevel" ]; - var NORMAL_CLIP = {}; - var EO_CLIP = {}; - - // Used for tiling patterns - var PAINT_TYPE_COLORED = 1, PAINT_TYPE_UNCOLORED = 2; - - constructor.prototype = { + }, + translateFont: function(fontDict, xref, resources) { var fd = fontDict.get("FontDescriptor"); if (!fd) @@ -2663,12 +2672,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(); } @@ -2749,6 +2764,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; }, @@ -2803,6 +2821,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); @@ -2810,6 +2831,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; }, @@ -2820,11 +2845,15 @@ var CanvasGraphics = (function() { this.ctx.save(); this.ctx.transform.apply(this.ctx, this.current.textMatrix); this.ctx.scale(1, -1); - this.ctx.translate(0, -2 * this.current.y); - text = Fonts.charsToUnicode(text); - this.ctx.fillText(text, this.current.x, this.current.y); - this.current.x += this.ctx.measureText(text).width; + if (this.ctx.$showText) { + this.ctx.$showText(this.current.y, Fonts.charsToUnicode(text)); + } else { + text = Fonts.charsToUnicode(text); + this.ctx.translate(this.current.x, -1 * this.current.y); + this.ctx.fillText(text, 0, 0); + this.current.x += this.ctx.measureText(text).width; + } this.ctx.restore(); }, @@ -2832,7 +2861,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 { @@ -2971,9 +3004,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"); @@ -3035,6 +3069,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"); @@ -3292,9 +3327,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; @@ -3462,6 +3495,7 @@ var ColorSpace = (function() { break; case "ICCBased": var dict = stream.dict; + this.stream = stream; this.dict = dict; this.numComps = dict.get("N"); @@ -3568,6 +3602,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); } @@ -3595,6 +3630,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/viewer_worker.html b/viewer_worker.html new file mode 100644 index 000000000..51f2b9d8a --- /dev/null +++ b/viewer_worker.html @@ -0,0 +1,46 @@ + +
+