From dbccbaaa273cc9956882f1ae811facb3b735abb2 Mon Sep 17 00:00:00 2001 From: Mack Duan Date: Mon, 8 Apr 2013 15:14:56 -0700 Subject: [PATCH] Make getOperatorList() calls independent and merge queues at end --- src/core.js | 134 ++- src/evaluator.js | 1581 +++++++++++++++++++++-------------- src/obj.js | 31 +- src/parser.js | 15 + src/pdf_manager.js | 5 +- src/stream.js | 13 +- src/util.js | 13 +- src/worker.js | 11 +- test/test.py | 1 - test/unit/evaluator_spec.js | 157 ++-- 10 files changed, 1208 insertions(+), 753 deletions(-) diff --git a/src/core.js b/src/core.js index 941ce563e..9532be72c 100644 --- a/src/core.js +++ b/src/core.js @@ -18,7 +18,7 @@ isArrayBuffer, isDict, isName, isStream, isString, Lexer, Linearization, NullStream, PartialEvaluator, shadow, Stream, StreamsSequenceStream, stringToPDFString, TODO, Util, warn, XRef, - MissingDataException */ + MissingDataException, PDFJS */ 'use strict'; @@ -60,7 +60,8 @@ var Page = (function PageClosure() { return appearance; } - function Page(xref, pageIndex, pageDict, ref) { + function Page(pdfManager, xref, pageIndex, pageDict, ref) { + this.pdfManager = pdfManager; this.pageIndex = pageIndex; this.pageDict = pageDict; this.xref = xref; @@ -146,28 +147,71 @@ var Page = (function PageClosure() { } return content; }, - getOperatorList: function Page_getOperatorList(handler, dependency) { - var xref = this.xref; - var contentStream = this.getContentStream(); - var resources = this.resources; - var pe = this.pe = new PartialEvaluator( - xref, handler, this.pageIndex, - 'p' + this.pageIndex + '_'); + getOperatorList: function Page_getOperatorList(handler) { + var self = this; + var promise = new PDFJS.Promise(); - var list = pe.getOperatorList(contentStream, resources, dependency); + var pageListPromise = new PDFJS.Promise(); + var annotationListPromise = new PDFJS.Promise(); - var annotations = this.getAnnotationsForDraw(); - var annotationEvaluator = new PartialEvaluator( - xref, handler, this.pageIndex, - 'p' + this.pageIndex + '_annotation'); - var annotationsList = annotationEvaluator.getAnnotationsOperatorList( - annotations, dependency); + var pdfManager = this.pdfManager; + var contentStreamPromise = pdfManager.ensure(this, 'getContentStream', + []); + var resourcesPromise = pdfManager.ensure(this, 'resources'); + var dataPromises = PDFJS.Promise.all( + [contentStreamPromise, resourcesPromise]); + dataPromises.then(function(data) { + var contentStream = data[0]; + var resources = data[1]; + var pe = self.pe = new PartialEvaluator( + self.xref, handler, self.pageIndex, + 'p' + self.pageIndex + '_'); - Util.concatenateToArray(list.fnArray, annotationsList.fnArray); - Util.concatenateToArray(list.argsArray, annotationsList.argsArray); + pdfManager.ensure(pe, 'getOperatorList', + [contentStream, resources]).then( + function(opListPromise) { + opListPromise.then(function(data) { + pageListPromise.resolve(data); + }); + } + ); + }); + + pdfManager.ensure(this, 'getAnnotationsForDraw', []).then( + function(annotations) { + var annotationEvaluator = new PartialEvaluator( + self.xref, handler, self.pageIndex, + 'p' + self.pageIndex + '_annotation'); + + pdfManager.ensure(annotationEvaluator, 'getAnnotationsOperatorList', + [annotations]).then( + function(opListPromise) { + opListPromise.then(function(data) { + annotationListPromise.resolve(data); + }); + } + ); + } + ); + + PDFJS.Promise.all([pageListPromise, annotationListPromise]).then( + function(datas) { + var pageData = datas[0]; + var pageQueue = pageData.queue; + var annotationData = datas[1]; + var annotationQueue = annotationData.queue; + Util.concatenateToArray(pageQueue.fnArray, annotationQueue.fnArray); + Util.concatenateToArray(pageQueue.argsArray, + annotationQueue.argsArray); + PartialEvaluator.optimizeQueue(pageQueue); + Util.extendObj(pageData.dependencies, annotationData.dependencies); + + promise.resolve(pageData); + } + ); + + return promise; - pe.optimizeQueue(list); - return list; }, extractTextContent: function Page_extractTextContent() { var handler = { @@ -175,14 +219,39 @@ var Page = (function PageClosure() { send: function nullHandlerSend() {} }; - var xref = this.xref; - var contentStream = this.getContentStream(); - var resources = xref.fetchIfRef(this.resources); + var self = this; - var pe = new PartialEvaluator( - xref, handler, this.pageIndex, - 'p' + this.pageIndex + '_'); - return pe.getTextContent(contentStream, resources); + var textContentPromise = new PDFJS.Promise(); + + var pdfManager = this.pdfManager; + var contentStreamPromise = pdfManager.ensure(this, 'getContentStream', + []); + var resourcesPromise = new PDFJS.Promise(); + pdfManager.ensure(this, 'resources').then(function(resources) { + pdfManager.ensure(self.xref, 'fetchIfRef', [resources]).then( + function(resources) { + resourcesPromise.resolve(resources); + } + ); + }); + + var dataPromises = PDFJS.Promise.all([contentStreamPromise, + resourcesPromise]); + dataPromises.then(function(data) { + var contentStream = data[0]; + var resources = data[1]; + var pe = new PartialEvaluator( + self.xref, handler, self.pageIndex, + 'p' + self.pageIndex + '_'); + + pe.getTextContent(contentStream, resources).then(function(bidiTexts) { + textContentPromise.resolve({ + bidiTexts: bidiTexts + }); + }); + }); + + return textContentPromise; }, getLinks: function Page_getLinks() { var links = []; @@ -422,17 +491,18 @@ var Page = (function PageClosure() { * `PDFDocument` objects on the main thread created. */ var PDFDocument = (function PDFDocumentClosure() { - function PDFDocument(arg, password) { + function PDFDocument(pdfManager, arg, password) { if (isStream(arg)) - init.call(this, arg, password); + init.call(this, pdfManager, arg, password); else if (isArrayBuffer(arg)) - init.call(this, new Stream(arg), password); + init.call(this, pdfManager, new Stream(arg), password); else error('PDFDocument: Unknown argument type'); } - function init(stream, password) { + function init(pdfManager, stream, password) { assertWellFormed(stream.length > 0, 'stream must have data'); + this.pdfManager = pdfManager; this.stream = stream; var xref = new XRef(this.stream, password); this.xref = xref; @@ -576,7 +646,7 @@ var PDFDocument = (function PDFDocumentClosure() { }, setup: function PDFDocument_setup(recoveryMode) { this.xref.parse(recoveryMode); - this.catalog = new Catalog(this.xref); + this.catalog = new Catalog(this.pdfManager, this.xref); }, get numPages() { var linearization = this.linearization; diff --git a/src/evaluator.js b/src/evaluator.js index 37b7317d3..1a9aaf7d5 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -19,7 +19,7 @@ IDENTITY_MATRIX, info, isArray, isCmd, isDict, isEOF, isName, isNum, isStream, isString, JpegStream, Lexer, Metrics, Name, Parser, Pattern, PDFImage, PDFJS, serifFonts, stdFontMap, symbolsFonts, - TilingPattern, TODO, warn, Util, MissingDataException */ + TilingPattern, TODO, warn, Util, MissingDataException, globalScope */ 'use strict'; @@ -148,9 +148,360 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { 'null': null }; + var TILING_PATTERN = 1, SHADING_PATTERN = 2; + + function createOperatorList(fnArray, argsArray, dependencies) { + return { + queue: { + fnArray: fnArray || [], + argsArray: argsArray || [] + }, + dependencies: dependencies || {} + }; + } + PartialEvaluator.prototype = { + + buildFormXObject: function PartialEvaluator_buildFormXObject(resources, + xobj, smask) { + var self = this; + var promise = new PDFJS.Promise(); + var fnArray = []; + var argsArray = []; + + var matrix = xobj.dict.get('Matrix'); + var bbox = xobj.dict.get('BBox'); + var group = xobj.dict.get('Group'); + if (group) { + var groupOptions = { + matrix: matrix, + bbox: bbox, + smask: !!smask, + isolated: false, + knockout: false + }; + + var groupSubtype = group.get('S'); + if (isName(groupSubtype) && groupSubtype.name === 'Transparency') { + groupOptions.isolated = group.get('I') || false; + groupOptions.knockout = group.get('K') || false; + // There is also a group colorspace, but since we put everything in + // RGB I'm not sure we need it. + } + fnArray.push('beginGroup'); + argsArray.push([groupOptions]); + } + + fnArray.push('paintFormXObjectBegin'); + argsArray.push([matrix, bbox]); + + // Pass in the current `queue` object. That means the `fnArray` + // and the `argsArray` in this scope is reused and new commands + // are added to them. + var opListPromise = this.getOperatorList(xobj, + xobj.dict.get('Resources') || resources); + opListPromise.then(function(data) { + var queue = data.queue; + var dependencies = data.dependencies; + Util.prependToArray(queue.fnArray, fnArray); + Util.prependToArray(queue.argsArray, argsArray); + self.insertDependencies(queue, dependencies); + + queue.fnArray.push('paintFormXObjectEnd'); + queue.argsArray.push([]); + + if (group) { + queue.fnArray.push('endGroup'); + queue.argsArray.push([groupOptions]); + } + + promise.resolve({ + queue: queue, + dependencies: dependencies + }); + }); + + return promise; + }, + + buildPaintImageXObject: function PartialEvaluator_buildPaintImageXObject( + resources, image, inline) { + var self = this; + var dict = image.dict; + var w = dict.get('Width', 'W'); + var h = dict.get('Height', 'H'); + + var dependencies = {}; + var retData = { + dependencies: dependencies + }; + + var imageMask = dict.get('ImageMask', 'IM') || false; + if (imageMask) { + // This depends on a tmpCanvas beeing filled with the + // current fillStyle, such that processing the pixel + // data can't be done here. Instead of creating a + // complete PDFImage, only read the information needed + // for later. + + var width = dict.get('Width', 'W'); + var height = dict.get('Height', 'H'); + var bitStrideLength = (width + 7) >> 3; + var imgArray = image.getBytes(bitStrideLength * height); + var decode = dict.get('Decode', 'D'); + var inverseDecode = !!decode && decode[0] > 0; + + retData.fn = 'paintImageMaskXObject'; + retData.args = [imgArray, inverseDecode, width, height]; + return retData; + } + + var softMask = dict.get('SMask', 'SM') || false; + var mask = dict.get('Mask') || false; + + var SMALL_IMAGE_DIMENSIONS = 200; + // Inlining small images into the queue as RGB data + if (inline && !softMask && !mask && + !(image instanceof JpegStream) && + (w + h) < SMALL_IMAGE_DIMENSIONS) { + var imageObj = new PDFImage(this.xref, resources, image, + inline, null, null); + var imgData = imageObj.getImageData(); + retData.fn = 'paintInlineImageXObject'; + retData.args = [imgData]; + return retData; + } + + // If there is no imageMask, create the PDFImage and a lot + // of image processing can be done here. + var uniquePrefix = this.uniquePrefix || ''; + var objId = 'img_' + uniquePrefix + (++this.objIdCounter); + dependencies[objId] = true; + retData.args = [objId, w, h]; + + if (!softMask && !mask && image instanceof JpegStream && + image.isNativelySupported(this.xref, resources)) { + // These JPEGs don't need any more processing so we can just send it. + retData.fn = 'paintJpegXObject'; + this.handler.send( + 'obj', [objId, this.pageIndex, 'JpegStream', image.getIR()]); + return retData; + } + + retData.fn = 'paintImageXObject'; + + PDFImage.buildImage(function(imageObj) { + var imgData = imageObj.getImageData(); + self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData]); + }, self.handler, self.xref, resources, image, inline); + + return retData; + }, + + handleTilingType: function PartialEvaluator_handleTilingType( + fn, args, resources, pattern, patternDict) { + var self = this; + // Create an IR of the pattern code. + var promise = new PDFJS.Promise(); + var opListPromise = this.getOperatorList(pattern, + patternDict.get('Resources') || resources); + opListPromise.then(function(data) { + var opListData = createOperatorList([], [], data.dependencies); + var queue = opListData.queue; + + // Add the dependencies that are required to execute the + // operatorList. + self.insertDependencies(queue, data.dependencies); + queue.fnArray.push(fn); + queue.argsArray.push( + TilingPattern.getIR(data.queue, patternDict, args)); + promise.resolve(opListData); + }); + + return promise; + }, + + handleSetFont: function PartialEvaluator_handleSetFont( + resources, fontArgs, font) { + + var promise = new PDFJS.Promise(); + // TODO(mack): Not needed? + var fontName; + if (fontArgs) { + fontArgs = fontArgs.slice(); + fontName = fontArgs[0].name; + } + var self = this; + var fontPromise = this.loadFont(fontName, font, this.xref, resources); + fontPromise.then(function(data) { + var font = data.font; + var loadedName = font.loadedName; + if (!font.sent) { + var fontData = font.translated.exportData(); + + self.handler.send('commonobj', [ + loadedName, + 'Font', + fontData + ]); + font.sent = true; + } + + // Ensure the font is ready before the font is set + // and later on used for drawing. + // OPTIMIZE: This should get insert to the operatorList only once per + // page. + var fnArray = []; + var argsArray = []; + var queue = { + fnArray: fnArray, + argsArray: argsArray + }; + var dependencies = data.dependencies; + dependencies[loadedName] = true; + self.insertDependencies(queue, dependencies); + if (fontArgs) { + fontArgs[0] = loadedName; + fnArray.push('setFont'); + argsArray.push(fontArgs); + } + promise.resolve({ + loadedName: loadedName, + queue: queue, + dependencies: dependencies + }); + }); + return promise; + }, + + insertDependencies: function PartialEvaluator_insertDependencies( + queue, dependencies) { + + var fnArray = queue.fnArray; + var argsArray = queue.argsArray; + var depList = Object.keys(dependencies); + if (depList.length) { + fnArray.push('dependency'); + argsArray.push(depList); + } + }, + + setGState: function PartialEvaluator_setGState(resources, gState) { + + var self = this; + var opListData = createOperatorList(); + var queue = opListData.queue; + var fnArray = queue.fnArray; + var argsArray = queue.argsArray; + var dependencies = opListData.dependencies; + + // TODO(mack): This should be rewritten so that this function returns + // what should be added to the queue during each iteration + function setGStateForKey(gStateObj, key, value) { + switch (key) { + case 'Type': + break; + case 'LW': + case 'LC': + case 'LJ': + case 'ML': + case 'D': + case 'RI': + case 'FL': + case 'CA': + case 'ca': + gStateObj.push([key, value]); + break; + case 'Font': + var promise = new PDFJS.Promise(); + self.handleSetFont(resources, null, value[0]).then(function(data) { + var gState = ['Font', data.loadedName, value[1]]; + promise.resolve({ + gState: gState, + queue: data.queue, + dependencies: data.dependencies + }); + }); + gStateObj.push(['promise', promise]); + break; + case 'BM': + if (!isName(value) || value.name !== 'Normal') { + queue.transparency = true; + } + gStateObj.push([key, value]); + break; + case 'SMask': + // We support the default so don't trigger the TODO. + if (!isName(value) || value.name != 'None') + TODO('graphic state operator ' + key); + break; + // Only generate info log messages for the following since + // they are unlikey to have a big impact on the rendering. + case 'OP': + case 'op': + case 'OPM': + case 'BG': + case 'BG2': + case 'UCR': + case 'UCR2': + case 'TR': + case 'TR2': + case 'HT': + case 'SM': + case 'SA': + case 'AIS': + case 'TK': + // TODO implement these operators. + info('graphic state operator ' + key); + break; + default: + info('Unknown graphic state operator ' + key); + break; + } + } + + // This array holds the converted/processed state data. + var gStateObj = []; + var gStateMap = gState.map; + for (var key in gStateMap) { + var value = gStateMap[key]; + setGStateForKey(gStateObj, key, value); + } + + var promises = []; + var indices = []; + for (var i = 0, n = gStateObj.length; i < n; ++i) { + var value = gStateObj[i]; + if (value[0] === 'promise') { + promises.push(value[1]); + indices.push(i); + } + } + + var promise = new PDFJS.Promise(); + PDFJS.Promise.all(promises).then(function(datas) { + for (var i = 0, n = datas.length; i < n; ++i) { + var data = datas[i]; + var index = indices[i]; + gStateObj[index] = data.gState; + var subQueue = data.queue; + Util.concatenateToArray(fnArray, subQueue.fnArray); + Util.concatenateToArray(argsArray, subQueue.argsArray); + queue.transparency = subQueue.transparency || queue.transparency; + Util.extendObj(dependencies, data.dependencies); + } + fnArray.push('setGState'); + argsArray.push([gStateObj]); + promise.resolve(opListData); + }); + + return promise; + }, + loadFont: function PartialEvaluator_loadFont(fontName, font, xref, - resources, dependency) { + resources) { + var promise = new PDFJS.Promise(); + var fontRes = resources.get('Font'); assert(fontRes, 'fontRes not available'); @@ -158,10 +509,14 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { font = xref.fetchIfRef(font) || fontRes.get(fontName); if (!isDict(font)) { ++this.fontIdCounter; - return { - translated: new ErrorFont('Font ' + fontName + ' is not available'), - loadedName: 'g_font_' + this.uniquePrefix + this.fontIdCounter - }; + promise.resolve({ + font: { + translated: new ErrorFont('Font ' + fontName + ' is not available'), + loadedName: 'g_font_' + this.uniquePrefix + this.fontIdCounter + }, + dependencies: {} + }); + return promise; } var loadedName = font.loadedName; @@ -173,8 +528,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var translated; try { - translated = this.translateFont(font, xref, resources, - dependency); + translated = this.translateFont(font, xref); } catch (e) { if (e instanceof MissingDataException) { font.loadedName = undefined; @@ -184,198 +538,66 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } font.translated = translated; - var data = translated; - if (data.loadCharProcs) { - delete data.loadCharProcs; + if (translated.loadCharProcs) { + delete translated.loadCharProcs; var charProcs = font.get('CharProcs').getAll(); var fontResources = font.get('Resources') || resources; - var charProcOperatorList = {}; - for (var key in charProcs) { + var opListPromises = []; + var charProcKeys = Object.keys(charProcs); + for (var i = 0, n = charProcKeys.length; i < n; ++i) { + var key = charProcKeys[i]; var glyphStream = charProcs[key]; - charProcOperatorList[key] = - this.getOperatorList(glyphStream, fontResources, dependency); + opListPromises.push( + this.getOperatorList(glyphStream, fontResources)); } - data.charProcOperatorList = charProcOperatorList; + PDFJS.Promise.all(opListPromises).then(function(datas) { + var charProcOperatorList = {}; + var dependencies = {}; + for (var i = 0, n = charProcKeys.length; i < n; ++i) { + var key = charProcKeys[i]; + var data = datas[i]; + charProcOperatorList[key] = data.queue; + Util.extendObj(dependencies, data.dependencies); + } + translated.charProcOperatorList = charProcOperatorList; + promise.resolve({ + font: font, + dependencies: dependencies + }); + }); + } else { + promise.resolve({ + font: font, + dependencies: {} + }); } + + ++this.fontIdCounter; + } else { + promise.resolve({ + font: font, + dependencies: {} + }); } - ++this.fontIdCounter; - return font; + return promise; }, getOperatorList: function PartialEvaluator_getOperatorList(stream, - resources, - dependency, - queue) { + resources) { var self = this; var xref = this.xref; var handler = this.handler; - var pageIndex = this.pageIndex; - var uniquePrefix = this.uniquePrefix || ''; - function insertDependency(depList) { - fnArray.push('dependency'); - argsArray.push(depList); - for (var i = 0, ii = depList.length; i < ii; i++) { - var dep = depList[i]; - if (dependency.indexOf(dep) == -1) { - dependency.push(depList[i]); - } - } - } - - function handleSetFont(fontName, font) { - font = self.loadFont(fontName, font, xref, resources, dependency); - - var loadedName = font.loadedName; - if (!font.sent) { - var data = font.translated.exportData(); - - handler.send('commonobj', [ - loadedName, - 'Font', - data - ]); - font.sent = true; - } - - // Ensure the font is ready before the font is set - // and later on used for drawing. - // OPTIMIZE: This should get insert to the operatorList only once per - // page. - insertDependency([loadedName]); - return loadedName; - } - - function buildFormXObject(xobj, smask) { - var matrix = xobj.dict.get('Matrix'); - var bbox = xobj.dict.get('BBox'); - var group = xobj.dict.get('Group'); - if (group) { - var groupOptions = { - matrix: matrix, - bbox: bbox, - smask: !!smask, - isolated: false, - knockout: false - }; - - var groupSubtype = group.get('S'); - if (isName(groupSubtype) && groupSubtype.name === 'Transparency') { - groupOptions.isolated = group.get('I') || false; - groupOptions.knockout = group.get('K') || false; - // There is also a group colorspace, but since we put everything in - // RGB I'm not sure we need it. - } - fnArray.push('beginGroup'); - argsArray.push([groupOptions]); - } - - fnArray.push('paintFormXObjectBegin'); - argsArray.push([matrix, bbox]); - - // This adds the operatorList of the xObj to the current queue. - var depIdx = dependencyArray.length; - - // Pass in the current `queue` object. That means the `fnArray` - // and the `argsArray` in this scope is reused and new commands - // are added to them. - self.getOperatorList(xobj, - xobj.dict.get('Resources') || resources, - dependencyArray, queue); - - // Add the dependencies that are required to execute the - // operatorList. - insertDependency(dependencyArray.slice(depIdx)); - - fnArray.push('paintFormXObjectEnd'); - argsArray.push([]); - - if (group) { - fnArray.push('endGroup'); - argsArray.push([groupOptions]); - } - } - - function buildPaintImageXObject(image, inline) { - var dict = image.dict; - var w = dict.get('Width', 'W'); - var h = dict.get('Height', 'H'); - - var imageMask = dict.get('ImageMask', 'IM') || false; - if (imageMask) { - // This depends on a tmpCanvas beeing filled with the - // current fillStyle, such that processing the pixel - // data can't be done here. Instead of creating a - // complete PDFImage, only read the information needed - // for later. - - var width = dict.get('Width', 'W'); - var height = dict.get('Height', 'H'); - var bitStrideLength = (width + 7) >> 3; - var imgArray = image.getBytes(bitStrideLength * height); - var decode = dict.get('Decode', 'D'); - var inverseDecode = !!decode && decode[0] > 0; - - fn = 'paintImageMaskXObject'; - args = [imgArray, inverseDecode, width, height]; - return; - } - - var softMask = dict.get('SMask', 'SM') || false; - var mask = dict.get('Mask') || false; - - var SMALL_IMAGE_DIMENSIONS = 200; - // Inlining small images into the queue as RGB data - if (inline && !softMask && !mask && - !(image instanceof JpegStream) && - (w + h) < SMALL_IMAGE_DIMENSIONS) { - var imageObj = new PDFImage(xref, resources, image, - inline, null, null); - var imgData = imageObj.getImageData(); - fn = 'paintInlineImageXObject'; - args = [imgData]; - return; - } - - // If there is no imageMask, create the PDFImage and a lot - // of image processing can be done here. - var objId = 'img_' + uniquePrefix + (++self.objIdCounter); - insertDependency([objId]); - args = [objId, w, h]; - - if (!softMask && !mask && image instanceof JpegStream && - image.isNativelySupported(xref, resources)) { - // These JPEGs don't need any more processing so we can just send it. - fn = 'paintJpegXObject'; - handler.send('obj', [objId, pageIndex, 'JpegStream', image.getIR()]); - return; - } - - fn = 'paintImageXObject'; - - PDFImage.buildImage(function(imageObj) { - var imgData = imageObj.getImageData(); - handler.send('obj', [objId, pageIndex, 'Image', imgData]); - }, handler, xref, resources, image, inline); - } - - if (!queue) { - queue = { - transparency: false - }; - } - - if (!queue.argsArray) { - queue.argsArray = []; - } - if (!queue.fnArray) { - queue.fnArray = []; - } - - var fnArray = queue.fnArray, argsArray = queue.argsArray; - var dependencyArray = dependency || []; + var fnArray = []; + var argsArray = []; + var queue = { + transparency: false, + fnArray: fnArray, + argsArray: argsArray + }; + var dependencies = {}; resources = resources || new Dict(); var xobjs = resources.get('XObject') || new Dict(); @@ -383,222 +605,235 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { // TODO(mduan): pass array of knownCommands rather than OP_MAP // dictionary var parser = new Parser(new Lexer(stream, OP_MAP), false, xref); - var res = resources; - var args = [], obj; - var TILING_PATTERN = 1, SHADING_PATTERN = 2; - while (true) { - obj = parser.getObj(); - if (isEOF(obj)) { - break; - } + var promise = new PDFJS.Promise(); + function parseCommands() { + try { + parser.restoreState(); + var args = []; + while (true) { - if (isCmd(obj)) { - var cmd = obj.cmd; + var obj = parser.getObj(); - // Check that the command is valid - var opSpec = OP_MAP[cmd]; - if (!opSpec) { - warn('Unknown command "' + cmd + '"'); - continue; - } - - var fn = opSpec.fnName; - - // Validate the number of arguments for the command - if (opSpec.variableArgs) { - if (args.length > opSpec.numArgs) { - info('Command ' + fn + ': expected [0,' + opSpec.numArgs + - '] args, but received ' + args.length + ' args'); + if (isEOF(obj)) { + break; } - } else { - if (args.length < opSpec.numArgs) { - // If we receive too few args, it's not possible to possible - // to execute the command, so skip the command - info('Command ' + fn + ': because expected ' + opSpec.numArgs + - ' args, but received ' + args.length + ' args; skipping'); - args = []; - continue; - } else if (args.length > opSpec.numArgs) { - info('Command ' + fn + ': expected ' + opSpec.numArgs + - ' args, but received ' + args.length + ' args'); - } - } - // TODO figure out how to type-check vararg functions + if (isCmd(obj)) { + var cmd = obj.cmd; - if ((cmd == 'SCN' || cmd == 'scn') && !args[args.length - 1].code) { - // compile tiling patterns - var patternName = args[args.length - 1]; - // SCN/scn applies patterns along with normal colors - if (isName(patternName)) { - var pattern = patterns.get(patternName.name); - if (pattern) { - var dict = isStream(pattern) ? pattern.dict : pattern; - var typeNum = dict.get('PatternType'); - - if (typeNum == TILING_PATTERN) { - // Create an IR of the pattern code. - var depIdx = dependencyArray.length; - var operatorList = this.getOperatorList(pattern, - dict.get('Resources') || resources, dependencyArray); - - // Add the dependencies that are required to execute the - // operatorList. - insertDependency(dependencyArray.slice(depIdx)); - - args = TilingPattern.getIR(operatorList, dict, args); - } - else if (typeNum == SHADING_PATTERN) { - var shading = dict.get('Shading'); - var matrix = dict.get('Matrix'); - var pattern = Pattern.parseShading(shading, matrix, xref, - res); - args = pattern.getIR(); - } else { - error('Unkown PatternType ' + typeNum); - } - } - } - } else if (cmd == 'Do' && !args[0].code) { - // eagerly compile XForm objects - var name = args[0].name; - var xobj = xobjs.get(name); - if (xobj) { - assertWellFormed(isStream(xobj), 'XObject should be a stream'); - - var type = xobj.dict.get('Subtype'); - assertWellFormed( - isName(type), - 'XObject should have a Name subtype' - ); - - if ('Form' == type.name) { - buildFormXObject(xobj); - args = []; + // Check that the command is valid + var opSpec = OP_MAP[cmd]; + if (!opSpec) { + warn('Unknown command "' + cmd + '"'); continue; - } else if ('Image' == type.name) { - buildPaintImageXObject(xobj, false); - } else { - error('Unhandled XObject subtype ' + type.name); } - } - } else if (cmd == 'Tf') { // eagerly collect all fonts - args[0] = handleSetFont(args[0].name); - } else if (cmd == 'EI') { - buildPaintImageXObject(args[0], true); - } - switch (fn) { - // Parse the ColorSpace data to a raw format. - case 'setFillColorSpace': - case 'setStrokeColorSpace': - args = [ColorSpace.parseToIR(args[0], xref, resources)]; - break; - case 'shadingFill': - var shadingRes = res.get('Shading'); - if (!shadingRes) - error('No shading resource found'); + var fn = opSpec.fnName; - var shading = shadingRes.get(args[0].name); - if (!shading) - error('No shading object found'); + // Validate the number of arguments for the command + if (opSpec.variableArgs) { + if (args.length > opSpec.numArgs) { + info('Command ' + fn + ': expected [0,' + opSpec.numArgs + + '] args, but received ' + args.length + ' args'); + } + } else { + if (args.length < opSpec.numArgs) { + // If we receive too few args, it's not possible to possible + // to execute the command, so skip the command + info('Command ' + fn + ': because expected ' + + opSpec.numArgs + ' args, but received ' + args.length + + ' args; skipping'); + args = []; + continue; + } else if (args.length > opSpec.numArgs) { + info('Command ' + fn + ': expected ' + opSpec.numArgs + + ' args, but received ' + args.length + ' args'); + } + } - var shadingFill = Pattern.parseShading(shading, null, xref, res); - var patternIR = shadingFill.getIR(); - args = [patternIR]; - fn = 'shadingFill'; - break; - case 'setGState': - var dictName = args[0]; - var extGState = resources.get('ExtGState'); + // TODO figure out how to type-check vararg functions - if (!isDict(extGState) || !extGState.has(dictName.name)) - break; + if ((cmd == 'SCN' || cmd == 'scn') && + !args[args.length - 1].code) { + // compile tiling patterns + var patternName = args[args.length - 1]; + // SCN/scn applies patterns along with normal colors + var pattern; + if (isName(patternName) && + (pattern = patterns.get(patternName.name))) { - var gsState = extGState.get(dictName.name); + var dict = isStream(pattern) ? pattern.dict : pattern; + var typeNum = dict.get('PatternType'); - // This array holds the converted/processed state data. - var gsStateObj = []; - - gsState.forEach( - function canvasGraphicsSetGStateForEach(key, value) { - switch (key) { - case 'Type': - break; - case 'LW': - case 'LC': - case 'LJ': - case 'ML': - case 'D': - case 'RI': - case 'FL': - case 'CA': - case 'ca': - gsStateObj.push([key, value]); - break; - case 'Font': - gsStateObj.push([ - 'Font', - handleSetFont(null, value[0]), - value[1] - ]); - break; - case 'BM': - if (!isName(value) || value.name !== 'Normal') { - queue.transparency = true; - } - gsStateObj.push([key, value]); - break; - case 'SMask': - // We support the default so don't trigger the TODO. - if (!isName(value) || value.name != 'None') - TODO('graphic state operator ' + key); - break; - // Only generate info log messages for the following since - // they are unlikey to have a big impact on the rendering. - case 'OP': - case 'op': - case 'OPM': - case 'BG': - case 'BG2': - case 'UCR': - case 'UCR2': - case 'TR': - case 'TR2': - case 'HT': - case 'SM': - case 'SA': - case 'AIS': - case 'TK': - // TODO implement these operators. - info('graphic state operator ' + key); - break; - default: - info('Unknown graphic state operator ' + key); - break; + if (typeNum == TILING_PATTERN) { + var patternPromise = self.handleTilingType( + fn, args, resources, pattern, dict); + fn = 'promise'; + args = [patternPromise]; + } else if (typeNum == SHADING_PATTERN) { + var shading = dict.get('Shading'); + var matrix = dict.get('Matrix'); + var pattern = Pattern.parseShading(shading, matrix, xref, + resources); + args = pattern.getIR(); + } else { + error('Unkown PatternType ' + typeNum); } } - ); - args = [gsStateObj]; - break; - } // switch + } else if (cmd == 'Do' && !args[0].code) { + // eagerly compile XForm objects + var name = args[0].name; + var xobj = xobjs.get(name); + if (xobj) { + assertWellFormed( + isStream(xobj), 'XObject should be a stream'); - fnArray.push(fn); - argsArray.push(args); - args = []; - } else if (obj !== null && obj !== undefined) { - args.push(obj instanceof Dict ? obj.getAll() : obj); - assertWellFormed(args.length <= 33, 'Too many arguments'); + var type = xobj.dict.get('Subtype'); + assertWellFormed( + isName(type), + 'XObject should have a Name subtype' + ); + + if ('Form' == type.name) { + fn = 'promise'; + args = [self.buildFormXObject(resources, xobj)]; + } else if ('Image' == type.name) { + var data = self.buildPaintImageXObject( + resources, xobj, false); + Util.extendObj(dependencies, data.dependencies); + self.insertDependencies(queue, data.dependencies); + fn = data.fn; + args = data.args; + } else { + error('Unhandled XObject subtype ' + type.name); + } + } + } else if (cmd == 'Tf') { // eagerly collect all fonts + fn = 'promise'; + args = [self.handleSetFont(resources, args)]; + } else if (cmd == 'EI') { + var data = self.buildPaintImageXObject( + resources, args[0], true); + Util.extendObj(dependencies, data.dependencies); + self.insertDependencies(queue, data.dependencies); + fn = data.fn; + args = data.args; + } + + switch (fn) { + // Parse the ColorSpace data to a raw format. + case 'setFillColorSpace': + case 'setStrokeColorSpace': + args = [ColorSpace.parseToIR(args[0], xref, resources)]; + break; + case 'shadingFill': + var shadingRes = resources.get('Shading'); + if (!shadingRes) + error('No shading resource found'); + + var shading = shadingRes.get(args[0].name); + if (!shading) + error('No shading object found'); + + var shadingFill = Pattern.parseShading( + shading, null, xref, resources); + var patternIR = shadingFill.getIR(); + args = [patternIR]; + fn = 'shadingFill'; + break; + case 'setGState': + var dictName = args[0]; + var extGState = resources.get('ExtGState'); + + if (!isDict(extGState) || !extGState.has(dictName.name)) + break; + + var gState = extGState.get(dictName.name); + fn = 'promise'; + args = [self.setGState(resources, gState)]; + } // switch + + fnArray.push(fn); + argsArray.push(args); + args = []; + parser.saveState(); + } else if (obj !== null && obj !== undefined) { + args.push(obj instanceof Dict ? obj.getAll() : obj); + assertWellFormed(args.length <= 33, 'Too many arguments'); + } + } + + var subQueuePromises = []; + for (var i = 0; i < fnArray.length; ++i) { + if (fnArray[i] === 'promise') { + subQueuePromises.push(argsArray[i][0]); + } + } + PDFJS.Promise.all(subQueuePromises).then(function(datas) { + // TODO(mack): Optimize by using repositioning elements + // in original queue rather than creating new queue + + for (var i = 0, n = datas.length; i < n; ++i) { + var data = datas[i]; + var subQueue = data.queue; + queue.transparency = subQueue.transparency || queue.transparency; + Util.extendObj(dependencies, data.dependencies); + } + + var newFnArray = []; + var newArgsArray = []; + var currOffset = 0; + var subQueueIdx = 0; + for (var i = 0, n = fnArray.length; i < n; ++i) { + var offset = i + currOffset; + if (fnArray[i] === 'promise') { + var data = datas[subQueueIdx++]; + var subQueue = data.queue; + var subQueueFnArray = subQueue.fnArray; + var subQueueArgsArray = subQueue.argsArray; + for (var j = 0, nn = subQueueFnArray.length; j < nn; ++j) { + newFnArray[offset + j] = subQueueFnArray[j]; + newArgsArray[offset + j] = subQueueArgsArray[j]; + } + currOffset += subQueueFnArray.length - 1; + } else { + newFnArray[offset] = fnArray[i]; + newArgsArray[offset] = argsArray[i]; + } + } + + promise.resolve({ + queue: { + fnArray: newFnArray, + argsArray: newArgsArray, + transparency: queue.transparency + }, + dependencies: dependencies + }); + }); + } catch (e) { + if (!(e instanceof MissingDataException)) { + throw e; + } + + var streamManager = globalScope.pdfManager.streamManager; + streamManager.requestRange(e.begin, e.end, function() { + parseCommands(); + }); } } + parser.saveState(); + parseCommands(); - return queue; + return promise; }, getAnnotationsOperatorList: function PartialEvaluator_getAnnotationsOperatorList(annotations, dependency) { + var promise = new PDFJS.Promise(); + // 12.5.5: Algorithm: Appearance streams function getTransformMatrix(rect, bbox, matrix) { var bounds = Util.getAxialAlignedBoundingBox(bbox, matrix); @@ -620,8 +855,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { ]; } - var fnArray = []; - var argsArray = []; + var opListPromises = []; + var includedAnnotations = []; + // deal with annotations for (var i = 0, length = annotations.length; i < length; ++i) { var annotation = annotations[i]; @@ -635,287 +871,245 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { continue; } - // apply rectangle - var rect = annotation.rect; - var bbox = annotation.bbox; - var matrix = annotation.matrix; - var transform = getTransformMatrix(rect, bbox, matrix); - var border = annotation.border; - - fnArray.push('beginAnnotation'); - argsArray.push([rect, transform, matrix, border]); + includedAnnotations.push(annotation); if (annotation.appearance) { - var list = this.getOperatorList(annotation.appearance, - annotation.resources, dependency); - - Util.concatenateToArray(fnArray, list.fnArray); - Util.concatenateToArray(argsArray, list.argsArray); - } - - fnArray.push('endAnnotation'); - argsArray.push([]); - } - - return { - fnArray: fnArray, - argsArray: argsArray - }; - }, - - optimizeQueue: function PartialEvaluator_optimizeQueue(queue) { - var fnArray = queue.fnArray, argsArray = queue.argsArray; - // grouping paintInlineImageXObject's into paintInlineImageXObjectGroup - // searching for (save, transform, paintInlineImageXObject, restore)+ - var MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10; - var MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200; - var MAX_WIDTH = 1000; - var IMAGE_PADDING = 1; - for (var i = 0, ii = fnArray.length; i < ii; i++) { - if (fnArray[i] === 'paintInlineImageXObject' && - fnArray[i - 2] === 'save' && fnArray[i - 1] === 'transform' && - fnArray[i + 1] === 'restore') { - var j = i - 2; - for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) { - } - var count = Math.min((i - j) >> 2, - MAX_IMAGES_IN_INLINE_IMAGES_BLOCK); - if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) { - continue; - } - // assuming that heights of those image is too small (~1 pixel) - // packing as much as possible by lines - var maxX = 0; - var map = [], maxLineHeight = 0; - var currentX = IMAGE_PADDING, currentY = IMAGE_PADDING; - for (var q = 0; q < count; q++) { - var transform = argsArray[j + (q << 2) + 1]; - var img = argsArray[j + (q << 2) + 2][0]; - if (currentX + img.width > MAX_WIDTH) { - // starting new line - maxX = Math.max(maxX, currentX); - currentY += maxLineHeight + 2 * IMAGE_PADDING; - currentX = 0; - maxLineHeight = 0; - } - map.push({ - transform: transform, - x: currentX, y: currentY, - w: img.width, h: img.height - }); - currentX += img.width + 2 * IMAGE_PADDING; - maxLineHeight = Math.max(maxLineHeight, img.height); - } - var imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING; - var imgHeight = currentY + maxLineHeight + IMAGE_PADDING; - var imgData = new Uint8Array(imgWidth * imgHeight * 4); - var imgRowSize = imgWidth << 2; - for (var q = 0; q < count; q++) { - var data = argsArray[j + (q << 2) + 2][0].data; - // copy image by lines and extends pixels into padding - var rowSize = map[q].w << 2; - var dataOffset = 0; - var offset = (map[q].x + map[q].y * imgWidth) << 2; - imgData.set( - data.subarray(0, rowSize), offset - imgRowSize); - for (var k = 0, kk = map[q].h; k < kk; k++) { - imgData.set( - data.subarray(dataOffset, dataOffset + rowSize), offset); - dataOffset += rowSize; - offset += imgRowSize; - } - imgData.set( - data.subarray(dataOffset - rowSize, dataOffset), offset); - while (offset >= 0) { - data[offset - 4] = data[offset]; - data[offset - 3] = data[offset + 1]; - data[offset - 2] = data[offset + 2]; - data[offset - 1] = data[offset + 3]; - data[offset + rowSize] = data[offset + rowSize - 4]; - data[offset + rowSize + 1] = data[offset + rowSize - 3]; - data[offset + rowSize + 2] = data[offset + rowSize - 2]; - data[offset + rowSize + 3] = data[offset + rowSize - 1]; - offset -= imgRowSize; - } - } - // replacing queue items - fnArray.splice(j, count * 4, ['paintInlineImageXObjectGroup']); - argsArray.splice(j, count * 4, - [{width: imgWidth, height: imgHeight, data: imgData}, map]); - i = j; - ii = fnArray.length; + var opListPromise = this.getOperatorList(annotation.appearance, + annotation.resources); + opListPromises.push(opListPromise); + } else { + var opListPromise = new PDFJS.Promise(); + opListPromise.resolve(createOperatorList()); + opListPromises.push(opListPromise); } } - // grouping paintImageMaskXObject's into paintImageMaskXObjectGroup - // searching for (save, transform, paintImageMaskXObject, restore)+ - var MIN_IMAGES_IN_MASKS_BLOCK = 10; - var MAX_IMAGES_IN_MASKS_BLOCK = 100; - for (var i = 0, ii = fnArray.length; i < ii; i++) { - if (fnArray[i] === 'paintImageMaskXObject' && - fnArray[i - 2] === 'save' && fnArray[i - 1] === 'transform' && - fnArray[i + 1] === 'restore') { - var j = i - 2; - for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) { - } - var count = Math.min((i - j) >> 2, - MAX_IMAGES_IN_MASKS_BLOCK); - if (count < MIN_IMAGES_IN_MASKS_BLOCK) { - continue; - } - var images = []; - for (var q = 0; q < count; q++) { - var transform = argsArray[j + (q << 2) + 1]; - var maskParams = argsArray[j + (q << 2) + 2]; - images.push({data: maskParams[0], width: maskParams[2], - height: maskParams[3], transform: transform, - inverseDecode: maskParams[1]}); - } - // replacing queue items - fnArray.splice(j, count * 4, ['paintImageMaskXObjectGroup']); - argsArray.splice(j, count * 4, [images]); - i = j; - ii = fnArray.length; + + PDFJS.Promise.all(opListPromises).then(function(datas) { + var fnArray = []; + var argsArray = []; + var dependencies = {}; + for (var i = 0, n = datas.length; i < n; ++i) { + var annotation = includedAnnotations[i]; + var data = datas[i]; + + // apply rectangle + var rect = annotation.rect; + var bbox = annotation.bbox; + var matrix = annotation.matrix; + var transform = getTransformMatrix(rect, bbox, matrix); + var border = annotation.border; + + fnArray.push('beginAnnotation'); + argsArray.push([rect, transform, matrix, border]); + + Util.concatenateToArray(fnArray, data.queue.fnArray); + Util.concatenateToArray(argsArray, data.queue.argsArray); + Util.extendObj(dependencies, data.dependencies); + + fnArray.push('endAnnotation'); + argsArray.push([]); } - } + + promise.resolve(createOperatorList(fnArray, argsArray, dependencies)); + }); + + return promise; }, getTextContent: function PartialEvaluator_getTextContent( - stream, resources, state) { - var bidiTexts; + stream, resources) { + var SPACE_FACTOR = 0.35; var MULTI_SPACE_FACTOR = 1.5; - - if (!state) { - bidiTexts = []; - state = { - bidiTexts: bidiTexts - }; - } else { - bidiTexts = state.bidiTexts; - } - var self = this; - var xref = this.xref; - function handleSetFont(fontName, fontRef) { - return self.loadFont(fontName, fontRef, xref, resources, null); + var statePromise = new PDFJS.Promise(); + + function handleSetFont(fontName, fontRef, resources) { + var promise = new PDFJS.Promise(); + self.loadFont(fontName, fontRef, self.xref, resources).then( + function(data) { + promise.resolve(data.font.translated); + } + ); + return promise; } - resources = xref.fetchIfRef(resources) || new Dict(); + function getBidiText(str, startLevel, vertical) { + if (str) { + return PDFJS.bidi(str, -1, vertical); + } + } + + resources = this.xref.fetchIfRef(resources) || new Dict(); // The xobj is parsed iff it's needed, e.g. if there is a `DO` cmd. var xobjs = null; var parser = new Parser(new Lexer(stream), false); - var res = resources; - var args = [], obj; - var chunk = ''; - var font = null; - while (!isEOF(obj = parser.getObj())) { - if (isCmd(obj)) { - var cmd = obj.cmd; - switch (cmd) { - // TODO: Add support for SAVE/RESTORE and XFORM here. - case 'Tf': - font = handleSetFont(args[0].name).translated; + var chunkPromises = []; + var fontPromise; + function parseCommands() { + try { + parser.restoreState(); + var args = []; + + while (true) { + var obj = parser.getObj(); + if (isEOF(obj)) { break; - case 'TJ': - var items = args[0]; - for (var j = 0, jj = items.length; j < jj; j++) { - if (typeof items[j] === 'string') { - chunk += fontCharsToUnicode(items[j], font); - } else if (items[j] < 0 && font.spaceWidth > 0) { - var fakeSpaces = -items[j] / font.spaceWidth; - if (fakeSpaces > MULTI_SPACE_FACTOR) { - fakeSpaces = Math.round(fakeSpaces); - while (fakeSpaces--) { - chunk += ' '; + } + + if (isCmd(obj)) { + var cmd = obj.cmd; + switch (cmd) { + // TODO: Add support for SAVE/RESTORE and XFORM here. + case 'Tf': + fontPromise = handleSetFont(args[0].name, null, resources); + //.translated; + break; + case 'TJ': + var chunkPromise = new PDFJS.Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(items, font) { + var chunk = ''; + for (var j = 0, jj = items.length; j < jj; j++) { + if (typeof items[j] === 'string') { + chunk += fontCharsToUnicode(items[j], font); + } else if (items[j] < 0 && font.spaceWidth > 0) { + var fakeSpaces = -items[j] / font.spaceWidth; + if (fakeSpaces > MULTI_SPACE_FACTOR) { + fakeSpaces = Math.round(fakeSpaces); + while (fakeSpaces--) { + chunk += ' '; + } + } else if (fakeSpaces > SPACE_FACTOR) { + chunk += ' '; + } + } } - } else if (fakeSpaces > SPACE_FACTOR) { - chunk += ' '; + + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[0])); + break; + case 'Tj': + var chunkPromise = new PDFJS.Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(charCodes, font) { + var chunk = fontCharsToUnicode(charCodes, font); + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[0])); + break; + case '\'': + // For search, adding a extra white space for line breaks + // would be better here, but that causes too much spaces in + // the text-selection divs. + var chunkPromise = new PDFJS.Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(charCodes, font) { + var chunk = fontCharsToUnicode(charCodes, font); + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[0])); + break; + case '"': + // Note comment in "'" + var chunkPromise = new PDFJS.Promise(); + chunkPromises.push(chunkPromise); + fontPromise.then(function(charCodes, font) { + var chunk = fontCharsToUnicode(charCodes, font); + chunkPromise.resolve( + getBidiText(chunk, -1, font.vertical)); + }.bind(null, args[2])); + break; + case 'Do': + if (args[0].code) { + break; } - } + + if (!xobjs) { + xobjs = resources.get('XObject') || new Dict(); + } + + var name = args[0].name; + var xobj = xobjs.get(name); + if (!xobj) + break; + assertWellFormed(isStream(xobj), + 'XObject should be a stream'); + + var type = xobj.dict.get('Subtype'); + assertWellFormed( + isName(type), + 'XObject should have a Name subtype' + ); + + if ('Form' !== type.name) + break; + + var chunkPromise = self.getTextContent( + xobj, + xobj.dict.get('Resources') || resources + ); + chunkPromises.push(chunkPromise); + break; + case 'gs': + var dictName = args[0]; + var extGState = resources.get('ExtGState'); + + if (!isDict(extGState) || !extGState.has(dictName.name)) + break; + + var gsState = extGState.get(dictName.name); + + for (var i = 0; i < gsState.length; i++) { + if (gsState[i] === 'Font') { + fontPromise = handleSetFont( + args[0].name, null, resources); + } + } + break; + } // switch + + args = []; + parser.saveState(); + } else if (obj !== null && obj !== undefined) { + assertWellFormed(args.length <= 33, 'Too many arguments'); + args.push(obj); + } + } // while + + PDFJS.Promise.all(chunkPromises).then(function(datas) { + var bidiTexts = []; + for (var i = 0, n = datas.length; i < n; ++i) { + var bidiText = datas[i]; + if (!bidiText) { + continue; + } else if (isArray(bidiText)) { + Util.concatenateToArray(bidiTexts, bidiText); + } else { + bidiTexts.push(bidiText); } - break; - case 'Tj': - chunk += fontCharsToUnicode(args[0], font); - break; - case '\'': - // For search, adding a extra white space for line breaks would be - // better here, but that causes too much spaces in the - // text-selection divs. - chunk += fontCharsToUnicode(args[0], font); - break; - case '"': - // Note comment in "'" - chunk += fontCharsToUnicode(args[2], font); - break; - case 'Do': - // Set the chunk such that the following if won't add something - // to the state. - chunk = ''; - - if (args[0].code) { - break; - } - - if (!xobjs) { - xobjs = resources.get('XObject') || new Dict(); - } - - var name = args[0].name; - var xobj = xobjs.get(name); - if (!xobj) - break; - assertWellFormed(isStream(xobj), 'XObject should be a stream'); - - var type = xobj.dict.get('Subtype'); - assertWellFormed( - isName(type), - 'XObject should have a Name subtype' - ); - - if ('Form' !== type.name) - break; - - state = this.getTextContent( - xobj, - xobj.dict.get('Resources') || resources, - state - ); - break; - case 'gs': - var dictName = args[0]; - var extGState = resources.get('ExtGState'); - - if (!isDict(extGState) || !extGState.has(dictName.name)) - break; - - var gsState = extGState.get(dictName.name); - - for (var i = 0; i < gsState.length; i++) { - if (gsState[i] === 'Font') { - font = handleSetFont(args[0].name).translated; - } - } - break; - } // switch - - if (chunk !== '') { - var bidiText = PDFJS.bidi(chunk, -1, font.vertical); - bidiTexts.push(bidiText); - - chunk = ''; + } + statePromise.resolve(bidiTexts); + }); + } catch (e) { + if (!(e instanceof MissingDataException)) { + throw e; } - args = []; - } else if (obj !== null && obj !== undefined) { - assertWellFormed(args.length <= 33, 'Too many arguments'); - args.push(obj); + var streamManager = globalScope.pdfManager.streamManager; + streamManager.requestRange(e.begin, e.end, function() { + parseCommands(); + }); } - } // while + } + parser.saveState(); + parseCommands(); - return state; + return statePromise; }, extractDataStructures: function @@ -1257,9 +1451,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, translateFont: function PartialEvaluator_translateFont(dict, - xref, - resources, - dependency) { + xref) { var baseDict = dict; var type = dict.get('Subtype'); assertWellFormed(isName(type), 'invalid font Subtype'); @@ -1408,6 +1600,125 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } }; + PartialEvaluator.optimizeQueue = + function PartialEvaluator_optimizeQueue(queue) { + + var fnArray = queue.fnArray, argsArray = queue.argsArray; + // grouping paintInlineImageXObject's into paintInlineImageXObjectGroup + // searching for (save, transform, paintInlineImageXObject, restore)+ + var MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10; + var MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200; + var MAX_WIDTH = 1000; + var IMAGE_PADDING = 1; + for (var i = 0, ii = fnArray.length; i < ii; i++) { + if (fnArray[i] === 'paintInlineImageXObject' && + fnArray[i - 2] === 'save' && fnArray[i - 1] === 'transform' && + fnArray[i + 1] === 'restore') { + var j = i - 2; + for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) { + } + var count = Math.min((i - j) >> 2, + MAX_IMAGES_IN_INLINE_IMAGES_BLOCK); + if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) { + continue; + } + // assuming that heights of those image is too small (~1 pixel) + // packing as much as possible by lines + var maxX = 0; + var map = [], maxLineHeight = 0; + var currentX = IMAGE_PADDING, currentY = IMAGE_PADDING; + for (var q = 0; q < count; q++) { + var transform = argsArray[j + (q << 2) + 1]; + var img = argsArray[j + (q << 2) + 2][0]; + if (currentX + img.width > MAX_WIDTH) { + // starting new line + maxX = Math.max(maxX, currentX); + currentY += maxLineHeight + 2 * IMAGE_PADDING; + currentX = 0; + maxLineHeight = 0; + } + map.push({ + transform: transform, + x: currentX, y: currentY, + w: img.width, h: img.height + }); + currentX += img.width + 2 * IMAGE_PADDING; + maxLineHeight = Math.max(maxLineHeight, img.height); + } + var imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING; + var imgHeight = currentY + maxLineHeight + IMAGE_PADDING; + var imgData = new Uint8Array(imgWidth * imgHeight * 4); + var imgRowSize = imgWidth << 2; + for (var q = 0; q < count; q++) { + var data = argsArray[j + (q << 2) + 2][0].data; + // copy image by lines and extends pixels into padding + var rowSize = map[q].w << 2; + var dataOffset = 0; + var offset = (map[q].x + map[q].y * imgWidth) << 2; + imgData.set( + data.subarray(0, rowSize), offset - imgRowSize); + for (var k = 0, kk = map[q].h; k < kk; k++) { + imgData.set( + data.subarray(dataOffset, dataOffset + rowSize), offset); + dataOffset += rowSize; + offset += imgRowSize; + } + imgData.set( + data.subarray(dataOffset - rowSize, dataOffset), offset); + while (offset >= 0) { + data[offset - 4] = data[offset]; + data[offset - 3] = data[offset + 1]; + data[offset - 2] = data[offset + 2]; + data[offset - 1] = data[offset + 3]; + data[offset + rowSize] = data[offset + rowSize - 4]; + data[offset + rowSize + 1] = data[offset + rowSize - 3]; + data[offset + rowSize + 2] = data[offset + rowSize - 2]; + data[offset + rowSize + 3] = data[offset + rowSize - 1]; + offset -= imgRowSize; + } + } + // replacing queue items + fnArray.splice(j, count * 4, ['paintInlineImageXObjectGroup']); + argsArray.splice(j, count * 4, + [{width: imgWidth, height: imgHeight, data: imgData}, map]); + i = j; + ii = fnArray.length; + } + } + // grouping paintImageMaskXObject's into paintImageMaskXObjectGroup + // searching for (save, transform, paintImageMaskXObject, restore)+ + var MIN_IMAGES_IN_MASKS_BLOCK = 10; + var MAX_IMAGES_IN_MASKS_BLOCK = 100; + for (var i = 0, ii = fnArray.length; i < ii; i++) { + if (fnArray[i] === 'paintImageMaskXObject' && + fnArray[i - 2] === 'save' && fnArray[i - 1] === 'transform' && + fnArray[i + 1] === 'restore') { + var j = i - 2; + for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) { + } + var count = Math.min((i - j) >> 2, + MAX_IMAGES_IN_MASKS_BLOCK); + if (count < MIN_IMAGES_IN_MASKS_BLOCK) { + continue; + } + var images = []; + for (var q = 0; q < count; q++) { + var transform = argsArray[j + (q << 2) + 1]; + var maskParams = argsArray[j + (q << 2) + 2]; + images.push({data: maskParams[0], width: maskParams[2], + height: maskParams[3], transform: transform, + inverseDecode: maskParams[1]}); + } + // replacing queue items + fnArray.splice(j, count * 4, ['paintImageMaskXObjectGroup']); + argsArray.splice(j, count * 4, [images]); + i = j; + ii = fnArray.length; + } + } + }; + + return PartialEvaluator; })(); diff --git a/src/obj.js b/src/obj.js index 7f2f1d985..6427d7098 100644 --- a/src/obj.js +++ b/src/obj.js @@ -18,7 +18,7 @@ InvalidPDFException, isArray, isCmd, isDict, isInt, isName, isRef, isStream, JpegStream, Lexer, log, Page, Parser, Promise, shadow, stringToPDFString, stringToUTF8String, warn, isString, assert, PDFJS, - MissingDataException, XRefParseException */ + MissingDataException, XRefParseException, Stream */ 'use strict'; @@ -151,7 +151,8 @@ var RefSet = (function RefSetClosure() { })(); var Catalog = (function CatalogClosure() { - function Catalog(xref) { + function Catalog(pdfManager, xref) { + this.pdfManager = pdfManager; this.xref = xref; this.catDict = xref.getCatalogObj(); assertWellFormed(isDict(this.catDict), @@ -363,7 +364,8 @@ var Catalog = (function CatalogClosure() { var kid = this.xref.fetch(kidRef); if (isDict(kid, 'Page') || (isDict(kid) && !kid.has('Kids'))) { var pageIndex = this.currPageIndex++; - var page = new Page(this.xref, pageIndex, kid, kidRef); + var page = new Page(this.pdfManager, this.xref, pageIndex, kid, + kidRef); if (!(pageIndex in this.pagePromises)) { this.pagePromises[pageIndex] = new PDFJS.Promise(); } @@ -832,10 +834,16 @@ var XRef = (function XRefClosure() { fetch: function XRef_fetch(ref, suppressEncryption) { assertWellFormed(isRef(ref), 'ref object is not a reference'); var num = ref.num; - if (num in this.cache) - return this.cache[num]; + var e; + if (num in this.cache) { + e = this.cache[num]; + if (e instanceof Stream) { + return e.makeSubStream(e.start, e.length, e.dict); + } + return e; + } - var e = this.getEntry(num); + e = this.getEntry(num); // the referenced entry can be free if (e === null) @@ -877,9 +885,16 @@ var XRef = (function XRefClosure() { } else { e = parser.getObj(); } - // Don't cache streams since they are mutable (except images). - if (!isStream(e) || e instanceof JpegStream) + if (!isStream(e) || e instanceof JpegStream) { this.cache[num] = e; + } else if (e instanceof Stream) { + e = e.makeSubStream(e.start, e.length, e.dict); + this.cache[num] = e; + } else if ('readBlock' in e) { + e.getBytes(); + e = e.makeSubStream(0, e.bufferLength, e.dict); + this.cache[num] = e; + } return e; } diff --git a/src/parser.js b/src/parser.js index 570203f5a..af4f9b5c8 100644 --- a/src/parser.js +++ b/src/parser.js @@ -36,6 +36,21 @@ var Parser = (function ParserClosure() { } Parser.prototype = { + saveState: function Parser_saveState() { + this.state = { + buf1: this.buf1, + buf2: this.buf2, + streamPos: this.lexer.stream.pos + }; + }, + + restoreState: function Parser_restoreState() { + var state = this.state; + this.buf1 = state.buf1; + this.buf2 = state.buf2; + this.lexer.stream.pos = state.streamPos; + }, + refill: function Parser_refill() { this.buf1 = this.lexer.getObj(); this.buf2 = this.lexer.getObj(); diff --git a/src/pdf_manager.js b/src/pdf_manager.js index 2a93fe7d9..b92c183e2 100644 --- a/src/pdf_manager.js +++ b/src/pdf_manager.js @@ -67,7 +67,7 @@ var BasePdfManager = (function BasePdfManagerClosure() { var LocalPdfManager = (function LocalPdfManagerClosure() { function LocalPdfManager(data, password) { var stream = new Stream(data); - this.pdfModel = new PDFDocument(stream, password); + this.pdfModel = new PDFDocument(this, stream, password); this.loadedStream = new PDFJS.Promise(); this.loadedStream.resolve(stream); } @@ -124,13 +124,14 @@ var NetworkPdfManager = (function NetworkPdfManagerClosure() { this.streamManager = new ChunkedStreamManager(args.length, CHUNK_SIZE, args.url, params); - this.pdfModel = new PDFDocument(this.streamManager.getStream(), + this.pdfModel = new PDFDocument(this, this.streamManager.getStream(), args.password); } NetworkPdfManager.prototype = Object.create(BasePdfManager.prototype); NetworkPdfManager.prototype.constructor = NetworkPdfManager; + // FIXME(mack): Make ensure() use array for all arguments NetworkPdfManager.prototype.ensure = function NetworkPdfManager_ensure(obj, prop) { var promise = new PDFJS.Promise(); diff --git a/src/stream.js b/src/stream.js index 69279a500..bca26517d 100644 --- a/src/stream.js +++ b/src/stream.js @@ -26,7 +26,7 @@ var Stream = (function StreamClosure() { this.start = start || 0; this.pos = this.start; this.end = (start + length) || this.bytes.length; - this.dict = dict; + this.parameters = this.dict = dict; } // required methods for a stream. if a particular stream does not @@ -645,6 +645,10 @@ var PredictorStream = (function PredictorStreamClosure() { var colors = this.colors; var rawBytes = this.stream.getBytes(rowBytes); + this.eof = !rawBytes.length; + if (this.eof) { + return; + } var inbuf = 0, outbuf = 0; var inbits = 0, outbits = 0; @@ -705,6 +709,10 @@ var PredictorStream = (function PredictorStreamClosure() { var predictor = this.stream.getByte(); var rawBytes = this.stream.getBytes(rowBytes); + this.eof = !rawBytes.length; + if (this.eof) { + return; + } var bufferLength = this.bufferLength; var buffer = this.ensureBuffer(bufferLength + rowBytes); @@ -853,6 +861,7 @@ var JpegStream = (function JpegStreamClosure() { var data = jpegImage.getData(width, height); this.buffer = data; this.bufferLength = data.length; + this.eof = true; } catch (e) { error('JPEG error: ' + e); } @@ -988,6 +997,7 @@ var JpxStream = (function JpxStreamClosure() { this.buffer = data; this.bufferLength = data.length; + this.eof = true; }; JpxStream.prototype.getChar = function JpxStream_getChar() { error('internal error: getChar is not valid on JpxStream'); @@ -1032,6 +1042,7 @@ var Jbig2Stream = (function Jbig2StreamClosure() { this.buffer = data; this.bufferLength = dataLength; + this.eof = true; }; Jbig2Stream.prototype.getChar = function Jbig2Stream_getChar() { error('internal error: getChar is not valid on Jbig2Stream'); diff --git a/src/util.js b/src/util.js index 6b5106155..25dc035bc 100644 --- a/src/util.js +++ b/src/util.js @@ -397,8 +397,19 @@ var Util = PDFJS.Util = (function UtilClosure() { return num < 0 ? -1 : 1; }; + // TODO(mack): Rename appendToArray Util.concatenateToArray = function concatenateToArray(arr1, arr2) { - return Array.prototype.push.apply(arr1, arr2); + Array.prototype.push.apply(arr1, arr2); + }; + + Util.prependToArray = function concatenateToArray(arr1, arr2) { + Array.prototype.unshift.apply(arr1, arr2); + }; + + Util.extendObj = function extendObj(obj1, obj2) { + for (var key in obj2) { + obj1[key] = obj2[key]; + } }; return Util; diff --git a/src/worker.js b/src/worker.js index 3fa9d3e7e..bd5c9a0af 100644 --- a/src/worker.js +++ b/src/worker.js @@ -285,6 +285,7 @@ var WorkerMessageHandler = { }; getPdfManager(data).then(function() { + globalScope.pdfManager = pdfManager; loadDocument(false).then(onSuccess, function(ex) { // Try again with recoveryMode == true if (!(ex instanceof XRefParseException)) { @@ -358,10 +359,11 @@ var WorkerMessageHandler = { var pageNum = data.pageIndex + 1; var start = Date.now(); - var dependency = []; // Pre compile the pdf page and fetch the fonts/images. - pdfManager.ensure(page, 'getOperatorList', handler, - dependency).then(function(operatorList) { + page.getOperatorList(handler).then(function(opListData) { + + var operatorList = opListData.queue; + var dependency = Object.keys(opListData.dependencies); // The following code does quite the same as // Page.prototype.startRendering, but stops at one point and sends the @@ -420,8 +422,7 @@ var WorkerMessageHandler = { pdfManager.getPage(data.pageIndex).then(function(page) { var pageNum = data.pageIndex + 1; var start = Date.now(); - pdfManager.ensure(page, - 'extractTextContent').then(function(textContent) { + page.extractTextContent().then(function(textContent) { promise.resolve(textContent); log('text indexing: page=%d - time=%dms', pageNum, Date.now() - start); diff --git a/test/test.py b/test/test.py index b5bcf75cf..25558b407 100644 --- a/test/test.py +++ b/test/test.py @@ -158,7 +158,6 @@ class TestHandlerBase(BaseHTTPRequestHandler): elif v[0] == errno.EPIPE: print 'Detected remote peer disconnected' elif v[0] == 10053: - # FIXME(mack): Address this issue print 'An established connection was aborted by the' \ ' software in your host machine' else: diff --git a/test/unit/evaluator_spec.js b/test/unit/evaluator_spec.js index a54b38e82..acedb640d 100644 --- a/test/unit/evaluator_spec.js +++ b/test/unit/evaluator_spec.js @@ -33,23 +33,27 @@ describe('evaluator', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('qTT'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(1); - expect(result.fnArray[0]).toEqual('save'); - expect(result.argsArray[0].length).toEqual(0); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(1); + expect(result.fnArray[0]).toEqual('save'); + expect(result.argsArray[0].length).toEqual(0); + }); }); it('should handle one operations', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('Q'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(1); - expect(result.fnArray[0]).toEqual('restore'); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(1); + expect(result.fnArray[0]).toEqual('restore'); + }); }); it('should handle two glued operations', function() { @@ -58,25 +62,29 @@ describe('evaluator', function() { var resources = new ResourcesMock(); resources.Res1 = {}; var stream = new StringStream('/Res1 DoQ'); - var result = evaluator.getOperatorList(stream, resources, []); - - expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(2); - expect(result.fnArray[0]).toEqual('paintXObject'); - expect(result.fnArray[1]).toEqual('restore'); + var promise = evaluator.getOperatorList(stream, resources); + promise.then(function(data) { + var result = data.queue; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(2); + expect(result.fnArray[0]).toEqual('paintXObject'); + expect(result.fnArray[1]).toEqual('restore'); + }); }); it('should handle tree glued operations', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('qqq'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(3); - expect(result.fnArray[0]).toEqual('save'); - expect(result.fnArray[1]).toEqual('save'); - expect(result.fnArray[2]).toEqual('save'); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(3); + expect(result.fnArray[0]).toEqual('save'); + expect(result.fnArray[1]).toEqual('save'); + expect(result.fnArray[2]).toEqual('save'); + }); }); it('should handle three glued operations #2', function() { @@ -85,47 +93,53 @@ describe('evaluator', function() { var resources = new ResourcesMock(); resources.Res1 = {}; var stream = new StringStream('B*Bf*'); - var result = evaluator.getOperatorList(stream, resources, []); - - expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(3); - expect(result.fnArray[0]).toEqual('eoFillStroke'); - expect(result.fnArray[1]).toEqual('fillStroke'); - expect(result.fnArray[2]).toEqual('eoFill'); + var promise = evaluator.getOperatorList(stream, resources); + promise.then(function(data) { + var result = data.queue; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(3); + expect(result.fnArray[0]).toEqual('eoFillStroke'); + expect(result.fnArray[1]).toEqual('fillStroke'); + expect(result.fnArray[2]).toEqual('eoFill'); + }); }); it('should handle glued operations and operands', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('q5 Ts'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(2); - expect(result.fnArray[0]).toEqual('save'); - expect(result.fnArray[1]).toEqual('setTextRise'); - expect(result.argsArray.length).toEqual(2); - expect(result.argsArray[1].length).toEqual(1); - expect(result.argsArray[1][0]).toEqual(5); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(2); + expect(result.fnArray[0]).toEqual('save'); + expect(result.fnArray[1]).toEqual('setTextRise'); + expect(result.argsArray.length).toEqual(2); + expect(result.argsArray[1].length).toEqual(1); + expect(result.argsArray[1][0]).toEqual(5); + }); }); it('should handle glued operations and literals', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('trueifalserinullq'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(3); - expect(result.fnArray[0]).toEqual('setFlatness'); - expect(result.fnArray[1]).toEqual('setRenderingIntent'); - expect(result.fnArray[2]).toEqual('save'); - expect(result.argsArray.length).toEqual(3); - expect(result.argsArray[0].length).toEqual(1); - expect(result.argsArray[0][0]).toEqual(true); - expect(result.argsArray[1].length).toEqual(1); - expect(result.argsArray[1][0]).toEqual(false); - expect(result.argsArray[2].length).toEqual(0); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(!!result.fnArray && !!result.argsArray).toEqual(true); + expect(result.fnArray.length).toEqual(3); + expect(result.fnArray[0]).toEqual('setFlatness'); + expect(result.fnArray[1]).toEqual('setRenderingIntent'); + expect(result.fnArray[2]).toEqual('save'); + expect(result.argsArray.length).toEqual(3); + expect(result.argsArray[0].length).toEqual(1); + expect(result.argsArray[0][0]).toEqual(true); + expect(result.argsArray[1].length).toEqual(1); + expect(result.argsArray[1][0]).toEqual(false); + expect(result.argsArray[2].length).toEqual(0); + }); }); }); @@ -134,31 +148,38 @@ describe('evaluator', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('5 1 d0'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(result.argsArray[0][0]).toEqual(5); - expect(result.argsArray[0][1]).toEqual(1); - expect(result.fnArray[0]).toEqual('setCharWidth'); + console.log('here!'); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(result.argsArray[0][0]).toEqual(5); + expect(result.argsArray[0][1]).toEqual(1); + expect(result.fnArray[0]).toEqual('setCharWidth'); + }); }); it('should execute if too many arguments', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('5 1 4 d0'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(result.argsArray[0][0]).toEqual(5); - expect(result.argsArray[0][1]).toEqual(1); - expect(result.argsArray[0][2]).toEqual(4); - expect(result.fnArray[0]).toEqual('setCharWidth'); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(result.argsArray[0][0]).toEqual(5); + expect(result.argsArray[0][1]).toEqual(1); + expect(result.argsArray[0][2]).toEqual(4); + expect(result.fnArray[0]).toEqual('setCharWidth'); + }); }); it('should skip if too few arguments', function() { var evaluator = new PartialEvaluator(new XrefMock(), new HandlerMock(), 'prefix'); var stream = new StringStream('5 d0'); - var result = evaluator.getOperatorList(stream, new ResourcesMock(), []); - - expect(result.argsArray).toEqual([]); - expect(result.fnArray).toEqual([]); + var promise = evaluator.getOperatorList(stream, new ResourcesMock()); + promise.then(function(data) { + var result = data.queue; + expect(result.argsArray).toEqual([]); + expect(result.fnArray).toEqual([]); + }); }); }); });