Skip to content

Commit 2847d25

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 143ba30 commit 2847d25

File tree

3 files changed

+135
-26
lines changed

3 files changed

+135
-26
lines changed

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

+111-25
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,16 @@ 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 (canvas instanceof HTMLCanvasElement) {
1286+
canvas.width = canvas.height = 0;
1287+
}
1288+
}
1289+
cache.clear();
1290+
}
1291+
this._cachedBitmapsMap.clear();
1292+
12781293
if (this.imageLayer) {
12791294
this.imageLayer.endLayout();
12801295
}
@@ -1316,7 +1331,8 @@ class CanvasGraphics {
13161331
tmpCanvas = this.cachedCanvases.getCanvas(
13171332
tmpCanvasId,
13181333
newWidth,
1319-
newHeight
1334+
newHeight,
1335+
/* trackTransform */ false
13201336
);
13211337
tmpCtx = tmpCanvas.context;
13221338
tmpCtx.clearRect(0, 0, newWidth, newHeight);
@@ -1349,19 +1365,60 @@ class CanvasGraphics {
13491365
height = img.height;
13501366
const fillColor = this.current.fillColor;
13511367
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);
1368+
const objToCanvas = ctx.mozCurrentTransform;
1369+
1370+
let cache, cacheKey, scaled, maskCanvas;
1371+
if ((img.bitmap || img.data) && img.count >= 2) {
1372+
const mainKey = img.bitmap || img.data.buffer;
1373+
// We're reusing the same image several times, so we can cache it.
1374+
// In case we've a pattern fill we just keep the scaled version of
1375+
// the image.
1376+
// Only the scaling part matters, the translation part is just used
1377+
// to compute offsets.
1378+
// TODO: handle the case of a pattern fill if it's possible.
1379+
const withoutTranslation = objToCanvas.slice(0, 4);
1380+
cacheKey = isPatternFill
1381+
? withoutTranslation
1382+
: [withoutTranslation, fillColor];
1383+
cacheKey = JSON.stringify(cacheKey);
1384+
1385+
cache = this._cachedBitmapsMap.get(mainKey);
1386+
if (!cache) {
1387+
cache = new Map();
1388+
this._cachedBitmapsMap.set(mainKey, cache);
1389+
}
1390+
const cachedImage = cache.get(cacheKey);
1391+
if (cachedImage && !isPatternFill) {
1392+
const offsetX = Math.round(
1393+
Math.min(objToCanvas[0], objToCanvas[2]) + objToCanvas[4]
1394+
);
1395+
const offsetY = Math.round(
1396+
Math.min(objToCanvas[1], objToCanvas[3]) + objToCanvas[5]
1397+
);
1398+
return {
1399+
canvas: cachedImage,
1400+
offsetX,
1401+
offsetY,
1402+
};
1403+
}
1404+
scaled = cachedImage;
1405+
}
1406+
1407+
if (!scaled) {
1408+
maskCanvas = this.cachedCanvases.getCanvas(
1409+
"maskCanvas",
1410+
width,
1411+
height,
1412+
/* trackTransform */ false
1413+
);
1414+
putBinaryImageMask(maskCanvas.context, img);
1415+
}
13591416

13601417
// Create the mask canvas at the size it will be drawn at and also set
13611418
// its transform to match the current transform so if there are any
13621419
// patterns applied they will be applied relative to the correct
13631420
// transform.
1364-
const objToCanvas = ctx.mozCurrentTransform;
1421+
13651422
let maskToCanvas = Util.transform(objToCanvas, [
13661423
1 / width,
13671424
0,
@@ -1380,29 +1437,41 @@ class CanvasGraphics {
13801437
"fillCanvas",
13811438
drawnWidth,
13821439
drawnHeight,
1383-
true
1440+
/* trackTransform */ true
13841441
);
13851442
const fillCtx = fillCanvas.context;
1443+
13861444
// The offset will be the top-left cordinate mask.
1445+
// If objToCanvas is [a,b,c,d,e,f] then:
1446+
// - offsetX = min(a, c) + e
1447+
// - offsetY = min(b, d) + f
13871448
const offsetX = Math.min(cord1[0], cord2[0]);
13881449
const offsetY = Math.min(cord1[1], cord2[1]);
13891450
fillCtx.translate(-offsetX, -offsetY);
13901451
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-
);
1452+
1453+
if (!scaled) {
1454+
// Pre-scale if needed to improve image smoothing.
1455+
scaled = this._scaleImage(
1456+
maskCanvas.canvas,
1457+
fillCtx.mozCurrentTransformInverse
1458+
);
1459+
scaled = scaled.img;
1460+
if (cache && isPatternFill) {
1461+
cache.set(cacheKey, scaled);
1462+
}
1463+
}
1464+
13961465
fillCtx.imageSmoothingEnabled = getImageSmoothingEnabled(
13971466
fillCtx.mozCurrentTransform,
13981467
img.interpolate
13991468
);
14001469
fillCtx.drawImage(
1401-
scaled.img,
1470+
scaled,
14021471
0,
14031472
0,
1404-
scaled.img.width,
1405-
scaled.img.height,
1473+
scaled.width,
1474+
scaled.height,
14061475
0,
14071476
0,
14081477
width,
@@ -1424,6 +1493,13 @@ class CanvasGraphics {
14241493

14251494
fillCtx.fillRect(0, 0, width, height);
14261495

1496+
if (cache && !isPatternFill) {
1497+
// The fill canvas is put in the cache associated to the mask image
1498+
// so we must remove from the cached canvas: it mustn't be used again.
1499+
this.cachedCanvases.delete("fillCanvas");
1500+
cache.set(cacheKey, fillCanvas.canvas);
1501+
}
1502+
14271503
// Round the offsets to avoid drawing fractional pixels.
14281504
return {
14291505
canvas: fillCanvas.canvas,
@@ -1555,7 +1631,7 @@ class CanvasGraphics {
15551631
cacheId,
15561632
drawnWidth,
15571633
drawnHeight,
1558-
true
1634+
/* trackTransform */ true
15591635
);
15601636
this.suspendedCtx = this.ctx;
15611637
this.ctx = scratchCanvas.context;
@@ -2097,7 +2173,8 @@ class CanvasGraphics {
20972173
const { context: ctx } = this.cachedCanvases.getCanvas(
20982174
"isFontSubpixelAAEnabled",
20992175
10,
2100-
10
2176+
10,
2177+
/* trackTransform */ false
21012178
);
21022179
ctx.scale(1.5, 1);
21032180
ctx.fillText("I", 0, 10);
@@ -2606,7 +2683,7 @@ class CanvasGraphics {
26062683
cacheId,
26072684
drawnWidth,
26082685
drawnHeight,
2609-
true
2686+
/* trackTransform */ true
26102687
);
26112688
const groupCtx = scratchCanvas.context;
26122689

@@ -2767,7 +2844,9 @@ class CanvasGraphics {
27672844
return;
27682845
}
27692846

2847+
const count = img.count;
27702848
img = this.getObject(img.data, img);
2849+
img.count = count;
27712850

27722851
const ctx = this.ctx;
27732852
const width = img.width,
@@ -2853,7 +2932,8 @@ class CanvasGraphics {
28532932
const maskCanvas = this.cachedCanvases.getCanvas(
28542933
"maskCanvas",
28552934
width,
2856-
height
2935+
height,
2936+
/* trackTransform */ false
28572937
);
28582938
const maskCtx = maskCanvas.context;
28592939
maskCtx.save();
@@ -2944,7 +3024,8 @@ class CanvasGraphics {
29443024
const tmpCanvas = this.cachedCanvases.getCanvas(
29453025
"inlineImage",
29463026
width,
2947-
height
3027+
height,
3028+
/* trackTransform */ false
29483029
);
29493030
const tmpCtx = tmpCanvas.context;
29503031
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
@@ -2990,7 +3071,12 @@ class CanvasGraphics {
29903071
const w = imgData.width;
29913072
const h = imgData.height;
29923073

2993-
const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h);
3074+
const tmpCanvas = this.cachedCanvases.getCanvas(
3075+
"inlineImage",
3076+
w,
3077+
h,
3078+
/* trackTransform */ false
3079+
);
29943080
const tmpCtx = tmpCanvas.context;
29953081
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
29963082

0 commit comments

Comments
 (0)