From d71c702dcf9cca161ebaa92a3ab05436f886a3c5 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Fri, 7 Dec 2012 12:19:43 -0600 Subject: [PATCH] Removes "too many inline images" limit --- src/canvas.js | 117 +++++++++++++++++++++++++++++---------- src/evaluator.js | 141 +++++++++++++++++++++++++++++++++++++++++++---- src/image.js | 12 ++++ src/parser.js | 10 ---- src/stream.js | 3 +- 5 files changed, 230 insertions(+), 53 deletions(-) diff --git a/src/canvas.js b/src/canvas.js index ad99ba74b..886deef98 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -224,6 +224,27 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } } + function applyStencilMask(imgArray, width, height, inverseDecode, buffer) { + var imgArrayPos = 0; + var i, j, mask, buf; + // removing making non-masked pixels transparent + var bufferPos = 3; // alpha component offset + for (i = 0; i < height; i++) { + mask = 0; + for (j = 0; j < width; j++) { + if (!mask) { + buf = imgArray[imgArrayPos++]; + mask = 128; + } + if (!(buf & mask) == inverseDecode) { + buffer[bufferPos] = 0; + } + bufferPos += 4; + mask >>= 1; + } + } + } + function rescaleImage(pixels, width, height, widthScale, heightScale) { var scaledWidth = Math.ceil(width / widthScale); var scaledHeight = Math.ceil(height / heightScale); @@ -1213,44 +1234,58 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject( imgArray, inverseDecode, width, height) { - function applyStencilMask(buffer, inverseDecode) { - var imgArrayPos = 0; - var i, j, mask, buf; - // removing making non-masked pixels transparent - var bufferPos = 3; // alpha component offset - for (i = 0; i < height; i++) { - mask = 0; - for (j = 0; j < width; j++) { - if (!mask) { - buf = imgArray[imgArrayPos++]; - mask = 128; - } - if (!(buf & mask) == inverseDecode) { - buffer[bufferPos] = 0; - } - bufferPos += 4; - mask >>= 1; - } - } - } - - var w = width, h = height; - - var tmpCanvas = createScratchCanvas(w, h); + var ctx = this.ctx; + var tmpCanvas = createScratchCanvas(width, height); var tmpCtx = tmpCanvas.getContext('2d'); var fillColor = this.current.fillColor; tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && fillColor.type === 'Pattern') ? fillColor.getPattern(tmpCtx) : fillColor; - tmpCtx.fillRect(0, 0, w, h); + tmpCtx.fillRect(0, 0, width, height); - var imgData = tmpCtx.getImageData(0, 0, w, h); + var imgData = tmpCtx.getImageData(0, 0, width, height); var pixels = imgData.data; - applyStencilMask(pixels, inverseDecode); + applyStencilMask(imgArray, width, height, inverseDecode, pixels); - this.paintImage(imgData); + this.paintInlineImageXObject(imgData); + }, + + paintImageMaskXObjectGroup: + function CanvasGraphics_paintImageMaskXObjectGroup(images) { + var ctx = this.ctx; + var tmpCanvasWidth = 0, tmpCanvasHeight = 0, tmpCanvas, tmpCtx; + for (var i = 0, ii = images.length; i < ii; i++) { + var image = images[i]; + var w = image.width, h = image.height; + if (w > tmpCanvasWidth || h > tmpCanvasHeight) { + tmpCanvasWidth = Math.max(w, tmpCanvasWidth); + tmpCanvasHeight = Math.max(h, tmpCanvasHeight); + tmpCanvas = createScratchCanvas(tmpCanvasWidth, tmpCanvasHeight); + tmpCtx = tmpCanvas.getContext('2d'); + + var fillColor = this.current.fillColor; + tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') && + fillColor.type === 'Pattern') ? + fillColor.getPattern(tmpCtx) : fillColor; + } + tmpCtx.fillRect(0, 0, w, h); + + var imgData = tmpCtx.getImageData(0, 0, w, h); + var pixels = imgData.data; + + applyStencilMask(image.data, w, h, image.inverseDecode, pixels); + + tmpCtx.putImageData(imgData, 0, 0); + + ctx.save(); + ctx.transform.apply(ctx, image.transform); + ctx.scale(1, -1); + ctx.drawImage(tmpCanvas, 0, 0, w, h, + 0, -1, 1, 1); + ctx.restore(); + } }, paintImageXObject: function CanvasGraphics_paintImageXObject(objId) { @@ -1258,10 +1293,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (!imgData) error('Dependent image isn\'t ready yet'); - this.paintImage(imgData); + this.paintInlineImageXObject(imgData); }, - paintImage: function CanvasGraphics_paintImage(imgData) { + paintInlineImageXObject: + function CanvasGraphics_paintInlineImageXObject(imgData) { var width = imgData.width; var height = imgData.height; var ctx = this.ctx; @@ -1294,6 +1330,27 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { this.restore(); }, + paintInlineImageXObjectGroup: + function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) { + var ctx = this.ctx; + var w = imgData.width; + var h = imgData.height; + + var tmpCanvas = createScratchCanvas(w, h); + var tmpCtx = tmpCanvas.getContext('2d'); + this.putBinaryImageData(tmpCtx, imgData); + + for (var i = 0, ii = map.length; i < ii; i++) { + var entry = map[i]; + ctx.save(); + ctx.transform.apply(ctx, entry.transform); + ctx.scale(1, -1); + ctx.drawImage(tmpCanvas, entry.x, entry.y, entry.w, entry.h, + 0, -1, 1, 1); + ctx.restore(); + } + }, + putBinaryImageData: function CanvasGraphics_putBinaryImageData(ctx, imgData) { var w = imgData.width, h = imgData.height; diff --git a/src/evaluator.js b/src/evaluator.js index 9ac4a0dc7..81efcca38 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -260,15 +260,28 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { 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]; - var softMask = dict.get('SMask', 'SM') || false; - var mask = dict.get('Mask') || false; - if (!softMask && !mask && image instanceof JpegStream && image.isNativelySupported(xref, resources)) { // These JPEGs don't need any more processing so we can just send it. @@ -280,19 +293,121 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { fn = 'paintImageXObject'; PDFImage.buildImage(function(imageObj) { - var drawWidth = imageObj.drawWidth; - var drawHeight = imageObj.drawHeight; - var imgData = { - width: drawWidth, - height: drawHeight, - data: new Uint8Array(drawWidth * drawHeight * 4) - }; - var pixels = imgData.data; - imageObj.fillRgbaBuffer(pixels, drawWidth, drawHeight); + var imgData = imageObj.getImageData(); handler.send('obj', [objId, pageIndex, 'Image', imgData]); }, handler, xref, resources, image, inline); } + function optimizeQueue() { + // grouping paintInlineImageXObject's into paintInlineImageXObjectGroup + // searching for (save, transform, paintInlineImageXObject, restore)+ + var MIN_IMAGES_COUNT = 10; + 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 = (i - j) >> 2; + if (count < MIN_IMAGES_COUNT) { + 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)+ + 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 = (i - j) >> 2; + if (count < MIN_IMAGES_COUNT) { + 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; + } + } + } + if (!queue) queue = {}; @@ -509,6 +624,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { } } + optimizeQueue(); + return queue; }, diff --git a/src/image.js b/src/image.js index 17e4d5b1a..ea4a647ff 100644 --- a/src/image.js +++ b/src/image.js @@ -433,6 +433,18 @@ var PDFImage = (function PDFImageClosure() { for (var i = 0; i < length; ++i) buffer[i] = (scale * comps[i]) | 0; }, + getImageData: function PDFImage_getImageData() { + var drawWidth = this.drawWidth; + var drawHeight = this.drawHeight; + var imgData = { + width: drawWidth, + height: drawHeight, + data: new Uint8Array(drawWidth * drawHeight * 4) + }; + var pixels = imgData.data; + this.fillRgbaBuffer(pixels, drawWidth, drawHeight); + return imgData; + }, getImageBytes: function PDFImage_getImageBytes(length) { this.image.reset(); return this.image.getBytes(length); diff --git a/src/parser.js b/src/parser.js index 6757e9bf0..6987bbf03 100644 --- a/src/parser.js +++ b/src/parser.js @@ -28,7 +28,6 @@ var Parser = (function ParserClosure() { this.lexer = lexer; this.allowStreams = allowStreams; this.xref = xref; - this.inlineImg = 0; this.refill(); } @@ -153,15 +152,6 @@ var Parser = (function ParserClosure() { } } - // TODO improve the small images performance to remove the limit - var inlineImgLimit = 500; - if (++this.inlineImg >= inlineImgLimit) { - if (this.inlineImg === inlineImgLimit) - warn('Too many inline images'); - this.shift(); - return null; - } - var length = (stream.pos - 4) - startPos; var imageStream = stream.makeSubStream(startPos, length, dict); if (cipherTransform) diff --git a/src/stream.js b/src/stream.js index 2a898df81..1b4c5f966 100644 --- a/src/stream.js +++ b/src/stream.js @@ -19,7 +19,8 @@ var Stream = (function StreamClosure() { function Stream(arrayBuffer, start, length, dict) { - this.bytes = new Uint8Array(arrayBuffer); + this.bytes = arrayBuffer instanceof Uint8Array ? arrayBuffer : + new Uint8Array(arrayBuffer); this.start = start || 0; this.pos = this.start; this.end = (start + length) || this.bytes.length;