Skip to content

Commit b4adbe4

Browse files
committed
Merge pull request mozilla#2417 from yurydelendik/inline-images
Removes "too many inline images" limit
2 parents 1f9b91b + cae6234 commit b4adbe4

File tree

6 files changed

+240
-54
lines changed

6 files changed

+240
-54
lines changed

src/canvas.js

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,27 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
224224
}
225225
}
226226

227+
function applyStencilMask(imgArray, width, height, inverseDecode, buffer) {
228+
var imgArrayPos = 0;
229+
var i, j, mask, buf;
230+
// removing making non-masked pixels transparent
231+
var bufferPos = 3; // alpha component offset
232+
for (i = 0; i < height; i++) {
233+
mask = 0;
234+
for (j = 0; j < width; j++) {
235+
if (!mask) {
236+
buf = imgArray[imgArrayPos++];
237+
mask = 128;
238+
}
239+
if (!(buf & mask) == inverseDecode) {
240+
buffer[bufferPos] = 0;
241+
}
242+
bufferPos += 4;
243+
mask >>= 1;
244+
}
245+
}
246+
}
247+
227248
function rescaleImage(pixels, width, height, widthScale, heightScale) {
228249
var scaledWidth = Math.ceil(width / widthScale);
229250
var scaledHeight = Math.ceil(height / heightScale);
@@ -309,7 +330,10 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
309330
'setFillCMYKColor': true,
310331
'paintJpegXObject': true,
311332
'paintImageXObject': true,
333+
'paintInlineImageXObject': true,
334+
'paintInlineImageXObjectGroup': true,
312335
'paintImageMaskXObject': true,
336+
'paintImageMaskXObjectGroup': true,
313337
'shadingFill': true
314338
},
315339

@@ -1213,55 +1237,70 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
12131237

12141238
paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(
12151239
imgArray, inverseDecode, width, height) {
1216-
function applyStencilMask(buffer, inverseDecode) {
1217-
var imgArrayPos = 0;
1218-
var i, j, mask, buf;
1219-
// removing making non-masked pixels transparent
1220-
var bufferPos = 3; // alpha component offset
1221-
for (i = 0; i < height; i++) {
1222-
mask = 0;
1223-
for (j = 0; j < width; j++) {
1224-
if (!mask) {
1225-
buf = imgArray[imgArrayPos++];
1226-
mask = 128;
1227-
}
1228-
if (!(buf & mask) == inverseDecode) {
1229-
buffer[bufferPos] = 0;
1230-
}
1231-
bufferPos += 4;
1232-
mask >>= 1;
1233-
}
1234-
}
1235-
}
1236-
1237-
var w = width, h = height;
1238-
1239-
var tmpCanvas = createScratchCanvas(w, h);
1240+
var ctx = this.ctx;
1241+
var tmpCanvas = createScratchCanvas(width, height);
12401242
var tmpCtx = tmpCanvas.getContext('2d');
12411243

12421244
var fillColor = this.current.fillColor;
12431245
tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
12441246
fillColor.type === 'Pattern') ?
12451247
fillColor.getPattern(tmpCtx) : fillColor;
1246-
tmpCtx.fillRect(0, 0, w, h);
1248+
tmpCtx.fillRect(0, 0, width, height);
12471249

1248-
var imgData = tmpCtx.getImageData(0, 0, w, h);
1250+
var imgData = tmpCtx.getImageData(0, 0, width, height);
12491251
var pixels = imgData.data;
12501252

1251-
applyStencilMask(pixels, inverseDecode);
1253+
applyStencilMask(imgArray, width, height, inverseDecode, pixels);
12521254

1253-
this.paintImage(imgData);
1255+
this.paintInlineImageXObject(imgData);
1256+
},
1257+
1258+
paintImageMaskXObjectGroup:
1259+
function CanvasGraphics_paintImageMaskXObjectGroup(images) {
1260+
var ctx = this.ctx;
1261+
var tmpCanvasWidth = 0, tmpCanvasHeight = 0, tmpCanvas, tmpCtx;
1262+
for (var i = 0, ii = images.length; i < ii; i++) {
1263+
var image = images[i];
1264+
var w = image.width, h = image.height;
1265+
if (w > tmpCanvasWidth || h > tmpCanvasHeight) {
1266+
tmpCanvasWidth = Math.max(w, tmpCanvasWidth);
1267+
tmpCanvasHeight = Math.max(h, tmpCanvasHeight);
1268+
tmpCanvas = createScratchCanvas(tmpCanvasWidth, tmpCanvasHeight);
1269+
tmpCtx = tmpCanvas.getContext('2d');
1270+
1271+
var fillColor = this.current.fillColor;
1272+
tmpCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
1273+
fillColor.type === 'Pattern') ?
1274+
fillColor.getPattern(tmpCtx) : fillColor;
1275+
}
1276+
tmpCtx.fillRect(0, 0, w, h);
1277+
1278+
var imgData = tmpCtx.getImageData(0, 0, w, h);
1279+
var pixels = imgData.data;
1280+
1281+
applyStencilMask(image.data, w, h, image.inverseDecode, pixels);
1282+
1283+
tmpCtx.putImageData(imgData, 0, 0);
1284+
1285+
ctx.save();
1286+
ctx.transform.apply(ctx, image.transform);
1287+
ctx.scale(1, -1);
1288+
ctx.drawImage(tmpCanvas, 0, 0, w, h,
1289+
0, -1, 1, 1);
1290+
ctx.restore();
1291+
}
12541292
},
12551293

12561294
paintImageXObject: function CanvasGraphics_paintImageXObject(objId) {
12571295
var imgData = this.objs.get(objId);
12581296
if (!imgData)
12591297
error('Dependent image isn\'t ready yet');
12601298

1261-
this.paintImage(imgData);
1299+
this.paintInlineImageXObject(imgData);
12621300
},
12631301

1264-
paintImage: function CanvasGraphics_paintImage(imgData) {
1302+
paintInlineImageXObject:
1303+
function CanvasGraphics_paintInlineImageXObject(imgData) {
12651304
var width = imgData.width;
12661305
var height = imgData.height;
12671306
var ctx = this.ctx;
@@ -1294,6 +1333,27 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
12941333
this.restore();
12951334
},
12961335

1336+
paintInlineImageXObjectGroup:
1337+
function CanvasGraphics_paintInlineImageXObjectGroup(imgData, map) {
1338+
var ctx = this.ctx;
1339+
var w = imgData.width;
1340+
var h = imgData.height;
1341+
1342+
var tmpCanvas = createScratchCanvas(w, h);
1343+
var tmpCtx = tmpCanvas.getContext('2d');
1344+
this.putBinaryImageData(tmpCtx, imgData);
1345+
1346+
for (var i = 0, ii = map.length; i < ii; i++) {
1347+
var entry = map[i];
1348+
ctx.save();
1349+
ctx.transform.apply(ctx, entry.transform);
1350+
ctx.scale(1, -1);
1351+
ctx.drawImage(tmpCanvas, entry.x, entry.y, entry.w, entry.h,
1352+
0, -1, 1, 1);
1353+
ctx.restore();
1354+
}
1355+
},
1356+
12971357
putBinaryImageData: function CanvasGraphics_putBinaryImageData(ctx,
12981358
imgData) {
12991359
var w = imgData.width, h = imgData.height;

src/core.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ var Page = (function PageClosure() {
193193
xref, handler, this.pageIndex,
194194
'p' + this.pageIndex + '_');
195195

196-
return pe.getOperatorList(contentStream, resources, dependency);
196+
var list = pe.getOperatorList(contentStream, resources, dependency);
197+
pe.optimizeQueue(list);
198+
return list;
197199
},
198200
extractTextContent: function Page_extractTextContent() {
199201
var handler = {

src/evaluator.js

Lines changed: 133 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,28 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
260260
return;
261261
}
262262

263+
var softMask = dict.get('SMask', 'SM') || false;
264+
var mask = dict.get('Mask') || false;
265+
266+
var SMALL_IMAGE_DIMENSIONS = 200;
267+
// Inlining small images into the queue as RGB data
268+
if (inline && !softMask && !mask &&
269+
!(image instanceof JpegStream) &&
270+
(w + h) < SMALL_IMAGE_DIMENSIONS) {
271+
var imageObj = new PDFImage(xref, resources, image,
272+
inline, null, null);
273+
var imgData = imageObj.getImageData();
274+
fn = 'paintInlineImageXObject';
275+
args = [imgData];
276+
return;
277+
}
278+
263279
// If there is no imageMask, create the PDFImage and a lot
264280
// of image processing can be done here.
265281
var objId = 'img_' + uniquePrefix + (++self.objIdCounter);
266282
insertDependency([objId]);
267283
args = [objId, w, h];
268284

269-
var softMask = dict.get('SMask', 'SM') || false;
270-
var mask = dict.get('Mask') || false;
271-
272285
if (!softMask && !mask && image instanceof JpegStream &&
273286
image.isNativelySupported(xref, resources)) {
274287
// These JPEGs don't need any more processing so we can just send it.
@@ -280,15 +293,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
280293
fn = 'paintImageXObject';
281294

282295
PDFImage.buildImage(function(imageObj) {
283-
var drawWidth = imageObj.drawWidth;
284-
var drawHeight = imageObj.drawHeight;
285-
var imgData = {
286-
width: drawWidth,
287-
height: drawHeight,
288-
data: new Uint8Array(drawWidth * drawHeight * 4)
289-
};
290-
var pixels = imgData.data;
291-
imageObj.fillRgbaBuffer(pixels, drawWidth, drawHeight);
296+
var imgData = imageObj.getImageData();
292297
handler.send('obj', [objId, pageIndex, 'Image', imgData]);
293298
}, handler, xref, resources, image, inline);
294299
}
@@ -512,6 +517,122 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
512517
return queue;
513518
},
514519

520+
optimizeQueue: function PartialEvaluator_optimizeQueue(queue) {
521+
var fnArray = queue.fnArray, argsArray = queue.argsArray;
522+
// grouping paintInlineImageXObject's into paintInlineImageXObjectGroup
523+
// searching for (save, transform, paintInlineImageXObject, restore)+
524+
var MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10;
525+
var MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200;
526+
var MAX_WIDTH = 1000;
527+
var IMAGE_PADDING = 1;
528+
for (var i = 0, ii = fnArray.length; i < ii; i++) {
529+
if (fnArray[i] === 'paintInlineImageXObject' &&
530+
fnArray[i - 2] === 'save' && fnArray[i - 1] === 'transform' &&
531+
fnArray[i + 1] === 'restore') {
532+
var j = i - 2;
533+
for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) {
534+
}
535+
var count = Math.min((i - j) >> 2,
536+
MAX_IMAGES_IN_INLINE_IMAGES_BLOCK);
537+
if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
538+
continue;
539+
}
540+
// assuming that heights of those image is too small (~1 pixel)
541+
// packing as much as possible by lines
542+
var maxX = 0;
543+
var map = [], maxLineHeight = 0;
544+
var currentX = IMAGE_PADDING, currentY = IMAGE_PADDING;
545+
for (var q = 0; q < count; q++) {
546+
var transform = argsArray[j + (q << 2) + 1];
547+
var img = argsArray[j + (q << 2) + 2][0];
548+
if (currentX + img.width > MAX_WIDTH) {
549+
// starting new line
550+
maxX = Math.max(maxX, currentX);
551+
currentY += maxLineHeight + 2 * IMAGE_PADDING;
552+
currentX = 0;
553+
maxLineHeight = 0;
554+
}
555+
map.push({
556+
transform: transform,
557+
x: currentX, y: currentY,
558+
w: img.width, h: img.height
559+
});
560+
currentX += img.width + 2 * IMAGE_PADDING;
561+
maxLineHeight = Math.max(maxLineHeight, img.height);
562+
}
563+
var imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING;
564+
var imgHeight = currentY + maxLineHeight + IMAGE_PADDING;
565+
var imgData = new Uint8Array(imgWidth * imgHeight * 4);
566+
var imgRowSize = imgWidth << 2;
567+
for (var q = 0; q < count; q++) {
568+
var data = argsArray[j + (q << 2) + 2][0].data;
569+
// copy image by lines and extends pixels into padding
570+
var rowSize = map[q].w << 2;
571+
var dataOffset = 0;
572+
var offset = (map[q].x + map[q].y * imgWidth) << 2;
573+
imgData.set(
574+
data.subarray(0, rowSize), offset - imgRowSize);
575+
for (var k = 0, kk = map[q].h; k < kk; k++) {
576+
imgData.set(
577+
data.subarray(dataOffset, dataOffset + rowSize), offset);
578+
dataOffset += rowSize;
579+
offset += imgRowSize;
580+
}
581+
imgData.set(
582+
data.subarray(dataOffset - rowSize, dataOffset), offset);
583+
while (offset >= 0) {
584+
data[offset - 4] = data[offset];
585+
data[offset - 3] = data[offset + 1];
586+
data[offset - 2] = data[offset + 2];
587+
data[offset - 1] = data[offset + 3];
588+
data[offset + rowSize] = data[offset + rowSize - 4];
589+
data[offset + rowSize + 1] = data[offset + rowSize - 3];
590+
data[offset + rowSize + 2] = data[offset + rowSize - 2];
591+
data[offset + rowSize + 3] = data[offset + rowSize - 1];
592+
offset -= imgRowSize;
593+
}
594+
}
595+
// replacing queue items
596+
fnArray.splice(j, count * 4, ['paintInlineImageXObjectGroup']);
597+
argsArray.splice(j, count * 4,
598+
[{width: imgWidth, height: imgHeight, data: imgData}, map]);
599+
i = j;
600+
ii = fnArray.length;
601+
}
602+
}
603+
// grouping paintImageMaskXObject's into paintImageMaskXObjectGroup
604+
// searching for (save, transform, paintImageMaskXObject, restore)+
605+
var MIN_IMAGES_IN_MASKS_BLOCK = 10;
606+
var MAX_IMAGES_IN_MASKS_BLOCK = 100;
607+
for (var i = 0, ii = fnArray.length; i < ii; i++) {
608+
if (fnArray[i] === 'paintImageMaskXObject' &&
609+
fnArray[i - 2] === 'save' && fnArray[i - 1] === 'transform' &&
610+
fnArray[i + 1] === 'restore') {
611+
var j = i - 2;
612+
for (i += 2; i < ii && fnArray[i - 4] === fnArray[i]; i++) {
613+
}
614+
var count = Math.min((i - j) >> 2,
615+
MAX_IMAGES_IN_MASKS_BLOCK);
616+
if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
617+
continue;
618+
}
619+
var images = [];
620+
for (var q = 0; q < count; q++) {
621+
var transform = argsArray[j + (q << 2) + 1];
622+
var maskParams = argsArray[j + (q << 2) + 2];
623+
images.push({data: maskParams[0], width: maskParams[2],
624+
height: maskParams[3], transform: transform,
625+
inverseDecode: maskParams[1]});
626+
}
627+
// replacing queue items
628+
fnArray.splice(j, count * 4, ['paintImageMaskXObjectGroup']);
629+
argsArray.splice(j, count * 4, [images]);
630+
i = j;
631+
ii = fnArray.length;
632+
}
633+
}
634+
},
635+
515636
getTextContent: function PartialEvaluator_getTextContent(
516637
stream, resources, state) {
517638
var bidiTexts;

src/image.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,18 @@ var PDFImage = (function PDFImageClosure() {
433433
for (var i = 0; i < length; ++i)
434434
buffer[i] = (scale * comps[i]) | 0;
435435
},
436+
getImageData: function PDFImage_getImageData() {
437+
var drawWidth = this.drawWidth;
438+
var drawHeight = this.drawHeight;
439+
var imgData = {
440+
width: drawWidth,
441+
height: drawHeight,
442+
data: new Uint8Array(drawWidth * drawHeight * 4)
443+
};
444+
var pixels = imgData.data;
445+
this.fillRgbaBuffer(pixels, drawWidth, drawHeight);
446+
return imgData;
447+
},
436448
getImageBytes: function PDFImage_getImageBytes(length) {
437449
this.image.reset();
438450
return this.image.getBytes(length);

src/parser.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ var Parser = (function ParserClosure() {
2828
this.lexer = lexer;
2929
this.allowStreams = allowStreams;
3030
this.xref = xref;
31-
this.inlineImg = 0;
3231
this.refill();
3332
}
3433

@@ -153,15 +152,6 @@ var Parser = (function ParserClosure() {
153152
}
154153
}
155154

156-
// TODO improve the small images performance to remove the limit
157-
var inlineImgLimit = 500;
158-
if (++this.inlineImg >= inlineImgLimit) {
159-
if (this.inlineImg === inlineImgLimit)
160-
warn('Too many inline images');
161-
this.shift();
162-
return null;
163-
}
164-
165155
var length = (stream.pos - 4) - startPos;
166156
var imageStream = stream.makeSubStream(startPos, length, dict);
167157
if (cipherTransform)

0 commit comments

Comments
 (0)