diff --git a/Makefile b/Makefile index 0ad2eb09c..2ce3aba6f 100644 --- a/Makefile +++ b/Makefile @@ -129,7 +129,7 @@ browser-test: # # SRC_DIRS := . src utils web test examples/helloworld extensions/firefox \ - extensions/firefox/components extensions/chrome + extensions/firefox/components extensions/chrome test/unit GJSLINT_FILES = $(foreach DIR,$(SRC_DIRS),$(wildcard $(DIR)/*.js)) lint: gjslint --nojsdoc $(GJSLINT_FILES) diff --git a/README.md b/README.md index da70a4f57..7e5d2eeb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pdf.js - + ## Overview diff --git a/src/canvas.js b/src/canvas.js index cd49c88b1..3fd55b45d 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -255,8 +255,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } // Scale so that canvas units are the same as PDF user space units this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height); - this.textDivs = []; - this.textLayerQueue = []; + // Move the media left-top corner to the (0,0) canvas position + this.ctx.translate(-mediaBox.x, -mediaBox.y); + + if (this.textLayer) + this.textLayer.beginLayout(); }, executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR, @@ -320,27 +323,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { endDrawing: function canvasGraphicsEndDrawing() { this.ctx.restore(); - var textLayer = this.textLayer; - if (!textLayer) - return; - - var self = this; - var textDivs = this.textDivs; - this.textLayerTimer = setInterval(function renderTextLayer() { - if (textDivs.length === 0) { - clearInterval(self.textLayerTimer); - return; - } - var textDiv = textDivs.shift(); - if (textDiv.dataset.textLength > 1) { // avoid div by zero - textLayer.appendChild(textDiv); - // Adjust div width (via letterSpacing) to match canvas text - // Due to the .offsetWidth calls, this is slow - textDiv.style.letterSpacing = - ((textDiv.dataset.canvasWidth - textDiv.offsetWidth) / - (textDiv.dataset.textLength - 1)) + 'px'; - } - }, 0); + if (this.textLayer) + this.textLayer.endLayout(); }, // Graphics state @@ -359,6 +343,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { setDash: function canvasGraphicsSetDash(dashArray, dashPhase) { this.ctx.mozDash = dashArray; this.ctx.mozDashOffset = dashPhase; + this.ctx.webkitLineDash = dashArray; + this.ctx.webkitLineDashOffset = dashPhase; }, setRenderingIntent: function canvasGraphicsSetRenderingIntent(intent) { TODO('set rendering intent: ' + intent); @@ -630,24 +616,6 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { return geometry; }, - pushTextDivs: function canvasGraphicsPushTextDivs(text) { - var div = document.createElement('div'); - var fontSize = this.current.fontSize; - - // vScale and hScale already contain the scaling to pixel units - // as mozCurrentTransform reflects ctx.scale() changes - // (see beginDrawing()) - var fontHeight = fontSize * text.geom.vScale; - div.dataset.canvasWidth = text.canvasWidth * text.geom.hScale; - - div.style.fontSize = fontHeight + 'px'; - div.style.fontFamily = this.current.font.loadedName || 'sans-serif'; - div.style.left = text.geom.x + 'px'; - div.style.top = (text.geom.y - fontHeight) + 'px'; - div.innerHTML = text.str; - div.dataset.textLength = text.length; - this.textDivs.push(div); - }, showText: function canvasGraphicsShowText(str, skipTextSelection) { var ctx = this.ctx; var current = this.current; @@ -672,6 +640,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { ctx.translate(current.x, current.y); ctx.scale(textHScale, 1); + ctx.lineWidth /= current.textMatrix[0]; if (textSelection) { this.save(); @@ -708,6 +677,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } else { ctx.save(); this.applyTextTransforms(); + ctx.lineWidth /= current.textMatrix[0] * fontMatrix[0]; + if (textSelection) text.geom = this.getTextGeometry(); @@ -744,7 +715,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { width += charWidth; - text.str += glyph.unicode === ' ' ? ' ' : glyph.unicode; + text.str += glyph.unicode === ' ' ? '\u00A0' : glyph.unicode; text.length++; text.canvasWidth += charWidth; } @@ -753,7 +724,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } if (textSelection) - this.pushTextDivs(text); + this.textLayer.appendText(text, font.loadedName, fontSize); return text; }, @@ -796,7 +767,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (e < 0 && text.geom.spaceWidth > 0) { // avoid div by zero var numFakeSpaces = Math.round(-e / text.geom.spaceWidth); if (numFakeSpaces > 0) { - text.str += ' '; + text.str += '\u00A0'; text.length++; } } @@ -806,7 +777,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (textSelection) { if (shownText.str === ' ') { - text.str += ' '; + text.str += '\u00A0'; } else { text.str += shownText.str; } @@ -819,7 +790,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } if (textSelection) - this.pushTextDivs(text); + this.textLayer.appendText(text, font.loadedName, fontSize); }, nextLineShowText: function canvasGraphicsNextLineShowText(text) { this.nextLine(); diff --git a/src/core.js b/src/core.js index 93cbc72ac..765a239b7 100644 --- a/src/core.js +++ b/src/core.js @@ -70,8 +70,7 @@ var Page = (function PageClosure() { this.xref = xref; this.ref = ref; - this.ctx = null; - this.callback = null; + this.displayReadyPromise = null; } Page.prototype = { @@ -110,9 +109,11 @@ var Page = (function PageClosure() { width: this.width, height: this.height }; + var mediaBox = this.mediaBox; + var offsetX = mediaBox[0], offsetY = mediaBox[1]; if (isArray(obj) && obj.length == 4) { - var tl = this.rotatePoint(obj[0], obj[1]); - var br = this.rotatePoint(obj[2], obj[3]); + var tl = this.rotatePoint(obj[0] - offsetX, obj[1] - offsetY); + var br = this.rotatePoint(obj[2] - offsetX, obj[3] - offsetY); view.x = Math.min(tl.x, br.x); view.y = Math.min(tl.y, br.y); view.width = Math.abs(tl.x - br.x); @@ -165,20 +166,12 @@ var Page = (function PageClosure() { IRQueue, fonts) { var self = this; this.IRQueue = IRQueue; - var gfx = new CanvasGraphics(this.ctx, this.objs, this.textLayer); var displayContinuation = function pageDisplayContinuation() { // Always defer call to display() to work around bug in // Firefox error reporting from XHR callbacks. setTimeout(function pageSetTimeout() { - try { - self.display(gfx, self.callback); - } catch (e) { - if (self.callback) - self.callback(e); - else - throw e; - } + self.displayReadyPromise.resolve(); }); }; @@ -395,12 +388,35 @@ var Page = (function PageClosure() { return items; }, startRendering: function pageStartRendering(ctx, callback, textLayer) { - this.ctx = ctx; - this.callback = callback; - this.textLayer = textLayer; - this.startRenderingTime = Date.now(); - this.pdf.startRendering(this); + + // If there is no displayReadyPromise yet, then the IRQueue was never + // requested before. Make the request and create the promise. + if (!this.displayReadyPromise) { + this.pdf.startRendering(this); + this.displayReadyPromise = new Promise(); + } + + // Once the IRQueue and fonts are loaded, perform the actual rendering. + this.displayReadyPromise.then( + function pageDisplayReadyPromise() { + var gfx = new CanvasGraphics(ctx, this.objs, textLayer); + try { + this.display(gfx, callback); + } catch (e) { + if (callback) + callback(e); + else + throw e; + } + }.bind(this), + function pageDisplayReadPromiseError(reason) { + if (callback) + callback(reason); + else + throw reason; + } + ); } }; @@ -523,10 +539,19 @@ var PDFDocModel = (function PDFDocModelClosure() { }, setup: function pdfDocSetup(ownerPassword, userPassword) { this.checkHeader(); - this.xref = new XRef(this.stream, - this.startXRef, - this.mainXRefEntriesOffset); - this.catalog = new Catalog(this.xref); + var xref = new XRef(this.stream, + this.startXRef, + this.mainXRefEntriesOffset); + this.xref = xref; + this.catalog = new Catalog(xref); + if (xref.trailer && xref.trailer.has('ID')) { + var fileID = ''; + var id = xref.fetchIfRef(xref.trailer.get('ID'))[0]; + id.split('').forEach(function(el) { + fileID += Number(el.charCodeAt(0)).toString(16); + }); + this.fileID = fileID; + } }, get numPages() { var linearization = this.linearization; @@ -534,6 +559,22 @@ var PDFDocModel = (function PDFDocModelClosure() { // shadow the prototype getter return shadow(this, 'numPages', num); }, + getFingerprint: function pdfDocGetFingerprint() { + if (this.fileID) { + return this.fileID; + } else { + // If we got no fileID, then we generate one, + // from the first 100 bytes of PDF + var data = this.stream.bytes.subarray(0, 100); + var hash = calculateMD5(data, 0, data.length); + var strHash = ''; + for (var i = 0, length = hash.length; i < length; i++) { + strHash += Number(hash[i]).toString(16); + } + + return strHash; + } + }, getPage: function pdfDocGetPage(n) { return this.catalog.getPage(n); } @@ -560,7 +601,7 @@ var PDFDoc = (function PDFDocClosure() { this.data = data; this.stream = stream; this.pdf = new PDFDocModel(stream); - + this.fingerprint = this.pdf.getFingerprint(); this.catalog = this.pdf.catalog; this.objs = new PDFObjects(); @@ -579,36 +620,35 @@ var PDFDoc = (function PDFDocClosure() { throw 'No PDFJS.workerSrc specified'; } - var worker; try { - worker = new Worker(workerSrc); - } catch (e) { // Some versions of FF can't create a worker on localhost, see: // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); + var worker = new Worker(workerSrc); + + var messageHandler = new MessageHandler('main', worker); + // Tell the worker the file it was created from. + messageHandler.send('workerSrc', workerSrc); + messageHandler.on('test', function pdfDocTest(supportTypedArray) { + if (supportTypedArray) { + this.worker = worker; + this.setupMessageHandler(messageHandler); + } else { + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + }.bind(this)); + + var testObj = new Uint8Array(1); + // Some versions of Opera throw a DATA_CLONE_ERR on + // serializing the typed array. + messageHandler.send('test', testObj); return; - } - - var messageHandler = new MessageHandler('main', worker); - - // Tell the worker the file it was created from. - messageHandler.send('workerSrc', workerSrc); - - messageHandler.on('test', function pdfDocTest(supportTypedArray) { - if (supportTypedArray) { - this.worker = worker; - this.setupMessageHandler(messageHandler); - } else { - this.setupFakeWorker(); - } - }.bind(this)); - - var testObj = new Uint8Array(1); - messageHandler.send('test', testObj); - } else { - this.setupFakeWorker(); + } catch (e) {} } + // Either workers are disabled, not supported or have thrown an exception. + // Thus, we fallback to a faked worker. + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); } PDFDoc.prototype = { @@ -692,8 +732,8 @@ var PDFDoc = (function PDFDocClosure() { messageHandler.on('page_error', function pdfDocError(data) { var page = this.pageCache[data.pageNum]; - if (page.callback) - page.callback(data.error); + if (page.displayReadyPromise) + page.displayReadyPromise.reject(data.error); else throw data.error; }, this); diff --git a/src/fonts.js b/src/fonts.js index 83ce4abaa..1b959d6c2 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -2092,7 +2092,7 @@ var Font = (function FontClosure() { window.btoa(data) + ');'); var rule = "@font-face { font-family:'" + fontName + "';src:" + url + '}'; - document.documentElement.firstChild.appendChild( + document.documentElement.getElementsByTagName('head')[0].appendChild( document.createElement('style')); var styleSheet = document.styleSheets[document.styleSheets.length - 1]; diff --git a/src/function.js b/src/function.js index 6b0063218..26b8fe679 100644 --- a/src/function.js +++ b/src/function.js @@ -270,7 +270,6 @@ var PDFFunction = (function PDFFunctionClosure() { constructStiched: function pdfFunctionConstructStiched(fn, dict, xref) { var domain = dict.get('Domain'); - var range = dict.get('Range'); if (!domain) error('No domain'); @@ -279,13 +278,13 @@ var PDFFunction = (function PDFFunctionClosure() { if (inputSize != 1) error('Bad domain for stiched function'); - var fnRefs = dict.get('Functions'); + var fnRefs = xref.fetchIfRef(dict.get('Functions')); var fns = []; for (var i = 0, ii = fnRefs.length; i < ii; ++i) fns.push(PDFFunction.getIR(xref, xref.fetchIfRef(fnRefs[i]))); - var bounds = dict.get('Bounds'); - var encode = dict.get('Encode'); + var bounds = xref.fetchIfRef(dict.get('Bounds')); + var encode = xref.fetchIfRef(dict.get('Encode')); return [CONSTRUCT_STICHED, domain, bounds, encode, fns]; }, @@ -336,16 +335,550 @@ var PDFFunction = (function PDFFunctionClosure() { }; }, - constructPostScript: function pdfFunctionConstructPostScript() { - return [CONSTRUCT_POSTSCRIPT]; + constructPostScript: function pdfFunctionConstructPostScript(fn, dict, + xref) { + var domain = dict.get('Domain'); + var range = dict.get('Range'); + + if (!domain) + error('No domain.'); + + if (!range) + error('No range.'); + + var lexer = new PostScriptLexer(fn); + var parser = new PostScriptParser(lexer); + var code = parser.parse(); + + return [CONSTRUCT_POSTSCRIPT, domain, range, code]; }, - constructPostScriptFromIR: function pdfFunctionConstructPostScriptFromIR() { - TODO('unhandled type of function'); - return function constructPostScriptFromIRResult() { - return [255, 105, 180]; + constructPostScriptFromIR: + function pdfFunctionConstructPostScriptFromIR(IR) { + var domain = IR[1]; + var range = IR[2]; + var code = IR[3]; + var numOutputs = range.length / 2; + var evaluator = new PostScriptEvaluator(code); + // Cache the values for a big speed up, the cache size is limited though + // since the number of possible values can be huge from a PS function. + var cache = new FunctionCache(); + return function constructPostScriptFromIRResult(args) { + var initialStack = []; + for (var i = 0, ii = (domain.length / 2); i < ii; ++i) { + initialStack.push(args[i]); + } + + var key = initialStack.join('_'); + if (cache.has(key)) + return cache.get(key); + + var stack = evaluator.execute(initialStack); + var transformed = new Array(numOutputs); + for (i = numOutputs - 1; i >= 0; --i) { + var out = stack.pop(); + var rangeIndex = 2 * i; + if (out < range[rangeIndex]) + out = range[rangeIndex]; + else if (out > range[rangeIndex + 1]) + out = range[rangeIndex + 1]; + transformed[i] = out; + } + cache.set(key, transformed); + return transformed; }; } }; })(); +var FunctionCache = (function FunctionCacheClosure() { + // Of 10 PDF's with type4 functions the maxium number of distinct values seen + // was 256. This still may need some tweaking in the future though. + var MAX_CACHE_SIZE = 1024; + function FunctionCache() { + this.cache = {}; + this.total = 0; + } + FunctionCache.prototype = { + has: function has(key) { + return key in this.cache; + }, + get: function get(key) { + return this.cache[key]; + }, + set: function set(key, value) { + if (this.total < MAX_CACHE_SIZE) { + this.cache[key] = value; + this.total++; + } + } + }; + return FunctionCache; +})(); + +var PostScriptStack = (function PostScriptStackClosure() { + var MAX_STACK_SIZE = 100; + function PostScriptStack(initialStack) { + this.stack = initialStack || []; + } + + PostScriptStack.prototype = { + push: function push(value) { + if (this.stack.length >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + this.stack.push(value); + }, + pop: function pop() { + if (this.stack.length <= 0) + error('PostScript function stack underflow.'); + return this.stack.pop(); + }, + copy: function copy(n) { + if (this.stack.length + n >= MAX_STACK_SIZE) + error('PostScript function stack overflow.'); + var stack = this.stack; + for (var i = stack.length - n, j = n - 1; j >= 0; j--, i++) + stack.push(stack[i]); + }, + index: function index(n) { + this.push(this.stack[this.stack.length - n - 1]); + }, + // rotate the last n stack elements p times + roll: function roll(n, p) { + var stack = this.stack; + var l = stack.length - n; + var r = stack.length - 1, c = l + (p - Math.floor(p / n) * n), i, j, t; + for (i = l, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = l, j = c - 1; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + for (i = c, j = r; i < j; i++, j--) { + t = stack[i]; stack[i] = stack[j]; stack[j] = t; + } + } + }; + return PostScriptStack; +})(); +var PostScriptEvaluator = (function PostScriptEvaluatorClosure() { + function PostScriptEvaluator(operators, operands) { + this.operators = operators; + this.operands = operands; + } + PostScriptEvaluator.prototype = { + execute: function execute(initialStack) { + var stack = new PostScriptStack(initialStack); + var counter = 0; + var operators = this.operators; + var length = operators.length; + var operator, a, b; + while (counter < length) { + operator = operators[counter++]; + if (typeof operator == 'number') { + // Operator is really an operand and should be pushed to the stack. + stack.push(operator); + continue; + } + switch (operator) { + // non standard ps operators + case 'jz': // jump if false + b = stack.pop(); + a = stack.pop(); + if (!a) + counter = b; + break; + case 'j': // jump + a = stack.pop(); + counter = a; + break; + + // all ps operators in alphabetical order (excluding if/ifelse) + case 'abs': + a = stack.pop(); + stack.push(Math.abs(a)); + break; + case 'add': + b = stack.pop(); + a = stack.pop(); + stack.push(a + b); + break; + case 'and': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'atan': + a = stack.pop(); + stack.push(Math.atan(a)); + break; + case 'bitshift': + b = stack.pop(); + a = stack.pop(); + if (a > 0) + stack.push(a << b); + else + stack.push(a >> b); + break; + case 'ceiling': + a = stack.pop(); + stack.push(Math.ceil(a)); + break; + case 'copy': + a = stack.pop(); + stack.copy(a); + break; + case 'cos': + a = stack.pop(); + stack.push(Math.cos(a)); + break; + case 'cvi': + a = stack.pop() | 0; + stack.push(a); + break; + case 'cvr': + // noop + break; + case 'div': + b = stack.pop(); + a = stack.pop(); + stack.push(a / b); + break; + case 'dup': + stack.copy(1); + break; + case 'eq': + b = stack.pop(); + a = stack.pop(); + stack.push(a == b); + break; + case 'exch': + stack.roll(2, 1); + break; + case 'exp': + b = stack.pop(); + a = stack.pop(); + stack.push(Math.pow(a, b)); + break; + case 'false': + stack.push(false); + break; + case 'floor': + a = stack.pop(); + stack.push(Math.floor(a)); + break; + case 'ge': + b = stack.pop(); + a = stack.pop(); + stack.push(a >= b); + break; + case 'gt': + b = stack.pop(); + a = stack.pop(); + stack.push(a > b); + break; + case 'idiv': + b = stack.pop(); + a = stack.pop(); + stack.push((a / b) | 0); + break; + case 'index': + a = stack.pop(); + stack.index(a); + break; + case 'le': + b = stack.pop(); + a = stack.pop(); + stack.push(a <= b); + break; + case 'ln': + a = stack.pop(); + stack.push(Math.log(a)); + break; + case 'log': + a = stack.pop(); + stack.push(Math.log(a) / Math.LN10); + break; + case 'lt': + b = stack.pop(); + a = stack.pop(); + stack.push(a < b); + break; + case 'mod': + b = stack.pop(); + a = stack.pop(); + stack.push(a % b); + break; + case 'mul': + b = stack.pop(); + a = stack.pop(); + stack.push(a * b); + break; + case 'ne': + b = stack.pop(); + a = stack.pop(); + stack.push(a != b); + break; + case 'neg': + a = stack.pop(); + stack.push(-b); + break; + case 'not': + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a && b); + else + stack.push(a & b); + break; + case 'or': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a || b); + else + stack.push(a | b); + break; + case 'pop': + stack.pop(); + break; + case 'roll': + b = stack.pop(); + a = stack.pop(); + stack.roll(a, b); + break; + case 'round': + a = stack.pop(); + stack.push(Math.round(a)); + break; + case 'sin': + a = stack.pop(); + stack.push(Math.sin(a)); + break; + case 'sqrt': + a = stack.pop(); + stack.push(Math.sqrt(a)); + break; + case 'sub': + b = stack.pop(); + a = stack.pop(); + stack.push(a - b); + break; + case 'true': + stack.push(true); + break; + case 'truncate': + a = stack.pop(); + a = a < 0 ? Math.ceil(a) : Math.floor(a); + stack.push(a); + break; + case 'xor': + b = stack.pop(); + a = stack.pop(); + if (isBool(a) && isBool(b)) + stack.push(a != b); + else + stack.push(a ^ b); + break; + default: + error('Unknown operator ' + operator); + break; + } + } + return stack.stack; + } + }; + return PostScriptEvaluator; +})(); + +var PostScriptParser = (function PostScriptParserClosure() { + function PostScriptParser(lexer) { + this.lexer = lexer; + this.operators = []; + this.token; + this.prev; + } + PostScriptParser.prototype = { + nextToken: function nextToken() { + this.prev = this.token; + this.token = this.lexer.getToken(); + }, + accept: function accept(type) { + if (this.token.type == type) { + this.nextToken(); + return true; + } + return false; + }, + expect: function expect(type) { + if (this.accept(type)) + return true; + error('Unexpected symbol: found ' + this.token.type + ' expected ' + + type + '.'); + }, + parse: function parse() { + this.nextToken(); + this.expect(PostScriptTokenTypes.LBRACE); + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + return this.operators; + }, + parseBlock: function parseBlock() { + while (true) { + if (this.accept(PostScriptTokenTypes.NUMBER)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.OPERATOR)) { + this.operators.push(this.prev.value); + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + this.parseCondition(); + } else { + return; + } + } + }, + parseCondition: function parseCondition() { + // Add two place holders that will be updated later + var conditionLocation = this.operators.length; + this.operators.push(null, null); + + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + if (this.accept(PostScriptTokenTypes.IF)) { + // The true block is right after the 'if' so it just falls through on + // true else it jumps and skips the true block. + this.operators[conditionLocation] = this.operators.length; + this.operators[conditionLocation + 1] = 'jz'; + } else if (this.accept(PostScriptTokenTypes.LBRACE)) { + var jumpLocation = this.operators.length; + this.operators.push(null, null); + var endOfTrue = this.operators.length; + this.parseBlock(); + this.expect(PostScriptTokenTypes.RBRACE); + this.expect(PostScriptTokenTypes.IFELSE); + // The jump is added at the end of the true block to skip the false + // block. + this.operators[jumpLocation] = this.operators.length; + this.operators[jumpLocation + 1] = 'j'; + + this.operators[conditionLocation] = endOfTrue; + this.operators[conditionLocation + 1] = 'jz'; + } else { + error('PS Function: error parsing conditional.'); + } + } + }; + return PostScriptParser; +})(); + +var PostScriptTokenTypes = { + LBRACE: 0, + RBRACE: 1, + NUMBER: 2, + OPERATOR: 3, + IF: 4, + IFELSE: 5 +}; + +var PostScriptToken = (function PostScriptTokenClosure() { + function PostScriptToken(type, value) { + this.type = type; + this.value = value; + } + + var opCache = {}; + + PostScriptToken.getOperator = function getOperator(op) { + var opValue = opCache[op]; + if (opValue) + return opValue; + + return opCache[op] = new PostScriptToken(PostScriptTokenTypes.OPERATOR, op); + }; + + PostScriptToken.LBRACE = new PostScriptToken(PostScriptTokenTypes.LBRACE, + '{'); + PostScriptToken.RBRACE = new PostScriptToken(PostScriptTokenTypes.RBRACE, + '}'); + PostScriptToken.IF = new PostScriptToken(PostScriptTokenTypes.IF, 'IF'); + PostScriptToken.IFELSE = new PostScriptToken(PostScriptTokenTypes.IFELSE, + 'IFELSE'); + return PostScriptToken; +})(); + +var PostScriptLexer = (function PostScriptLexerClosure() { + function PostScriptLexer(stream) { + this.stream = stream; + } + PostScriptLexer.prototype = { + getToken: function getToken() { + var s = ''; + var ch; + var comment = false; + var stream = this.stream; + + // skip comments + while (true) { + if (!(ch = stream.getChar())) + return EOF; + + if (comment) { + if (ch == '\x0a' || ch == '\x0d') + comment = false; + } else if (ch == '%') { + comment = true; + } else if (!Lexer.isSpace(ch)) { + break; + } + } + switch (ch) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '+': case '-': case '.': + return new PostScriptToken(PostScriptTokenTypes.NUMBER, + this.getNumber(ch)); + case '{': + return PostScriptToken.LBRACE; + case '}': + return PostScriptToken.RBRACE; + } + // operator + var str = ch.toLowerCase(); + while (true) { + ch = stream.lookChar().toLowerCase(); + if (ch >= 'a' && ch <= 'z') + str += ch; + else + break; + stream.skip(); + } + switch (str) { + case 'if': + return PostScriptToken.IF; + case 'ifelse': + return PostScriptToken.IFELSE; + default: + return PostScriptToken.getOperator(str); + } + }, + getNumber: function getNumber(ch) { + var str = ch; + var stream = this.stream; + while (true) { + ch = stream.lookChar(); + if ((ch >= '0' && ch <= '9') || ch == '-' || ch == '.') + str += ch; + else + break; + stream.skip(); + } + var value = parseFloat(str); + if (isNaN(value)) + error('Invalid floating point number: ' + value); + return value; + } + }; + return PostScriptLexer; +})(); + diff --git a/src/obj.js b/src/obj.js index 453014a91..a0c1fdc8a 100644 --- a/src/obj.js +++ b/src/obj.js @@ -8,8 +8,7 @@ var Name = (function NameClosure() { this.name = name; } - Name.prototype = { - }; + Name.prototype = {}; return Name; })(); @@ -19,9 +18,7 @@ var Cmd = (function CmdClosure() { this.cmd = cmd; } - Cmd.prototype = { - }; - + Cmd.prototype = {}; var cmdCache = {}; @@ -80,8 +77,7 @@ var Ref = (function RefClosure() { this.gen = gen; } - Ref.prototype = { - }; + Ref.prototype = {}; return Ref; })(); @@ -273,7 +269,7 @@ var XRef = (function XRefClosure() { this.entries = []; this.xrefstms = {}; var trailerDict = this.readXRef(startXRef); - + this.trailer = trailerDict; // prepare the XRef cache this.cache = []; diff --git a/src/stream.js b/src/stream.js index d996f5c91..8d3f0f5bb 100644 --- a/src/stream.js +++ b/src/stream.js @@ -1856,10 +1856,10 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { // values. The first array element indicates whether a valid code is being // returned. The second array element is the actual code. The third array // element indicates whether EOF was reached. - var findTableCode = function ccittFaxStreamFindTableCode(start, end, table, - limit) { - var limitValue = limit || 0; + CCITTFaxStream.prototype.findTableCode = + function ccittFaxStreamFindTableCode(start, end, table, limit) { + var limitValue = limit || 0; for (var i = start; i <= end; ++i) { var code = this.lookBits(i); if (code == EOF) @@ -1890,7 +1890,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { return p[1]; } } else { - var result = findTableCode(1, 7, twoDimTable); + var result = this.findTableCode(1, 7, twoDimTable); if (result[0] && result[2]) return result[1]; } @@ -1919,11 +1919,11 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { return p[1]; } } else { - var result = findTableCode(1, 9, whiteTable2); + var result = this.findTableCode(1, 9, whiteTable2); if (result[0]) return result[1]; - result = findTableCode(11, 12, whiteTable1); + result = this.findTableCode(11, 12, whiteTable1); if (result[0]) return result[1]; } @@ -1952,15 +1952,15 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() { return p[1]; } } else { - var result = findTableCode(2, 6, blackTable3); + var result = this.findTableCode(2, 6, blackTable3); if (result[0]) return result[1]; - result = findTableCode(7, 12, blackTable2, 64); + result = this.findTableCode(7, 12, blackTable2, 64); if (result[0]) return result[1]; - result = findTableCode(10, 13, blackTable1); + result = this.findTableCode(10, 13, blackTable1); if (result[0]) return result[1]; } diff --git a/src/util.js b/src/util.js index 57dbca4bb..99b422296 100644 --- a/src/util.js +++ b/src/util.js @@ -206,6 +206,8 @@ var Promise = (function PromiseClosure() { */ function Promise(name, data) { this.name = name; + this.isRejected = false; + this.error = null; // If you build a promise and pass in some data it's already resolved. if (data != null) { this.isResolved = true; @@ -216,6 +218,7 @@ var Promise = (function PromiseClosure() { this._data = EMPTY_PROMISE; } this.callbacks = []; + this.errbacks = []; }; /** * Builds a promise that is resolved when all the passed in promises are @@ -282,9 +285,12 @@ var Promise = (function PromiseClosure() { if (this.isResolved) { throw 'A Promise can be resolved only once ' + this.name; } + if (this.isRejected) { + throw 'The Promise was already rejected ' + this.name; + } this.isResolved = true; - this.data = data; + this.data = data || null; var callbacks = this.callbacks; for (var i = 0, ii = callbacks.length; i < ii; i++) { @@ -292,7 +298,24 @@ var Promise = (function PromiseClosure() { } }, - then: function promiseThen(callback) { + reject: function proimseReject(reason) { + if (this.isRejected) { + throw 'A Promise can be rejected only once ' + this.name; + } + if (this.isResolved) { + throw 'The Promise was already resolved ' + this.name; + } + + this.isRejected = true; + this.error = reason || null; + var errbacks = this.errbacks; + + for (var i = 0, ii = errbacks.length; i < ii; i++) { + errbacks[i].call(null, reason); + } + }, + + then: function promiseThen(callback, errback) { if (!callback) { throw 'Requiring callback' + this.name; } @@ -301,8 +324,13 @@ var Promise = (function PromiseClosure() { if (this.isResolved) { var data = this.data; callback.call(null, data); + } else if (this.isRejected && errorback) { + var error = this.error; + errback.call(null, error); } else { this.callbacks.push(callback); + if (errback) + this.errbacks.push(errback); } } }; diff --git a/test/driver.js b/test/driver.js index 64fceee90..85d25658a 100644 --- a/test/driver.js +++ b/test/driver.js @@ -165,9 +165,14 @@ function nextPage(task, loadError) { canvas.height = pageHeight * pdfToCssUnitsCoef; clear(ctx); - // using non-attached to the document div to test + // using the text layer builder that does nothing to test // text layer creation operations - var textLayer = document.createElement('div'); + var textLayerBuilder = { + beginLayout: function nullTextLayerBuilderBeginLayout() {}, + endLayout: function nullTextLayerBuilderEndLayout() {}, + appendText: function nullTextLayerBuilderAppendText(text, fontName, + fontSize) {} + }; page.startRendering( ctx, @@ -177,7 +182,7 @@ function nextPage(task, loadError) { failureMessage = 'render : ' + error.message; snapshotCurrentPage(task, failureMessage); }, - textLayer + textLayerBuilder ); } catch (e) { failure = 'page setup : ' + e.toString(); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index fd541a06d..956980782 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -21,3 +21,4 @@ !freeculture.pdf !issue918.pdf !smaskdim.pdf +!type4psfunc.pdf diff --git a/test/pdfs/issue1001.pdf.link b/test/pdfs/issue1001.pdf.link new file mode 100644 index 000000000..24e1bebc2 --- /dev/null +++ b/test/pdfs/issue1001.pdf.link @@ -0,0 +1 @@ +http://www.myhillsapartment.com/island_club/floorplans/images/links/Island_IC_brochure.pdf diff --git a/test/pdfs/issue1015.pdf.link b/test/pdfs/issue1015.pdf.link new file mode 100644 index 000000000..0878ab443 --- /dev/null +++ b/test/pdfs/issue1015.pdf.link @@ -0,0 +1 @@ +http://faculty.washington.edu/fidelr/RayaPubs/TheCaseStudyMethod.pdf diff --git a/test/pdfs/ocs.pdf.link b/test/pdfs/ocs.pdf.link new file mode 100644 index 000000000..10c2b1b9e --- /dev/null +++ b/test/pdfs/ocs.pdf.link @@ -0,0 +1 @@ +http://www.unibuc.ro/uploads_en/29535/10/Cyrillic_Alphabets-Chars.pdf diff --git a/test/pdfs/type4psfunc.pdf b/test/pdfs/type4psfunc.pdf new file mode 100755 index 000000000..e4886e918 Binary files /dev/null and b/test/pdfs/type4psfunc.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 5b88b3136..7954aa094 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -17,12 +17,13 @@ "rounds": 1, "type": "load" }, - { "id": "intelisa-load", + { "id": "intelisa-eq", "file": "pdfs/intelisa.pdf", "md5": "f5712097d29287a97f1278839814f682", "link": true, + "pageLimit": 100, "rounds": 1, - "type": "load" + "type": "eq" }, { "id": "pdfspec-load", "file": "pdfs/pdf.pdf", @@ -355,6 +356,13 @@ "rounds": 1, "type": "eq" }, + { "id": "issue1001", + "file": "pdfs/issue1001.pdf", + "md5": "0f1496e80a82a923e91d9e74c55ad94e", + "rounds": 1, + "link": true, + "type": "eq" + }, { "id": "aboutstacks", "file": "pdfs/aboutstacks.pdf", "md5": "6e7c8416a293ba2d83bc8dd20c6ccf51", @@ -367,5 +375,25 @@ "md5": "de80aeca7cbf79940189fd34d59671ee", "rounds": 1, "type": "eq" + }, + { "id": "type4psfunc", + "file": "pdfs/type4psfunc.pdf", + "md5": "7e6027a02ff78577f74dccdf84e37189", + "rounds": 1, + "type": "eq" + }, + { "id": "ocs", + "file": "pdfs/ocs.pdf", + "md5": "2ade57e954ae7632749cf328daeaa7a8", + "rounds": 1, + "link": true, + "type": "load" + }, + { "id": "issue1015", + "file": "pdfs/issue1015.pdf", + "md5": "b61503d1b445742b665212866afb60e2", + "rounds": 1, + "link": true, + "type": "eq" } ] diff --git a/test/unit/function_spec.js b/test/unit/function_spec.js new file mode 100644 index 000000000..2a1dc0b75 --- /dev/null +++ b/test/unit/function_spec.js @@ -0,0 +1,225 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +'use strict'; + +describe('function', function() { + beforeEach(function() { + this.addMatchers({ + toMatchArray: function(expected) { + var actual = this.actual; + if (actual.length != expected.length) + return false; + for (var i = 0; i < expected.length; i++) { + var a = actual[i], b = expected[i]; + if (isArray(b)) { + if (a.length != b.length) + return false; + for (var j = 0; j < a.length; j++) { + var suba = a[j], subb = b[j]; + if (suba !== subb) + return false; + } + } else { + if (a !== b) + return false; + } + } + return true; + } + }); + }); + + describe('PostScriptParser', function() { + function parse(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + return parser.parse(); + } + it('parses empty programs', function() { + var output = parse('{}'); + expect(output.length).toEqual(0); + }); + it('parses positive numbers', function() { + var number = 999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative numbers', function() { + var number = -999; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses negative floats', function() { + var number = 3.3; + var program = parse('{ ' + number + ' }'); + var expectedProgram = [number]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses operators', function() { + var program = parse('{ sub }'); + var expectedProgram = ['sub']; + expect(program).toMatchArray(expectedProgram); + }); + it('parses if statements', function() { + var program = parse('{ { 99 } if }'); + var expectedProgram = [3, 'jz', 99]; + expect(program).toMatchArray(expectedProgram); + }); + it('parses ifelse statements', function() { + var program = parse('{ { 99 } { 44 } ifelse }'); + var expectedProgram = [5, 'jz', 99, 6, 'j', 44]; + expect(program).toMatchArray(expectedProgram); + }); + it('handles missing brackets', function() { + expect(function() { parse('{'); }).toThrow( + new Error('Unexpected symbol: found undefined expected 1.')); + }); + }); + + describe('PostScriptEvaluator', function() { + function evaluate(program) { + var stream = new StringStream(program); + var parser = new PostScriptParser(new PostScriptLexer(stream)); + var code = parser.parse(); + var evaluator = new PostScriptEvaluator(code); + var output = evaluator.execute(); + return output; + } + + it('pushes stack', function() { + var stack = evaluate('{ 99 }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with true', function() { + var stack = evaluate('{ 1 {99} if }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles if with false', function() { + var stack = evaluate('{ 0 {99} if }'); + var expectedStack = []; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with true', function() { + var stack = evaluate('{ 1 {99} {77} ifelse }'); + var expectedStack = [99]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles ifelse with false', function() { + var stack = evaluate('{ 0 {99} {77} ifelse }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + it('handles nested if', function() { + var stack = evaluate('{ 1 {1 {77} if} if }'); + var expectedStack = [77]; + expect(stack).toMatchArray(expectedStack); + }); + + it('abs', function() { + var stack = evaluate('{ -2 abs }'); + var expectedStack = [2]; + expect(stack).toMatchArray(expectedStack); + }); + it('adds', function() { + var stack = evaluate('{ 1 2 add }'); + var expectedStack = [3]; + expect(stack).toMatchArray(expectedStack); + }); + it('boolean ands', function() { + var stack = evaluate('{ true false and }'); + var expectedStack = [false]; + expect(stack).toMatchArray(expectedStack); + }); + it('bitwise ands', function() { + var stack = evaluate('{ 254 1 and }'); + var expectedStack = [254 & 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO atan + // TODO bitshift + // TODO ceiling + // TODO copy + // TODO cos + it('converts to int', function() { + var stack = evaluate('{ 9.9 cvi }'); + var expectedStack = [9]; + expect(stack).toMatchArray(expectedStack); + }); + it('converts negatives to int', function() { + var stack = evaluate('{ -9.9 cvi }'); + var expectedStack = [-9]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO cvr + // TODO div + it('duplicates', function() { + var stack = evaluate('{ 99 dup }'); + var expectedStack = [99, 99]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO eq + it('exchanges', function() { + var stack = evaluate('{ 44 99 exch }'); + var expectedStack = [99, 44]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO exp + // TODO false + // TODO floor + // TODO ge + // TODO gt + it('divides to integer', function() { + var stack = evaluate('{ 2 3 idiv }'); + var expectedStack = [0]; + expect(stack).toMatchArray(expectedStack); + }); + it('divides to negative integer', function() { + var stack = evaluate('{ -2 3 idiv }'); + var expectedStack = [0]; + expect(stack).toMatchArray(expectedStack); + }); + it('duplicates index', function() { + var stack = evaluate('{ 4 3 2 1 2 index }'); + var expectedStack = [4, 3, 2, 1, 3]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO le + // TODO ln + // TODO log + // TODO lt + // TODO mod + // TODO mul + // TODO ne + // TODO neg + // TODO not + // TODO or + it('pops stack', function() { + var stack = evaluate('{ 1 2 pop }'); + var expectedStack = [1]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack right', function() { + var stack = evaluate('{ 1 3 2 2 4 1 roll }'); + var expectedStack = [2, 1, 3, 2]; + expect(stack).toMatchArray(expectedStack); + }); + it('rolls stack left', function() { + var stack = evaluate('{ 1 3 2 2 4 -1 roll }'); + var expectedStack = [3, 2, 2, 1]; + expect(stack).toMatchArray(expectedStack); + }); + // TODO round + // TODO sin + // TODO sqrt + // TODO sub + // TODO true + // TODO truncate + // TODO xor + }); +}); + diff --git a/test/unit/obj_spec.js b/test/unit/obj_spec.js index 4f1a0b57a..02e268fd4 100644 --- a/test/unit/obj_spec.js +++ b/test/unit/obj_spec.js @@ -3,14 +3,129 @@ 'use strict'; -describe("obj", function() { +describe('obj', function() { - describe("Name", function() { - it("should retain the given name", function() { - var givenName = "Font"; + describe('Name', function() { + it('should retain the given name', function() { + var givenName = 'Font'; var name = new Name(givenName); expect(name.name).toEqual(givenName); }); }); + + describe('Cmd', function() { + it('should retain the given cmd name', function() { + var givenCmd = 'BT'; + var cmd = new Cmd(givenCmd); + expect(cmd.cmd).toEqual(givenCmd); + }); + + it('should create only one object for a command and cache it', function() { + var firstBT = Cmd.get('BT'); + var secondBT = Cmd.get('BT'); + var firstET = Cmd.get('ET'); + var secondET = Cmd.get('ET'); + expect(firstBT).toBe(secondBT); + expect(firstET).toBe(secondET); + expect(firstBT).not.toBe(firstET); + }); + }); + + describe('Dict', function() { + var checkInvalidHasValues = function(dict) { + expect(dict.has()).toBeFalsy(); + expect(dict.has('Prev')).toBeFalsy(); + }; + + var checkInvalidKeyValues = function(dict) { + expect(dict.get()).toBeUndefined(); + expect(dict.get('Prev')).toBeUndefined(); + expect(dict.get('Decode', 'D')).toBeUndefined(); + + // Note that the getter with three arguments breaks the pattern here. + expect(dict.get('FontFile', 'FontFile2', 'FontFile3')).toBeNull(); + }; + + var emptyDict, dictWithSizeKey, dictWithManyKeys; + var storedSize = 42; + var testFontFile = 'file1'; + var testFontFile2 = 'file2'; + var testFontFile3 = 'file3'; + + beforeEach(function() { + emptyDict = new Dict(); + + dictWithSizeKey = new Dict(); + dictWithSizeKey.set('Size', storedSize); + + dictWithManyKeys = new Dict(); + dictWithManyKeys.set('FontFile', testFontFile); + dictWithManyKeys.set('FontFile2', testFontFile2); + dictWithManyKeys.set('FontFile3', testFontFile3); + }); + + it('should return invalid values for unknown keys', function() { + checkInvalidHasValues(emptyDict); + checkInvalidKeyValues(emptyDict); + }); + + it('should return correct value for stored Size key', function() { + expect(dictWithSizeKey.has('Size')).toBeTruthy(); + + expect(dictWithSizeKey.get('Size')).toEqual(storedSize); + expect(dictWithSizeKey.get('Prev', 'Size')).toEqual(storedSize); + expect(dictWithSizeKey.get('Prev', 'Root', 'Size')).toEqual(storedSize); + }); + + it('should return invalid values for unknown keys when Size key is stored', + function() { + checkInvalidHasValues(dictWithSizeKey); + checkInvalidKeyValues(dictWithSizeKey); + }); + + it('should return correct value for stored Size key with undefined value', + function() { + var dict = new Dict(); + dict.set('Size'); + + expect(dict.has('Size')).toBeTruthy(); + + checkInvalidKeyValues(dict); + }); + + it('should return correct values for multiple stored keys', function() { + expect(dictWithManyKeys.has('FontFile')).toBeTruthy(); + expect(dictWithManyKeys.has('FontFile2')).toBeTruthy(); + expect(dictWithManyKeys.has('FontFile3')).toBeTruthy(); + + expect(dictWithManyKeys.get('FontFile3')).toEqual(testFontFile3); + expect(dictWithManyKeys.get('FontFile2', 'FontFile3')) + .toEqual(testFontFile2); + expect(dictWithManyKeys.get('FontFile', 'FontFile2', 'FontFile3')) + .toEqual(testFontFile); + }); + + it('should callback for each stored key', function() { + var callbackSpy = jasmine.createSpy('spy on callback in dictionary'); + + dictWithManyKeys.forEach(callbackSpy); + + expect(callbackSpy).wasCalled(); + expect(callbackSpy.argsForCall[0]).toEqual(['FontFile', testFontFile]); + expect(callbackSpy.argsForCall[1]).toEqual(['FontFile2', testFontFile2]); + expect(callbackSpy.argsForCall[2]).toEqual(['FontFile3', testFontFile3]); + expect(callbackSpy.callCount).toEqual(3); + }); + }); + + describe('Ref', function() { + it('should retain the stored values', function() { + var storedNum = 4; + var storedGen = 2; + var ref = new Ref(storedNum, storedGen); + expect(ref.num).toEqual(storedNum); + expect(ref.gen).toEqual(storedGen); + }); + }); }); diff --git a/test/unit/unit_test.html b/test/unit/unit_test.html index 1fc28ef83..8d0af03d6 100644 --- a/test/unit/unit_test.html +++ b/test/unit/unit_test.html @@ -11,9 +11,28 @@ + + + + + + + + + + + + + + + + + + +