diff --git a/canvas_proxy.js b/canvas_proxy.js index 07ae31a63..83b57682f 100644 --- a/canvas_proxy.js +++ b/canvas_proxy.js @@ -1,260 +1,239 @@ -// var ImageCanvasProxyCounter = 0; -// function ImageCanvasProxy(width, height) { -// this.id = ImageCanvasProxyCounter++; -// this.width = width; -// this.height = height; -// -// // Using `Uint8ClampedArray` seems to be the type of ImageData - at least -// // Firebug tells me so. -// this.imgData = { -// data: Uint8ClampedArray(width * height * 4) -// }; -// } -// -// ImageCanvasProxy.prototype.putImageData = function(imgData) { -// // this.ctx.putImageData(imgData, 0, 0); -// } -// -// ImageCanvasProxy.prototype.getCanvas = function() { -// return this; -// } var JpegStreamProxyCounter = 0; // WebWorker Proxy for JpegStream. var JpegStreamProxy = (function() { - function constructor(bytes, dict) { - this.id = JpegStreamProxyCounter++; - this.dict = dict; + function constructor(bytes, dict) { + this.id = JpegStreamProxyCounter++; + this.dict = dict; - // create DOM image. - postMessage("jpeg_stream"); - postMessage({ - id: this.id, - str: bytesToString(bytes) - }); + // Tell the main thread to create an image. + postMessage("jpeg_stream"); + postMessage({ + id: this.id, + str: bytesToString(bytes) + }); + } - // var img = new Image(); - // img.src = "data:image/jpeg;base64," + window.btoa(bytesToString(bytes)); - // this.domImage = img; + constructor.prototype = { + getImage: function() { + return this; + }, + getChar: function() { + error("internal error: getChar is not valid on JpegStream"); } + }; - constructor.prototype = { - getImage: function() { - return this; - // return this.domImage; - }, - getChar: function() { - error("internal error: getChar is not valid on JpegStream"); - } - }; - - return constructor; + 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(stack, x0, y0, x1, y1) { - stack.push(["$createLinearGradient", [x0, y0, x1, y1]]); - this.addColorStop = function(i, rgba) { - stack.push(["$addColorStop", [i, rgba]]); - } + stack.push(["$createLinearGradient", [x0, y0, x1, y1]]); + this.addColorStop = function(i, rgba) { + stack.push(["$addColorStop", [i, rgba]]); + } } +// Really simple PatternProxy. var patternProxyCounter = 0; function PatternProxy(stack, object, kind) { - this.id = patternProxyCounter++; + 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(); - stack.push(["$createPatternFromCanvas", [this.id, object.id, kind]]); + 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(); + stack.push(["$createPatternFromCanvas", [this.id, object.id, kind]]); } var canvasProxyCounter = 0; function CanvasProxy(width, height) { - this.id = canvasProxyCounter++; + this.id = canvasProxyCounter++; - var stack = this.$stack = []; + // The `stack` holds the rendering calls and gets flushed to the main thead. + var stack = this.$stack = []; - // Dummy context exposed. - var ctx = {}; - this.getContext = function(type) { - if (type != "2d") { - throw "CanvasProxy can only provide a 2d context."; - } - return ctx; + // 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; + // 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; - var ctxFunc = [ - "createRadialGradient", - "arcTo", - "arc", - "fillText", - "strokeText", - // "drawImage", - // "getImageData", - // "putImageData", - "createImageData", - "drawWindow", - "save", - "restore", - "scale", - "rotate", - "translate", - "transform", - "setTransform", - // "createLinearGradient", - // "createPattern", - "clearRect", - "fillRect", - "strokeRect", - "beginPath", - "closePath", - "moveTo", - "lineTo", - "quadraticCurveTo", - "bezierCurveTo", - "rect", - "fill", - "stroke", - "clip", - "measureText", - "isPointInPath", + // 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", - "$setCurrentX", - "$addCurrentX", - "$saveCurrentX", - "$restoreCurrentX", - "$showText" - ]; + // 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" + ]; - ctx.createPattern = function(object, kind) { - return new PatternProxy(stack, object, kind); + function buildFuncCall(name) { + return function() { + // console.log("funcCall", name) + stack.push([name, Array.prototype.slice.call(arguments)]); } + } + var name; + for (var i = 0; i < ctxFunc.length; i++) { + name = ctxFunc[i]; + ctx[name] = buildFuncCall(name); + } - ctx.createLinearGradient = function(x0, y0, x1, y1) { - return new GradientProxy(stack, x0, y0, x1, y1); - } + // Some function calls that need more work. - ctx.getImageData = function(x, y, w, h) { - return { - width: w, - height: h, - data: Uint8ClampedArray(w * h * 4) - }; - } + ctx.createPattern = function(object, kind) { + return new PatternProxy(stack, object, kind); + } - ctx.putImageData = function(data, x, y, width, height) { - stack.push(["$putImageData", [data, x, y, width, height]]); - } + ctx.createLinearGradient = function(x0, y0, x1, y1) { + return new GradientProxy(stack, x0, y0, x1, y1); + } - 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(); - stack.push(["$drawCanvas", [image.id, x, y, sx, sy, swidth, sheight]]); - } else if(image instanceof JpegStreamProxy) { - stack.push(["$drawImage", [image.id, x, y, sx, sy, swidth, sheight]]) - } else { - throw "unkown type to drawImage"; - } - } + ctx.getImageData = function(x, y, w, h) { + return { + width: w, + height: h, + data: Uint8ClampedArray(w * h * 4) + }; + } - function buildFuncCall(name) { - return function() { - // console.log("funcCall", name) - stack.push([name, Array.prototype.slice.call(arguments)]); - } - } - var name; - for (var i = 0; i < ctxFunc.length; i++) { - name = ctxFunc[i]; - ctx[name] = buildFuncCall(name); - } + ctx.putImageData = function(data, x, y, width, height) { + stack.push(["$putImageData", [data, x, y, width, height]]); + } - 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", - "DRAWWINDOW_DRAW_CARET": "1", - "DRAWWINDOW_DO_NOT_FLUSH": "2", - "DRAWWINDOW_DRAW_VIEW": "4", - "DRAWWINDOW_USE_WIDGET_LAYERS": "8", - "DRAWWINDOW_ASYNC_DECODE_IMAGES": "16", + 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(); + stack.push(["$drawCanvas", [image.id, x, y, sx, sy, swidth, sheight]]); + } else if(image instanceof JpegStreamProxy) { + stack.push(["$drawImage", [image.id, x, y, sx, sy, swidth, sheight]]) + } else { + throw "unkown type to drawImage"; } + } - function buildGetter(name) { - return function() { - return ctx["$" + name]; - } + // 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) { + function buildSetter(name) { + return function(value) { + stack.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") { + function buildSetterStyle(name) { return function(value) { + if (value instanceof GradientProxy) { + stack.push(["$" + name + "Gradient"]); + } else if (value instanceof PatternProxy) { + stack.push(["$" + name + "Pattern", [value.id]]); + } else { stack.push(["$", name, value]); return ctx["$" + name] = value; + } } + } + ctx.__defineSetter__(name, buildSetterStyle(name)); + } else { + ctx.__defineSetter__(name, buildSetter(name)); } - - 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") { - function buildSetterStyle(name) { - return function(value) { - if (value instanceof GradientProxy) { - stack.push(["$" + name + "Gradient"]); - } else if (value instanceof PatternProxy) { - stack.push(["$" + name + "Pattern", [value.id]]); - } else { - stack.push(["$", name, value]); - return ctx["$" + name] = value; - } - } - } - ctx.__defineSetter__(name, buildSetterStyle(name)); - } else { - ctx.__defineSetter__(name, buildSetter(name)); - } - } + } } +/** +* Sends the current stack of the CanvasProxy over to the main thread and +* resets the stack. +*/ CanvasProxy.prototype.flush = function() { - postMessage("canvas_proxy_stack"); - postMessage({ - id: this.id, - stack: this.$stack, - width: this.width, - height: this.height - }); - this.$stack.length = 0; + postMessage("canvas_proxy_stack"); + postMessage({ + id: this.id, + stack: this.$stack, + width: this.width, + height: this.height + }); + this.$stack.length = 0; } diff --git a/viewer_worker.html b/viewer_worker.html index 92806bc99..a9f08388f 100644 --- a/viewer_worker.html +++ b/viewer_worker.html @@ -1,295 +1,22 @@ Simple pdf.js page viewer worker + @@ -301,9 +28,9 @@ window.onload = function() { - - - Previous + + -- diff --git a/worker.js b/worker.js index e59e37155..09e2b8145 100644 --- a/worker.js +++ b/worker.js @@ -1,15 +1,26 @@ "use strict"; +var timer = null; +function tic() { + timer = Date.now(); +} + +function toc(msg) { + log(msg + ": " + (Date.now() - timer) + "ms"); + timer = null; +} + function log() { - var args = Array.prototype.slice.call(arguments); - postMessage("log"); - postMessage(JSON.stringify(args)) + var args = Array.prototype.slice.call(arguments); + postMessage("log"); + postMessage(JSON.stringify(args)) } var console = { - log: log + log: log } +// importScripts("canvas_proxy.js"); importScripts("pdf.js"); importScripts("fonts.js"); @@ -18,55 +29,50 @@ importScripts("glyphlist.js") // Use the JpegStreamProxy proxy. JpegStream = JpegStreamProxy; -var timer = null; -function tic() { - timer = Date.now(); -} - -function toc(msg) { - log(msg + ": " + (Date.now() - timer) + "ms"); - timer = null; -} - // Create the WebWorkerProxyCanvas. var canvas = new CanvasProxy(1224, 1584); -var pageInterval; +// Listen for messages from the main thread. var pdfDocument = null; onmessage = function(event) { - var data = event.data; - if (!pdfDocument) { - pdfDocument = new PDFDoc(new Stream(data)); - postMessage("pdf_num_page"); - postMessage(pdfDocument.numPages) - return; - } else { - tic(); + 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("pdf_num_page"); + postMessage(pdfDocument.numPages) + return; + } + // User requested to render a certain page. + else { + tic(); - // Let's try to render the first page... - var page = pdfDocument.getPage(parseInt(data)); + // 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); + // 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); - // 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; - } + // 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); - } - toc("compiled page"); - - page.display(gfx); - canvas.flush(); + // This "builds" the font and sents it over to the main thread. + new Font(font.name, font.file, font.properties); } + toc("compiled page"); + + tic() + page.display(gfx); + canvas.flush(); + toc("displayed page"); + } } diff --git a/worker_client.js b/worker_client.js new file mode 100644 index 000000000..316ef1fc0 --- /dev/null +++ b/worker_client.js @@ -0,0 +1,294 @@ +"use strict"; + +function WorkerPDFDoc(canvas) { + var timer = null + function tic() { + timer = Date.now(); + } + + function toc(msg) { + console.log(msg + ": " + (Date.now() - timer) + "ms"); + } + + this.ctx = canvas.getContext("2d"); + this.canvas = canvas; + this.worker = new Worker('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, uniText) { + this.translate(currentX, -1 * y); + this.fillText(uniText, 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"; + } + 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, stack) { + var ctx = canvas.getContext("2d"); + for (var i = 0; i < stack.length; i++) { + var opp = stack[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]); + } + } + } + + /** + * onMessage state machine. + */ + const WAIT = 0; + const CANVAS_PROXY_STACK = 1; + const LOG = 2; + const FONT = 3; + const PDF_NUM_PAGE = 4; + const JPEG_STREAM = 5; + + var onMessageState = WAIT; + this.worker.onmessage = function(event) { + var data = event.data; + // console.log("onMessageRaw", data); + switch (onMessageState) { + case WAIT: + if (typeof data != "string") { + throw "expecting to get an string"; + } + switch (data) { + case "pdf_num_page": + onMessageState = PDF_NUM_PAGE; + return; + + case "log": + onMessageState = LOG; + return; + + case "canvas_proxy_stack": + onMessageState = CANVAS_PROXY_STACK; + return; + + case "font": + onMessageState = FONT; + return; + + case "jpeg_stream": + onMessageState = JPEG_STREAM; + return; + + default: + throw "unkown state: " + data + } + break; + + case JPEG_STREAM: + var img = new Image(); + img.src = "data:image/jpeg;base64," + window.btoa(data.str); + imagesList[data.id] = img; + console.log("got image", data.id) + break; + + case PDF_NUM_PAGE: + this.numPages = parseInt(data); + if (this.loadCallback) { + this.loadCallback(); + } + onMessageState = WAIT; + break; + + case FONT: + data = JSON.parse(data); + var base64 = window.btoa(data.str); + + // 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. + document.getElementById("fonts").innerHTML += "
j
"; + + onMessageState = WAIT; + break; + + case LOG: + console.log.apply(console, JSON.parse(data)); + onMessageState = WAIT; + break; + + case CANVAS_PROXY_STACK: + var id = data.id; + var stack = data.stack; + + // 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) tic(); + renderProxyCanvas(canvasList[id], stack); + if (id == 0) toc("canvas rendering") + }, 0); + onMessageState = WAIT; + break; + } + }.bind(this); +} + + 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) { + var ctx = this.ctx; + ctx.save(); + ctx.fillStyle = "rgb(255, 255, 255)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + + 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); +}