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