Skip to content

Commit e6a578d

Browse files
committed
Improve performances with image masks (bug 857031)
- it's the second part of the fix for https://bugzilla.mozilla.org/show_bug.cgi?id=857031; - some image masks can be used several times but at different positions; - an image need to be pre-process before to be rendered: * rescale it; * use the fill color/pattern. - the two operations above are time consuming so we can cache the generated canvas; - the cache key is based on the current transform matrix (without the translation part) and the current fill color when it isn't a pattern. - the rendering of the pdf in the above bug is really faster than without this patch.
1 parent b73a6cc commit e6a578d

File tree

5 files changed

+151
-29
lines changed

5 files changed

+151
-29
lines changed

bug857031.pdf.link

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
https://bug857031.bmoattachments.org/attachment.cgi?id=732270
2+

src/core/evaluator.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,7 @@ class PartialEvaluator {
675675
width: imgData.width,
676676
height: imgData.height,
677677
interpolate: imgData.interpolate,
678+
count: 1,
678679
},
679680
];
680681

@@ -1676,6 +1677,13 @@ class PartialEvaluator {
16761677
const localImage = localImageCache.getByName(name);
16771678
if (localImage) {
16781679
operatorList.addOp(localImage.fn, localImage.args);
1680+
if (
1681+
localImage.fn === OPS.paintImageMaskXObject &&
1682+
localImage.args[0] &&
1683+
localImage.args[0].count > 0
1684+
) {
1685+
localImage.args[0].count++;
1686+
}
16791687
args = null;
16801688
continue;
16811689
}
@@ -1692,7 +1700,13 @@ class PartialEvaluator {
16921700
const localImage = localImageCache.getByRef(xobj);
16931701
if (localImage) {
16941702
operatorList.addOp(localImage.fn, localImage.args);
1695-
1703+
if (
1704+
localImage.fn === OPS.paintImageMaskXObject &&
1705+
localImage.args[0] &&
1706+
localImage.args[0].count > 0
1707+
) {
1708+
localImage.args[0].count++;
1709+
}
16961710
resolveXObject();
16971711
return;
16981712
}
@@ -1809,6 +1823,13 @@ class PartialEvaluator {
18091823
const localImage = localImageCache.getByName(cacheKey);
18101824
if (localImage) {
18111825
operatorList.addOp(localImage.fn, localImage.args);
1826+
if (
1827+
localImage.fn === OPS.paintImageMaskXObject &&
1828+
localImage.args[0] &&
1829+
localImage.args[0].count > 0
1830+
) {
1831+
localImage.args[0].count++;
1832+
}
18121833
args = null;
18131834
continue;
18141835
}

src/core/operator_list.js

+2
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ addState(
256256
data: maskParams.data,
257257
width: maskParams.width,
258258
height: maskParams.height,
259+
interpolate: maskParams.interpolate,
260+
count: maskParams.count,
259261
transform: transformArgs,
260262
});
261263
}

src/display/canvas.js

+117-28
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,10 @@ class CachedCanvases {
364364
return canvasEntry;
365365
}
366366

367+
delete(id) {
368+
delete this.cache[id];
369+
}
370+
367371
clear() {
368372
for (const id in this.cache) {
369373
const canvasEntry = this.cache[id];
@@ -1121,6 +1125,7 @@ class CanvasGraphics {
11211125
}
11221126
this._cachedScaleForStroking = null;
11231127
this._cachedGetSinglePixelWidth = null;
1128+
this._cachedBitmapsMap = new Map();
11241129
}
11251130

11261131
getObject(data, fallback = null) {
@@ -1156,7 +1161,7 @@ class CanvasGraphics {
11561161
"transparent",
11571162
width,
11581163
height,
1159-
true
1164+
/* trackTransform */ true
11601165
);
11611166
this.compositeCtx = this.ctx;
11621167
this.transparentCanvas = transparentCanvas.canvas;
@@ -1275,6 +1280,19 @@ class CanvasGraphics {
12751280
this.cachedCanvases.clear();
12761281
this.cachedPatterns.clear();
12771282

1283+
for (const cache of this._cachedBitmapsMap.values()) {
1284+
for (const canvas of cache.values()) {
1285+
if (
1286+
typeof HTMLCanvasElement !== "undefined" &&
1287+
canvas instanceof HTMLCanvasElement
1288+
) {
1289+
canvas.width = canvas.height = 0;
1290+
}
1291+
}
1292+
cache.clear();
1293+
}
1294+
this._cachedBitmapsMap.clear();
1295+
12781296
if (this.imageLayer) {
12791297
this.imageLayer.endLayout();
12801298
}
@@ -1316,7 +1334,8 @@ class CanvasGraphics {
13161334
tmpCanvas = this.cachedCanvases.getCanvas(
13171335
tmpCanvasId,
13181336
newWidth,
1319-
newHeight
1337+
newHeight,
1338+
/* trackTransform */ false
13201339
);
13211340
tmpCtx = tmpCanvas.context;
13221341
tmpCtx.clearRect(0, 0, newWidth, newHeight);
@@ -1345,24 +1364,65 @@ class CanvasGraphics {
13451364

13461365
_createMaskCanvas(img) {
13471366
const ctx = this.ctx;
1348-
const width = img.width,
1349-
height = img.height;
1367+
const { width, height } = img;
13501368
const fillColor = this.current.fillColor;
13511369
const isPatternFill = this.current.patternFill;
1352-
const maskCanvas = this.cachedCanvases.getCanvas(
1353-
"maskCanvas",
1354-
width,
1355-
height
1356-
);
1357-
const maskCtx = maskCanvas.context;
1358-
putBinaryImageMask(maskCtx, img);
1370+
const currentTransform = ctx.mozCurrentTransform;
1371+
1372+
let cache, cacheKey, scaled, maskCanvas;
1373+
if ((img.bitmap || img.data) && img.count > 1) {
1374+
const mainKey = img.bitmap || img.data.buffer;
1375+
// We're reusing the same image several times, so we can cache it.
1376+
// In case we've a pattern fill we just keep the scaled version of
1377+
// the image.
1378+
// Only the scaling part matters, the translation part is just used
1379+
// to compute offsets.
1380+
// TODO: handle the case of a pattern fill if it's possible.
1381+
const withoutTranslation = currentTransform.slice(0, 4);
1382+
cacheKey = JSON.stringify(
1383+
isPatternFill ? withoutTranslation : [withoutTranslation, fillColor]
1384+
);
1385+
1386+
cache = this._cachedBitmapsMap.get(mainKey);
1387+
if (!cache) {
1388+
cache = new Map();
1389+
this._cachedBitmapsMap.set(mainKey, cache);
1390+
}
1391+
const cachedImage = cache.get(cacheKey);
1392+
if (cachedImage && !isPatternFill) {
1393+
const offsetX = Math.round(
1394+
Math.min(currentTransform[0], currentTransform[2]) +
1395+
currentTransform[4]
1396+
);
1397+
const offsetY = Math.round(
1398+
Math.min(currentTransform[1], currentTransform[3]) +
1399+
currentTransform[5]
1400+
);
1401+
return {
1402+
canvas: cachedImage,
1403+
offsetX,
1404+
offsetY,
1405+
};
1406+
}
1407+
scaled = cachedImage;
1408+
}
1409+
1410+
if (!scaled) {
1411+
maskCanvas = this.cachedCanvases.getCanvas(
1412+
"maskCanvas",
1413+
width,
1414+
height,
1415+
/* trackTransform */ false
1416+
);
1417+
putBinaryImageMask(maskCanvas.context, img);
1418+
}
13591419

13601420
// Create the mask canvas at the size it will be drawn at and also set
13611421
// its transform to match the current transform so if there are any
13621422
// patterns applied they will be applied relative to the correct
13631423
// transform.
1364-
const objToCanvas = ctx.mozCurrentTransform;
1365-
let maskToCanvas = Util.transform(objToCanvas, [
1424+
1425+
let maskToCanvas = Util.transform(currentTransform, [
13661426
1 / width,
13671427
0,
13681428
0,
@@ -1380,29 +1440,41 @@ class CanvasGraphics {
13801440
"fillCanvas",
13811441
drawnWidth,
13821442
drawnHeight,
1383-
true
1443+
/* trackTransform */ true
13841444
);
13851445
const fillCtx = fillCanvas.context;
1446+
13861447
// The offset will be the top-left cordinate mask.
1448+
// If objToCanvas is [a,b,c,d,e,f] then:
1449+
// - offsetX = min(a, c) + e
1450+
// - offsetY = min(b, d) + f
13871451
const offsetX = Math.min(cord1[0], cord2[0]);
13881452
const offsetY = Math.min(cord1[1], cord2[1]);
13891453
fillCtx.translate(-offsetX, -offsetY);
13901454
fillCtx.transform.apply(fillCtx, maskToCanvas);
1391-
// Pre-scale if needed to improve image smoothing.
1392-
const scaled = this._scaleImage(
1393-
maskCanvas.canvas,
1394-
fillCtx.mozCurrentTransformInverse
1395-
);
1455+
1456+
if (!scaled) {
1457+
// Pre-scale if needed to improve image smoothing.
1458+
scaled = this._scaleImage(
1459+
maskCanvas.canvas,
1460+
fillCtx.mozCurrentTransformInverse
1461+
);
1462+
scaled = scaled.img;
1463+
if (cache && isPatternFill) {
1464+
cache.set(cacheKey, scaled);
1465+
}
1466+
}
1467+
13961468
fillCtx.imageSmoothingEnabled = getImageSmoothingEnabled(
13971469
fillCtx.mozCurrentTransform,
13981470
img.interpolate
13991471
);
14001472
fillCtx.drawImage(
1401-
scaled.img,
1473+
scaled,
14021474
0,
14031475
0,
1404-
scaled.img.width,
1405-
scaled.img.height,
1476+
scaled.width,
1477+
scaled.height,
14061478
0,
14071479
0,
14081480
width,
@@ -1424,6 +1496,13 @@ class CanvasGraphics {
14241496

14251497
fillCtx.fillRect(0, 0, width, height);
14261498

1499+
if (cache && !isPatternFill) {
1500+
// The fill canvas is put in the cache associated to the mask image
1501+
// so we must remove from the cached canvas: it mustn't be used again.
1502+
this.cachedCanvases.delete("fillCanvas");
1503+
cache.set(cacheKey, fillCanvas.canvas);
1504+
}
1505+
14271506
// Round the offsets to avoid drawing fractional pixels.
14281507
return {
14291508
canvas: fillCanvas.canvas,
@@ -1555,7 +1634,7 @@ class CanvasGraphics {
15551634
cacheId,
15561635
drawnWidth,
15571636
drawnHeight,
1558-
true
1637+
/* trackTransform */ true
15591638
);
15601639
this.suspendedCtx = this.ctx;
15611640
this.ctx = scratchCanvas.context;
@@ -2097,7 +2176,8 @@ class CanvasGraphics {
20972176
const { context: ctx } = this.cachedCanvases.getCanvas(
20982177
"isFontSubpixelAAEnabled",
20992178
10,
2100-
10
2179+
10,
2180+
/* trackTransform */ false
21012181
);
21022182
ctx.scale(1.5, 1);
21032183
ctx.fillText("I", 0, 10);
@@ -2606,7 +2686,7 @@ class CanvasGraphics {
26062686
cacheId,
26072687
drawnWidth,
26082688
drawnHeight,
2609-
true
2689+
/* trackTransform */ true
26102690
);
26112691
const groupCtx = scratchCanvas.context;
26122692

@@ -2768,7 +2848,9 @@ class CanvasGraphics {
27682848
return;
27692849
}
27702850

2851+
const count = img.count;
27712852
img = this.getObject(img.data, img);
2853+
img.count = count;
27722854

27732855
const ctx = this.ctx;
27742856
const width = img.width,
@@ -2854,7 +2936,8 @@ class CanvasGraphics {
28542936
const maskCanvas = this.cachedCanvases.getCanvas(
28552937
"maskCanvas",
28562938
width,
2857-
height
2939+
height,
2940+
/* trackTransform */ false
28582941
);
28592942
const maskCtx = maskCanvas.context;
28602943
maskCtx.save();
@@ -2945,7 +3028,8 @@ class CanvasGraphics {
29453028
const tmpCanvas = this.cachedCanvases.getCanvas(
29463029
"inlineImage",
29473030
width,
2948-
height
3031+
height,
3032+
/* trackTransform */ false
29493033
);
29503034
const tmpCtx = tmpCanvas.context;
29513035
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
@@ -2991,7 +3075,12 @@ class CanvasGraphics {
29913075
const w = imgData.width;
29923076
const h = imgData.height;
29933077

2994-
const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h);
3078+
const tmpCanvas = this.cachedCanvases.getCanvas(
3079+
"inlineImage",
3080+
w,
3081+
h,
3082+
/* trackTransform */ false
3083+
);
29953084
const tmpCtx = tmpCanvas.context;
29963085
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
29973086

test/test_manifest.json

+8
Original file line numberDiff line numberDiff line change
@@ -6354,5 +6354,13 @@
63546354
"value": "Hello PDF.js World"
63556355
}
63566356
}
6357+
},
6358+
{ "id": "bug857031",
6359+
"file": "pdfs/bug857031.pdf",
6360+
"md5": "f11ecd7f75675e0cafbc9880c1a586c7",
6361+
"rounds": 1,
6362+
"link": true,
6363+
"lastPage": 1,
6364+
"type": "eq"
63576365
}
63586366
]

0 commit comments

Comments
 (0)