Browse Source

Merge pull request #4181 from nnethercote/compact-images

Reduce memory consumption of simple black and white images.
Yury Delendik 11 years ago
parent
commit
c5a804c43a
  1. 7
      src/core/evaluator.js
  2. 60
      src/core/image.js
  3. 84
      src/display/canvas.js

7
src/core/evaluator.js

@ -166,7 +166,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
(w + h) < SMALL_IMAGE_DIMENSIONS) { (w + h) < SMALL_IMAGE_DIMENSIONS) {
var imageObj = new PDFImage(this.xref, resources, image, var imageObj = new PDFImage(this.xref, resources, image,
inline, null, null); inline, null, null);
var imgData = imageObj.getImageData(); var imgData = imageObj.createImageData();
operatorList.addOp(OPS.paintInlineImageXObject, [imgData]); operatorList.addOp(OPS.paintInlineImageXObject, [imgData]);
return; return;
} }
@ -189,7 +189,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
PDFImage.buildImage(function(imageObj) { PDFImage.buildImage(function(imageObj) {
var imgData = imageObj.getImageData(); var imgData = imageObj.createImageData();
self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData], self.handler.send('obj', [objId, self.pageIndex, 'Image', imgData],
null, [imgData.data.buffer]); null, [imgData.data.buffer]);
}, self.handler, self.xref, resources, image, inline); }, self.handler, self.xref, resources, image, inline);
@ -1318,7 +1318,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
// replacing queue items // replacing queue items
squash(fnArray, j, count * 4, OPS.paintInlineImageXObjectGroup); squash(fnArray, j, count * 4, OPS.paintInlineImageXObjectGroup);
argsArray.splice(j, count * 4, argsArray.splice(j, count * 4,
[{width: imgWidth, height: imgHeight, data: imgData}, map]); [{width: imgWidth, height: imgHeight, kind: 'rgba_32bpp',
data: imgData}, map]);
i = j; i = j;
ii = argsArray.length; ii = argsArray.length;
} }

60
src/core/image.js

@ -267,10 +267,10 @@ var PDFImage = (function PDFImageClosure() {
var bpc = this.bpc; var bpc = this.bpc;
// This image doesn't require any extra work. // This image doesn't require any extra work.
if (bpc === 8) if (bpc === 8) {
return buffer; return buffer;
}
var bufferLength = buffer.length;
var width = this.width; var width = this.width;
var height = this.height; var height = this.height;
var numComps = this.numComps; var numComps = this.numComps;
@ -417,7 +417,14 @@ var PDFImage = (function PDFImageClosure() {
buffer[i + 2] = clamp((buffer[i + 2] - matteRgb[2]) * k + matteRgb[2]); buffer[i + 2] = clamp((buffer[i + 2] - matteRgb[2]) * k + matteRgb[2]);
} }
}, },
fillRgbaBuffer: function PDFImage_fillRgbaBuffer(buffer, width, height) { createImageData: function PDFImage_createImageData() {
var drawWidth = this.drawWidth;
var drawHeight = this.drawHeight;
var imgData = { // other fields are filled in below
width: drawWidth,
height: drawHeight,
};
var numComps = this.numComps; var numComps = this.numComps;
var originalWidth = this.width; var originalWidth = this.width;
var originalHeight = this.height; var originalHeight = this.height;
@ -429,21 +436,46 @@ var PDFImage = (function PDFImageClosure() {
// imgArray can be incomplete (e.g. after CCITT fax encoding) // imgArray can be incomplete (e.g. after CCITT fax encoding)
var actualHeight = 0 | (imgArray.length / rowBytes * var actualHeight = 0 | (imgArray.length / rowBytes *
height / originalHeight); 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 comps = this.getComponents(imgArray);
var rgbaBuf = new Uint8Array(drawWidth * drawHeight * 4);
// Handle opacity here since color key masking needs to be performed on // Handle opacity here since color key masking needs to be performed on
// undecoded values. // undecoded values.
this.fillOpacity(buffer, width, height, actualHeight, comps); this.fillOpacity(rgbaBuf, drawWidth, drawHeight, actualHeight, comps);
if (this.needsDecode) { if (this.needsDecode) {
this.decodeBuffer(comps); this.decodeBuffer(comps);
} }
this.colorSpace.fillRgb(buffer, originalWidth, originalHeight, width, this.colorSpace.fillRgb(rgbaBuf, originalWidth, originalHeight, drawWidth,
height, actualHeight, bpc, comps); drawHeight, actualHeight, bpc, comps);
this.undoPreblend(rgbaBuf, drawWidth, actualHeight);
this.undoPreblend(buffer, width, actualHeight); imgData.kind = 'rgba_32bpp';
imgData.data = rgbaBuf;
return imgData;
}, },
fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) { fillGrayBuffer: function PDFImage_fillGrayBuffer(buffer) {
var numComps = this.numComps; var numComps = this.numComps;
@ -468,18 +500,6 @@ var PDFImage = (function PDFImageClosure() {
for (var i = 0; i < length; ++i) for (var i = 0; i < length; ++i)
buffer[i] = (scale * comps[i]) | 0; 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) { getImageBytes: function PDFImage_getImageBytes(length) {
this.image.reset(); this.image.reset();
return this.image.getBytes(length); return this.image.getBytes(length);

84
src/display/canvas.js

@ -437,45 +437,81 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
// of putImageData(). (E.g. in Firefox we make two short-lived copies of // of putImageData(). (E.g. in Firefox we make two short-lived copies of
// the data passed to putImageData()). |n| shouldn't be too small, however, // the data passed to putImageData()). |n| shouldn't be too small, however,
// because too many putImageData() calls will slow things down. // because too many putImageData() calls will slow things down.
//
// Note: as written, if the last chunk is partial, the putImageData() call
// will (conceptually) put pixels past the bounds of the canvas. But
// that's ok; any such pixels are ignored.
var rowsInFullChunks = 16; var fullChunkHeight = 16;
var fullChunks = (imgData.height / rowsInFullChunks) | 0; var fracChunks = imgData.height / fullChunkHeight;
var rowsInLastChunk = imgData.height - fullChunks * rowsInFullChunks; var fullChunks = Math.floor(fracChunks);
var elemsInFullChunks = imgData.width * rowsInFullChunks * 4; var totalChunks = Math.ceil(fracChunks);
var elemsInLastChunk = imgData.width * rowsInLastChunk * 4; var partialChunkHeight = imgData.height - fullChunks * fullChunkHeight;
var chunkImgData = ctx.createImageData(imgData.width, rowsInFullChunks); var chunkImgData = ctx.createImageData(imgData.width, fullChunkHeight);
var srcPos = 0; var srcPos = 0;
var src = imgData.data; var src = imgData.data;
var dst = chunkImgData.data; var dst = chunkImgData.data;
var haveSetAndSubarray = 'set' in dst && 'subarray' in src;
// Do all the full-size chunks. // There are multiple forms in which the pixel data can be passed, and
for (var i = 0; i < fullChunks; i++) { // imgData.kind tells us which one this is.
if (haveSetAndSubarray) {
dst.set(src.subarray(srcPos, srcPos + elemsInFullChunks)); if (imgData.kind === 'grayscale_1bpp') {
srcPos += elemsInFullChunks; // Grayscale, 1 bit per pixel (i.e. black-and-white).
} else { var srcData = imgData.data;
for (var j = 0; j < elemsInFullChunks; j++) { var destData = chunkImgData.data;
chunkImgData.data[j] = imgData.data[srcPos++]; var alpha = 255;
var origLength = imgData.origLength;
for (var i = 0; i < totalChunks; i++) {
var thisChunkHeight =
(i < fullChunks) ? fullChunkHeight : partialChunkHeight;
var destPos = 0;
for (var j = 0; j < thisChunkHeight; j++) {
var mask = 0;
var srcByte = 0;
for (var k = 0; k < imgData.width; k++) {
if (srcPos >= origLength) {
// We ran out of input. Make all remaining pixels transparent.
alpha = 0;
}
if (mask === 0) {
srcByte = srcData[srcPos++];
mask = 128;
}
var c = (+!!(srcByte & mask)) * 255;
destData[destPos++] = c;
destData[destPos++] = c;
destData[destPos++] = c;
destData[destPos++] = alpha;
mask >>= 1;
} }
} }
ctx.putImageData(chunkImgData, 0, i * rowsInFullChunks); ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
} }
// Do the final, partial chunk, if required. } else if (imgData.kind === 'rgba_32bpp') {
if (rowsInLastChunk !== 0) { // RGBA, 32-bits per pixel.
var haveSetAndSubarray = 'set' in dst && 'subarray' in src;
for (var i = 0; i < totalChunks; i++) {
var thisChunkHeight =
(i < fullChunks) ? fullChunkHeight : partialChunkHeight;
var elemsInThisChunk = imgData.width * thisChunkHeight * 4;
if (haveSetAndSubarray) { if (haveSetAndSubarray) {
dst.set(src.subarray(srcPos, srcPos + elemsInLastChunk)); dst.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
srcPos += elemsInLastChunk; srcPos += elemsInThisChunk;
} else { } else {
for (var j = 0; j < elemsInLastChunk; j++) { for (var j = 0; j < elemsInThisChunk; j++) {
chunkImgData.data[j] = imgData.data[srcPos++]; chunkImgData.data[j] = imgData.data[srcPos++];
} }
} }
// This (conceptually) puts pixels past the bounds of the canvas. But ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
// that's ok; any such pixels are ignored. }
ctx.putImageData(chunkImgData, 0, fullChunks * rowsInFullChunks);
} else {
error('bad image kind: ' + imgData.kind);
} }
} }

Loading…
Cancel
Save