From dbb3d17d8f836e60ea5c364b9a51c8ab5cb59bd1 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Sun, 23 Feb 2014 20:42:54 -0600 Subject: [PATCH 1/5] Refactors optimization list --- src/core/evaluator.js | 302 ++++++++++++++++++++++++------------------ 1 file changed, 171 insertions(+), 131 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index b06dfe7f1..0cd268cb7 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -1236,136 +1236,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } }; - PartialEvaluator.optimizeQueue = - function PartialEvaluator_optimizeQueue(queue) { - - function squash(array, index, howMany, element) { - if (isArray(array)) { - array.splice(index, howMany, element); - } else { - // Replace the element. - array[index] = element; - // Shift everything after the element up. - var sub = array.subarray(index + howMany); - array.set(sub, index + 1); - } - } - - 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 = argsArray.length; i < ii; i++) { - if (fnArray[i] === OPS.paintInlineImageXObject && - fnArray[i - 2] === OPS.save && fnArray[i - 1] === OPS.transform && - fnArray[i + 1] === OPS.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 - squash(fnArray, j, count * 4, OPS.paintInlineImageXObjectGroup); - argsArray.splice(j, count * 4, - [{width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP, - data: imgData}, map]); - i = j; - ii = argsArray.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 = argsArray.length; i < ii; i++) { - if (fnArray[i] === OPS.paintImageMaskXObject && - fnArray[i - 2] === OPS.save && fnArray[i - 1] === OPS.transform && - fnArray[i + 1] === OPS.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][0]; - images.push({data: maskParams.data, width: maskParams.width, - height: maskParams.height, transform: transform}); - } - // replacing queue items - squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectGroup); - argsArray.splice(j, count * 4, [images]); - i = j; - ii = argsArray.length; - } - } - }; - return PartialEvaluator; })(); @@ -1454,7 +1324,7 @@ var OperatorList = (function OperatorListClosure() { }, flush: function(lastChunk) { - PartialEvaluator.optimizeQueue(this); + new QueueOptimizer().optimize(this); var transfers = getTransfers(this); this.messageHandler.send('RenderPageChunk', { operatorList: { @@ -1758,3 +1628,173 @@ var EvaluatorPreprocessor = (function EvaluatorPreprocessor() { }; return EvaluatorPreprocessor; })(); + +var QueueOptimizer = (function QueueOptimizerClosure() { + function squash(array, index, howMany, element) { + if (isArray(array)) { + array.splice(index, howMany, element); + } else { + // Replace the element. + array[index] = element; + // Shift everything after the element up. + var sub = array.subarray(index + howMany); + array.set(sub, index + 1); + } + } + + function addState(parentState, pattern, fn) { + var state = parentState; + for (var i = 0, ii = pattern.length - 1; i < ii; i++) { + var item = pattern[i]; + state = state[item] || (state[item] = []); + } + state[pattern[pattern.length - 1]] = fn; + } + + var InitialState = []; + + addState(InitialState, + [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore], + function foundInlineImageGroup(context) { + // 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; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var j = context.currentOperation - 3, i = j + 4; + var ii = context.operationsLength; + + for (; 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) { + context.currentOperation = i - 1; + return; + } + // 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 + squash(fnArray, j, count * 4, OPS.paintInlineImageXObjectGroup); + argsArray.splice(j, count * 4, + [{width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP, + data: imgData}, map]); + context.currentOperation = j; + context.operationsLength -= count * 4 - 1; + }); + + addState(InitialState, + [OPS.save, OPS.transform, OPS.paintImageMaskXObject, OPS.restore], + function foundImageMaskGroup(context) { + // 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; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var j = context.currentOperation - 3, i = j + 4; + var ii = context.operationsLength; + + for (; 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) { + context.currentOperation = i - 1; + return; + } + var images = []; + for (var q = 0; q < count; q++) { + var transform = argsArray[j + (q << 2) + 1]; + var maskParams = argsArray[j + (q << 2) + 2][0]; + images.push({data: maskParams.data, width: maskParams.width, + height: maskParams.height, transform: transform}); + } + // replacing queue items + squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectGroup); + argsArray.splice(j, count * 4, [images]); + + context.currentOperation = j; + context.operationsLength -= count * 4 - 1; + }); + + function QueueOptimizer() { + } + QueueOptimizer.prototype = { + optimize: function QueueOptimizer_optimize(queue) { + var fnArray = queue.fnArray, argsArray = queue.argsArray; + var context = { + currentOperation: 0, + operationsLength: argsArray.length, + fnArray: fnArray, + argsArray: argsArray + }; + var i, ii = argsArray.length; + var state; + for (i = 0; i < ii; i++) { + state = (state || InitialState)[fnArray[i]]; + if (typeof state === 'function') { // we found some handler + context.currentOperation = i; + state = state(context); + i = context.currentOperation; + ii = context.operationsLength; + } + } + } + }; + return QueueOptimizer; +})(); From 2c23be369b234bf25dad5b2e5f9063df53d913d0 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Sun, 23 Feb 2014 21:51:14 -0600 Subject: [PATCH 2/5] Optimizing pdf when text printed char-by-char --- src/core/evaluator.js | 60 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 0cd268cb7..63c7ad8cf 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -1633,12 +1633,16 @@ var QueueOptimizer = (function QueueOptimizerClosure() { function squash(array, index, howMany, element) { if (isArray(array)) { array.splice(index, howMany, element); - } else { + } else if (typeof element !== 'undefined') { // Replace the element. array[index] = element; // Shift everything after the element up. var sub = array.subarray(index + howMany); array.set(sub, index + 1); + } else { + // Shift everything after the element up. + var sub = array.subarray(index + howMany); + array.set(sub, index); } } @@ -1772,6 +1776,60 @@ var QueueOptimizer = (function QueueOptimizerClosure() { context.operationsLength -= count * 4 - 1; }); + addState(InitialState, + [OPS.beginText, OPS.setFont, OPS.setTextMatrix, OPS.showText, OPS.endText], + function (context) { + // moving single chars with same font into beginText/endText groups + // searching for (beginText, setFont, setTextMatrix, showText, endText)+ + var MIN_CHARS_IN_BLOCK = 3; + var MAX_CHARS_IN_BLOCK = 1000; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var j = context.currentOperation - 4, i = j + 5; + var ii = context.operationsLength; + + for (; i < ii && fnArray[i - 5] === fnArray[i]; i++) { + if (fnArray[i] === OPS.setFont) { + if (argsArray[i - 5][0] !== argsArray[i][0] || + argsArray[i - 5][1] !== argsArray[i][1]) { + break; + } + } + } + var count = Math.min(((i - j) / 5) | 0, MAX_CHARS_IN_BLOCK); + if (count < MIN_CHARS_IN_BLOCK) { + context.currentOperation = i - 1; + return; + } + if (j >= 4 && fnArray[j - 4] === fnArray[j + 1] && + fnArray[j - 3] === fnArray[j + 2] && + fnArray[j - 2] === fnArray[j + 3] && + fnArray[j - 1] === fnArray[j + 4] && + argsArray[j - 4][0] === argsArray[j + 1][0] && + argsArray[j - 4][1] === argsArray[j + 1][1]) { + // extending one block ahead (very first block might have 'dependency') + count++; + j -= 5; + } + var k = j + 7; + i = j + 4; + for (var q = 1; q < count; q++) { + fnArray[i] = fnArray[k]; + argsArray[i] = argsArray[k]; + fnArray[i + 1] = fnArray[k + 1]; + argsArray[i + 1] = argsArray[k + 1]; + i += 2; + k += 5; + } + var removed = (count - 1) * 3; + squash(fnArray, i, removed); + argsArray.splice(i, removed); + + context.currentOperation = i; + context.operationsLength -= removed; + + }); + function QueueOptimizer() { } QueueOptimizer.prototype = { From f48f57e30a06171ed1ba56fd73fed3e0a864c852 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 24 Feb 2014 08:00:08 -0600 Subject: [PATCH 3/5] Caches last parsed resource image, recornizes image repeats --- src/core/evaluator.js | 89 ++++++++++++++++++++++++++++++++++++++++--- src/display/canvas.js | 21 +++++++++- src/shared/util.js | 3 +- 3 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 63c7ad8cf..d05f1ee73 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -124,7 +124,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { }, buildPaintImageXObject: function PartialEvaluator_buildPaintImageXObject( - resources, image, inline, operatorList) { + resources, image, inline, operatorList, + cacheKey, cache) { var self = this; var dict = image.dict; var w = dict.get('Width', 'W'); @@ -197,8 +198,12 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], null, [imgData.data.buffer]); }, self.handler, self.xref, resources, image, inline); - operatorList.addOp(OPS.paintImageXObject, args); + if (cacheKey) { + cache.key = cacheKey; + cache.fn = OPS.paintImageXObject; + cache.args = args; + } }, handleSMask: function PartialEvaluator_handleSmask(smask, resources, @@ -452,6 +457,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var self = this; var xref = this.xref; var handler = this.handler; + var imageCache = {}; operatorList = operatorList || new OperatorList(); @@ -507,6 +513,12 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } // eagerly compile XForm objects var name = args[0].name; + if (imageCache.key === name) { + operatorList.addOp(imageCache.fn, imageCache.args); + args = []; + continue; + } + var xobj = xobjs.get(name); if (xobj) { assertWellFormed( @@ -525,7 +537,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { continue; } else if ('Image' == type.name) { self.buildPaintImageXObject(resources, xobj, false, - operatorList); + operatorList, name, imageCache); args = []; continue; } else { @@ -641,6 +653,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { resources = 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 xobjsCache = {}; var preprocessor = new EvaluatorPreprocessor(stream, xref); var res = resources; @@ -735,6 +748,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } var name = args[0].name; + if (xobjsCache.key === name) { + if (xobjsCache.texts) { + Util.concatenateToArray(bidiTexts, xobjsCache.texts); + } + break; + } + var xobj = xobjs.get(name); if (!xobj) break; @@ -746,14 +766,19 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { 'XObject should have a Name subtype' ); - if ('Form' !== type.name) + if ('Form' !== type.name) { + xobjsCache.key = name; + xobjsCache.texts = null; break; + } var formTexts = this.getTextContent( xobj, xobj.dict.get('Resources') || resources, textState ); + xobjsCache.key = name; + xobjsCache.texts = formTexts; Util.concatenateToArray(bidiTexts, formTexts); break; case OPS.setGState: @@ -1240,7 +1265,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { })(); var OperatorList = (function OperatorListClosure() { - var CHUNK_SIZE = 100; + var CHUNK_SIZE = 1000; function getTransfers(queue) { var transfers = []; @@ -1776,6 +1801,60 @@ var QueueOptimizer = (function QueueOptimizerClosure() { context.operationsLength -= count * 4 - 1; }); + addState(InitialState, + [OPS.save, OPS.transform, OPS.paintImageXObject, OPS.restore], + function (context) { + var MIN_IMAGES_IN_BLOCK = 3; + var MAX_IMAGES_IN_BLOCK = 1000; + + var fnArray = context.fnArray, argsArray = context.argsArray; + var j = context.currentOperation - 3, i = j + 4; + if (argsArray[j + 1][1] !== 0 || argsArray[j + 1][2] !== 0) { + return; + } + var ii = context.operationsLength; + for (; i + 3 < ii && fnArray[i - 4] === fnArray[i]; i += 4) { + if (fnArray[i - 3] !== fnArray[i + 1] || + fnArray[i - 2] !== fnArray[i + 2] || + fnArray[i - 1] !== fnArray[i + 3]) { + break; + } + if (argsArray[i - 2][0] !== argsArray[i + 2][0]) { + break; // different image + } + var prevTransformArgs = argsArray[i - 3]; + var transformArgs = argsArray[i + 1]; + if (prevTransformArgs[0] !== transformArgs[0] || + prevTransformArgs[1] !== transformArgs[1] || + prevTransformArgs[2] !== transformArgs[2] || + prevTransformArgs[3] !== transformArgs[3]) { + break; // different transform + } + } + var count = Math.min((i - j) >> 2, MAX_IMAGES_IN_BLOCK); + if (count < MIN_IMAGES_IN_BLOCK) { + context.currentOperation = i - 1; + return; + } + + var positions = new Float32Array(count * 2); + i = j + 1; + for (var q = 0; q < count; q++) { + var transformArgs = argsArray[i]; + positions[(q << 1)] = transformArgs[4]; + positions[(q << 1) + 1] = transformArgs[5]; + i += 4; + } + var args = [argsArray[j + 2][0], argsArray[j + 1][0], + argsArray[j + 1][3], positions]; + // replacing queue items + squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectRepeat); + argsArray.splice(j, count * 4, args); + + context.currentOperation = j; + context.operationsLength -= count * 4 - 1; + }); + addState(InitialState, [OPS.beginText, OPS.setFont, OPS.setTextMatrix, OPS.showText, OPS.endText], function (context) { diff --git a/src/display/canvas.js b/src/display/canvas.js index 3942f553e..0226b874e 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -1959,12 +1959,31 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { paintImageXObject: function CanvasGraphics_paintImageXObject(objId) { var imgData = this.objs.get(objId); - if (!imgData) + if (!imgData) { error('Dependent image isn\'t ready yet'); + } this.paintInlineImageXObject(imgData); }, + paintImageMaskXObjectRepeat: + function CanvasGraphics_paintImageMaskXObjectRepeat(objId, scaleX, scaleY, + positions) { + var imgData = this.objs.get(objId); + if (!imgData) { + error('Dependent image isn\'t ready yet'); + } + + var width = imgData.width; + var height = imgData.height; + var map = []; + for (var i = 0, ii = positions.length; i < ii; i += 2) { + map.push({transform: [scaleX, 0, 0, scaleY, positions[i], + positions[i + 1]], x: 0, y: 0, w: width, h: height}); + } + this.paintInlineImageXObjectGroup(imgData, map); + }, + paintInlineImageXObject: function CanvasGraphics_paintInlineImageXObject(imgData) { var width = imgData.width; diff --git a/src/shared/util.js b/src/shared/util.js index 07de8e33d..65b07a5c0 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -149,7 +149,8 @@ var OPS = PDFJS.OPS = { paintImageMaskXObjectGroup: 84, paintImageXObject: 85, paintInlineImageXObject: 86, - paintInlineImageXObjectGroup: 87 + paintInlineImageXObjectGroup: 87, + paintImageMaskXObjectRepeat: 88 }; // A notice for devs. These are good for things that are helpful to devs, such From 257898b359e108fa373115870c89605ba270c090 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 24 Feb 2014 09:59:02 -0600 Subject: [PATCH 4/5] Caching inlined mask images --- src/core/evaluator.js | 100 +++++++++++++++++++++++++++++++++--------- src/core/parser.js | 45 ++++++++++++++++++- src/display/canvas.js | 61 ++++++++++++++++++++------ src/shared/util.js | 3 +- 4 files changed, 173 insertions(+), 36 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index d05f1ee73..54b4a9e69 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -152,10 +152,16 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var canTransfer = image instanceof DecodeStream; var inverseDecode = !!decode && decode[0] > 0; - operatorList.addOp(OPS.paintImageMaskXObject, - [PDFImage.createMask(imgArray, width, height, canTransfer, - inverseDecode)] - ); + var imgData = PDFImage.createMask(imgArray, width, height, + canTransfer, inverseDecode); + imgData.cached = true; + var args = [imgData]; + operatorList.addOp(OPS.paintImageMaskXObject, args); + if (cacheKey) { + cache.key = cacheKey; + cache.fn = OPS.paintImageMaskXObject; + cache.args = args; + } return; } @@ -553,8 +559,14 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { args[0] = loadedName; break; case OPS.endInlineImage: + var cacheKey = args[0].cacheKey; + if (cacheKey && imageCache.key === cacheKey) { + operatorList.addOp(imageCache.fn, imageCache.args); + args = []; + continue; + } self.buildPaintImageXObject(resources, args[0], true, - operatorList); + operatorList, cacheKey, imageCache); args = []; continue; case OPS.save: @@ -1276,7 +1288,9 @@ var OperatorList = (function OperatorListClosure() { case OPS.paintInlineImageXObjectGroup: case OPS.paintImageMaskXObject: var arg = argsArray[i][0]; // first param in imgData - transfers.push(arg.data.buffer); + if (!arg.cached) { + transfers.push(arg.data.buffer); + } break; } } @@ -1774,6 +1788,7 @@ var QueueOptimizer = (function QueueOptimizerClosure() { // searching for (save, transform, paintImageMaskXObject, restore)+ var MIN_IMAGES_IN_MASKS_BLOCK = 10; var MAX_IMAGES_IN_MASKS_BLOCK = 100; + var MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000; var fnArray = context.fnArray, argsArray = context.argsArray; var j = context.currentOperation - 3, i = j + 4; @@ -1781,24 +1796,69 @@ var QueueOptimizer = (function QueueOptimizerClosure() { for (; i < ii && fnArray[i - 4] === fnArray[i]; i++) { } - var count = Math.min((i - j) >> 2, MAX_IMAGES_IN_MASKS_BLOCK); + var count = (i - j) >> 2; if (count < MIN_IMAGES_IN_MASKS_BLOCK) { context.currentOperation = i - 1; return; } - var images = []; - for (var q = 0; q < count; q++) { - var transform = argsArray[j + (q << 2) + 1]; - var maskParams = argsArray[j + (q << 2) + 2][0]; - images.push({data: maskParams.data, width: maskParams.width, - height: maskParams.height, transform: transform}); - } - // replacing queue items - squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectGroup); - argsArray.splice(j, count * 4, [images]); - context.currentOperation = j; - context.operationsLength -= count * 4 - 1; + var isSameImage = false; + if (argsArray[j + 1][1] === 0 && argsArray[j + 1][2] === 0) { + i = j + 4; + isSameImage = true; + for (var q = 1; q < count; q++, i += 4) { + var prevTransformArgs = argsArray[i - 3]; + var transformArgs = argsArray[i + 1]; + if (argsArray[i - 2][0] !== argsArray[i + 2][0] || + prevTransformArgs[0] !== transformArgs[0] || + prevTransformArgs[1] !== transformArgs[1] || + prevTransformArgs[2] !== transformArgs[2] || + prevTransformArgs[3] !== transformArgs[3]) { + if (q < MIN_IMAGES_IN_MASKS_BLOCK) { + isSameImage = false; + } else { + count = q; + } + break; // different image or transform + } + } + } + + if (isSameImage) { + count = Math.min(count, MAX_SAME_IMAGES_IN_MASKS_BLOCK); + var positions = new Float32Array(count * 2); + i = j + 1; + for (var q = 0; q < count; q++) { + var transformArgs = argsArray[i]; + positions[(q << 1)] = transformArgs[4]; + positions[(q << 1) + 1] = transformArgs[5]; + i += 4; + } + + // replacing queue items + squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectRepeat); + argsArray.splice(j, count * 4, [argsArray[j + 2][0], + argsArray[j + 1][0], argsArray[j + 1][3], positions]); + + context.currentOperation = j; + context.operationsLength -= count * 4 - 1; + } else { + count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK); + var images = []; + for (var q = 0; q < count; q++) { + var transformArgs = argsArray[j + (q << 2) + 1]; + var maskParams = argsArray[j + (q << 2) + 2][0]; + images.push({data: maskParams.data, width: maskParams.width, + height: maskParams.height, transform: transformArgs}); + } + + // replacing queue items + squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectGroup); + argsArray.splice(j, count * 4, [images]); + + context.currentOperation = j; + context.operationsLength -= count * 4 - 1; + } }); addState(InitialState, @@ -1848,7 +1908,7 @@ var QueueOptimizer = (function QueueOptimizerClosure() { var args = [argsArray[j + 2][0], argsArray[j + 1][0], argsArray[j + 1][3], positions]; // replacing queue items - squash(fnArray, j, count * 4, OPS.paintImageMaskXObjectRepeat); + squash(fnArray, j, count * 4, OPS.paintImageXObjectRepeat); argsArray.splice(j, count * 4, args); context.currentOperation = j; diff --git a/src/core/parser.js b/src/core/parser.js index 5d7aba4af..0eb62c501 100644 --- a/src/core/parser.js +++ b/src/core/parser.js @@ -32,6 +32,11 @@ var Parser = (function ParserClosure() { this.lexer = lexer; this.allowStreams = allowStreams; this.xref = xref; + this.imageCache = { + length: 0, + adler32: 0, + stream: null + }; this.refill(); } @@ -169,10 +174,48 @@ var Parser = (function ParserClosure() { var length = (stream.pos - 4) - startPos; var imageStream = stream.makeSubStream(startPos, length, dict); - if (cipherTransform) + + // trying to cache repeat images, first we are trying to "warm up" caching + // using length, then comparing adler32 + var MAX_LENGTH_TO_CACHE = 1000; + var cacheImage = false, adler32; + if (length < MAX_LENGTH_TO_CACHE && this.imageCache.length === length) { + var imageBytes = imageStream.getBytes(); + imageStream.reset(); + + var a = 1; + var b = 0; + for (var i = 0, ii = imageBytes.length; i < ii; ++i) { + a = (a + (imageBytes[i] & 0xff)) % 65521; + b = (b + a) % 65521; + } + adler32 = (b << 16) | a; + + if (this.imageCache.stream && this.imageCache.adler32 === adler32) { + this.buf2 = Cmd.get('EI'); + this.shift(); + + this.imageCache.stream.reset(); + return this.imageCache.stream; + } + cacheImage = true; + } + if (!cacheImage && !this.imageCache.stream) { + this.imageCache.length = length; + this.imageCache.stream = null; + } + + if (cipherTransform) { imageStream = cipherTransform.createStream(imageStream, length); + } + imageStream = this.filter(imageStream, dict, length); imageStream.dict = dict; + if (cacheImage) { + imageStream.cacheKey = 'inline_' + length + '_' + adler32; + this.imageCache.adler32 = adler32; + this.imageCache.stream = imageStream; + } this.buf2 = Cmd.get('EI'); this.shift(); diff --git a/src/display/canvas.js b/src/display/canvas.js index 0226b874e..0ff14eaae 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -1924,6 +1924,39 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.paintInlineImageXObject(maskCanvas.canvas); }, + paintImageMaskXObjectRepeat: + function CanvasGraphics_paintImageMaskXObjectRepeat(imgData, scaleX, + scaleY, positions) { + var width = imgData.width; + var height = imgData.height; + var ctx = this.ctx; + + var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height); + var maskCtx = maskCanvas.context; + maskCtx.save(); + + putBinaryImageMask(maskCtx, imgData); + + maskCtx.globalCompositeOperation = 'source-in'; + + var fillColor = this.current.fillColor; + maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && + fillColor.type === 'Pattern') ? + fillColor.getPattern(maskCtx, this) : fillColor; + maskCtx.fillRect(0, 0, width, height); + + maskCtx.restore(); + + for (var i = 0, ii = positions.length; i < ii; i += 2) { + ctx.save(); + ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]); + ctx.scale(1, -1); + ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, + 0, -1, 1, 1); + ctx.restore(); + } + }, + paintImageMaskXObjectGroup: function CanvasGraphics_paintImageMaskXObjectGroup(images) { var ctx = this.ctx; @@ -1966,22 +1999,22 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.paintInlineImageXObject(imgData); }, - paintImageMaskXObjectRepeat: - function CanvasGraphics_paintImageMaskXObjectRepeat(objId, scaleX, scaleY, + paintImageXObjectRepeat: + function CanvasGraphics_paintImageXObjectRepeat(objId, scaleX, scaleY, positions) { - var imgData = this.objs.get(objId); - if (!imgData) { - error('Dependent image isn\'t ready yet'); - } + var imgData = this.objs.get(objId); + if (!imgData) { + error('Dependent image isn\'t ready yet'); + } - var width = imgData.width; - var height = imgData.height; - var map = []; - for (var i = 0, ii = positions.length; i < ii; i += 2) { - map.push({transform: [scaleX, 0, 0, scaleY, positions[i], - positions[i + 1]], x: 0, y: 0, w: width, h: height}); - } - this.paintInlineImageXObjectGroup(imgData, map); + var width = imgData.width; + var height = imgData.height; + var map = []; + for (var i = 0, ii = positions.length; i < ii; i += 2) { + map.push({transform: [scaleX, 0, 0, scaleY, positions[i], + positions[i + 1]], x: 0, y: 0, w: width, h: height}); + } + this.paintInlineImageXObjectGroup(imgData, map); }, paintInlineImageXObject: diff --git a/src/shared/util.js b/src/shared/util.js index 65b07a5c0..4803e6daf 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -150,7 +150,8 @@ var OPS = PDFJS.OPS = { paintImageXObject: 85, paintInlineImageXObject: 86, paintInlineImageXObjectGroup: 87, - paintImageMaskXObjectRepeat: 88 + paintImageXObjectRepeat: 88, + paintImageMaskXObjectRepeat: 89 }; // A notice for devs. These are good for things that are helpful to devs, such From 1aaeec80208bf73bb57b025ca464a00bda1eeff2 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 24 Feb 2014 10:07:36 -0600 Subject: [PATCH 5/5] Heuristic to optimize chunking --- src/core/evaluator.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 54b4a9e69..226532187 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -1278,6 +1278,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { var OperatorList = (function OperatorListClosure() { var CHUNK_SIZE = 1000; + var CHUNK_SIZE_ABOUT = CHUNK_SIZE - 5; // close to chunk size function getTransfers(queue) { var transfers = []; @@ -1326,6 +1327,10 @@ var OperatorList = (function OperatorListClosure() { this.argsArray.push(args); if (this.fnIndex >= CHUNK_SIZE) { this.flush(); + } else if (this.fnIndex >= CHUNK_SIZE_ABOUT && + (fn === OPS.restore || fn === OPS.endText)) { + // heuristic to flush on boundary of restore or endText + this.flush(); } } else { this.fnArray.push(fn);