diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 3f9a179b9..288a7299b 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -16,7 +16,7 @@ */ /* globals assert, assertWellFormed, ColorSpace, Dict, Encodings, error, ErrorFont, Font, FONT_IDENTITY_MATRIX, fontCharsToUnicode, FontFlags, - info, isArray, isCmd, isDict, isEOF, isName, isNum, + ImageKind, info, isArray, isCmd, isDict, isEOF, isName, isNum, isStream, isString, JpegStream, Lexer, Metrics, Name, Parser, Pattern, PDFImage, PDFJS, serifFonts, stdFontMap, symbolsFonts, getTilingPatternIR, warn, Util, Promise, LegacyPromise, @@ -164,7 +164,9 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { (w + h) < SMALL_IMAGE_DIMENSIONS) { var imageObj = new PDFImage(this.xref, resources, image, inline, null, null); - var imgData = imageObj.createImageData(); + // We force the use of RGBA_32BPP images here, because we can't handle + // any other kind. + var imgData = imageObj.createImageData(/* forceRGBA = */ true); operatorList.addOp(OPS.paintInlineImageXObject, [imgData]); return; } @@ -187,7 +189,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { PDFImage.buildImage(function(imageObj) { - var imgData = imageObj.createImageData(); + var imgData = imageObj.createImageData(/* forceRGBA = */ false); self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], null, [imgData.data.buffer]); }, self.handler, self.xref, resources, image, inline); @@ -1313,7 +1315,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { // replacing queue items squash(fnArray, j, count * 4, OPS.paintInlineImageXObjectGroup); argsArray.splice(j, count * 4, - [{width: imgWidth, height: imgHeight, kind: 'rgba_32bpp', + [{width: imgWidth, height: imgHeight, kind: ImageKind.RGBA_32BPP, data: imgData}, map]); i = j; ii = argsArray.length; diff --git a/src/core/image.js b/src/core/image.js index e7d2b69a0..8c7d8d4e0 100644 --- a/src/core/image.js +++ b/src/core/image.js @@ -14,8 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals ColorSpace, error, isArray, isStream, JpegStream, Name, Promise, - Stream, warn, LegacyPromise */ +/* globals ColorSpace, error, isArray, ImageKind, isStream, JpegStream, Name, + Promise, Stream, warn, LegacyPromise */ 'use strict'; @@ -417,7 +417,7 @@ var PDFImage = (function PDFImageClosure() { buffer[i + 2] = clamp((buffer[i + 2] - matteRgb[2]) * k + matteRgb[2]); } }, - createImageData: function PDFImage_createImageData() { + createImageData: function PDFImage_createImageData(forceRGBA) { var drawWidth = this.drawWidth; var drawHeight = this.drawHeight; var imgData = { // other fields are filled in below @@ -430,32 +430,41 @@ var PDFImage = (function PDFImageClosure() { var originalHeight = this.height; var bpc = this.bpc; - // rows start at byte boundary; + // Rows start at byte boundary. var rowBytes = (originalWidth * numComps * bpc + 7) >> 3; var imgArray = this.getImageBytes(originalHeight * rowBytes); - // imgArray can be incomplete (e.g. after CCITT fax encoding) + if (!forceRGBA) { + // If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image + // without any complications, we pass a same-sized copy to the main + // thread rather than expanding by 32x to RGBA form. This saves *lots* + // of memory for many scanned documents. It's also much faster. + // + // Similarly, if it is a 24-bit-per pixel RGB image without any + // complications, we avoid expanding by 1.333x to RGBA form. + var kind; + if (this.colorSpace.name === 'DeviceGray' && bpc === 1) { + kind = ImageKind.GRAYSCALE_1BPP; + } else if (this.colorSpace.name === 'DeviceRGB' && bpc === 8) { + kind = ImageKind.RGB_24BPP; + } + if (kind && !this.smask && !this.mask && !this.needsDecode && + drawWidth === originalWidth && drawHeight === originalHeight) { + imgData.kind = kind; + + // We must make a copy of imgArray, otherwise it'll be neutered upon + // transfer which will break any code that subsequently reuses it. + var newArray = new Uint8Array(imgArray.length); + newArray.set(imgArray); + imgData.data = newArray; + return imgData; + } + } + + // imgArray can be incomplete (e.g. after CCITT fax encoding). var actualHeight = 0 | (imgArray.length / rowBytes * drawHeight / originalHeight); - // If it is a 1-bit-per-pixel grayscale (i.e. black-and-white) image - // without any complications, we pass a same-sized copy to the main - // thread rather than expanding by 32x to RGBA form. This saves *lots* of - // memory for many scanned documents. It's also much faster. - if (this.colorSpace.name === 'DeviceGray' && bpc === 1 && - !this.smask && !this.mask && !this.needsDecode && - drawWidth === originalWidth && drawHeight === originalHeight) { - imgData.kind = 'grayscale_1bpp'; - - // We must make a copy of imgArray, otherwise it'll be neutered upon - // transfer which will break any code that subsequently reuses it. - var newArray = new Uint8Array(imgArray.length); - newArray.set(imgArray); - imgData.data = newArray; - imgData.origLength = imgArray.length; - return imgData; - } - var comps = this.getComponents(imgArray); var rgbaBuf = new Uint8Array(drawWidth * drawHeight * 4); @@ -473,7 +482,7 @@ var PDFImage = (function PDFImageClosure() { this.undoPreblend(rgbaBuf, drawWidth, actualHeight); - imgData.kind = 'rgba_32bpp'; + imgData.kind = ImageKind.RGBA_32BPP; imgData.data = rgbaBuf; return imgData; }, diff --git a/src/display/canvas.js b/src/display/canvas.js index f94483b47..f9ca1721b 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -15,9 +15,9 @@ * limitations under the License. */ /* globals ColorSpace, DeviceCmykCS, DeviceGrayCS, DeviceRgbCS, error, - FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, ImageData, isArray, isNum, - TilingPattern, OPS, Promise, Util, warn, assert, info, shadow, - TextRenderingMode, getShadingPatternFromIR */ + FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, ImageData, ImageKind, + isArray, isNum, TilingPattern, OPS, Promise, Util, warn, assert, + info, shadow, TextRenderingMode, getShadingPatternFromIR */ 'use strict'; @@ -452,19 +452,17 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var chunkImgData = ctx.createImageData(width, fullChunkHeight); var srcPos = 0; var src = imgData.data; - var dst = chunkImgData.data; + var dest = chunkImgData.data; // There are multiple forms in which the pixel data can be passed, and // imgData.kind tells us which one this is. - if (imgData.kind === 'grayscale_1bpp') { + if (imgData.kind === ImageKind.GRAYSCALE_1BPP) { // Grayscale, 1 bit per pixel (i.e. black-and-white). - var srcData = imgData.data; - var destData = chunkImgData.data; - var destDataLength = destData.length; - var origLength = imgData.origLength; + var destDataLength = dest.length; + var srcLength = src.byteLength; for (var i = 3; i < destDataLength; i += 4) { - destData[i] = 255; + dest[i] = 255; } for (var i = 0; i < totalChunks; i++) { var thisChunkHeight = @@ -475,21 +473,21 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var srcByte = 0; for (var k = 0; k < width; k++, destPos += 4) { if (mask === 0) { - if (srcPos >= origLength) { + if (srcPos >= srcLength) { break; } - srcByte = srcData[srcPos++]; + srcByte = src[srcPos++]; mask = 128; } if ((srcByte & mask)) { - destData[destPos] = 255; - destData[destPos + 1] = 255; - destData[destPos + 2] = 255; + dest[destPos] = 255; + dest[destPos + 1] = 255; + dest[destPos + 2] = 255; } else { - destData[destPos] = 0; - destData[destPos + 1] = 0; - destData[destPos + 2] = 0; + dest[destPos] = 0; + dest[destPos + 1] = 0; + dest[destPos + 2] = 0; } mask >>= 1; @@ -499,7 +497,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { // We ran out of input. Make all remaining pixels transparent. destPos += 3; do { - destData[destPos] = 0; + dest[destPos] = 0; destPos += 4; } while (destPos < destDataLength); } @@ -507,25 +505,41 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); } - } else if (imgData.kind === 'rgba_32bpp') { + } else if (imgData.kind === ImageKind.RGBA_32BPP) { // RGBA, 32-bits per pixel. - var haveSetAndSubarray = 'set' in dst && 'subarray' in src; + var haveSetAndSubarray = 'set' in dest && 'subarray' in src; for (var i = 0; i < totalChunks; i++) { var thisChunkHeight = (i < fullChunks) ? fullChunkHeight : partialChunkHeight; var elemsInThisChunk = imgData.width * thisChunkHeight * 4; if (haveSetAndSubarray) { - dst.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); + dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk)); srcPos += elemsInThisChunk; } else { for (var j = 0; j < elemsInThisChunk; j++) { - chunkImgData.data[j] = imgData.data[srcPos++]; + dest[j] = src[srcPos++]; } } ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); } + } else if (imgData.kind === ImageKind.RGB_24BPP) { + // RGB, 24-bits per pixel. + for (var i = 0; i < totalChunks; i++) { + var thisChunkHeight = + (i < fullChunks) ? fullChunkHeight : partialChunkHeight; + var elemsInThisChunk = imgData.width * thisChunkHeight * 3; + var destPos = 0; + for (var j = 0; j < elemsInThisChunk; j += 3) { + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = src[srcPos++]; + dest[destPos++] = 255; + } + ctx.putImageData(chunkImgData, 0, i * fullChunkHeight); + } + } else { error('bad image kind: ' + imgData.kind); } diff --git a/src/shared/util.js b/src/shared/util.js index 587fbc0a3..343ac14e3 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -38,6 +38,12 @@ var TextRenderingMode = { ADD_TO_PATH_FLAG: 4 }; +var ImageKind = { + GRAYSCALE_1BPP: 1, + RGB_24BPP: 2, + RGBA_32BPP: 3 +}; + // The global PDFJS object exposes the API // In production, it will be declared outside a global wrapper // In development, it will be declared here