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.
This commit is contained in:
Calixte Denizet 2022-04-13 15:44:33 +02:00
parent b73a6cc213
commit f62d961dfe
5 changed files with 151 additions and 29 deletions

View File

@ -675,6 +675,7 @@ class PartialEvaluator {
width: imgData.width, width: imgData.width,
height: imgData.height, height: imgData.height,
interpolate: imgData.interpolate, interpolate: imgData.interpolate,
count: 1,
}, },
]; ];
@ -1676,6 +1677,13 @@ class PartialEvaluator {
const localImage = localImageCache.getByName(name); const localImage = localImageCache.getByName(name);
if (localImage) { if (localImage) {
operatorList.addOp(localImage.fn, localImage.args); operatorList.addOp(localImage.fn, localImage.args);
if (
localImage.fn === OPS.paintImageMaskXObject &&
localImage.args[0] &&
localImage.args[0].count > 0
) {
localImage.args[0].count++;
}
args = null; args = null;
continue; continue;
} }
@ -1692,7 +1700,13 @@ class PartialEvaluator {
const localImage = localImageCache.getByRef(xobj); const localImage = localImageCache.getByRef(xobj);
if (localImage) { if (localImage) {
operatorList.addOp(localImage.fn, localImage.args); operatorList.addOp(localImage.fn, localImage.args);
if (
localImage.fn === OPS.paintImageMaskXObject &&
localImage.args[0] &&
localImage.args[0].count > 0
) {
localImage.args[0].count++;
}
resolveXObject(); resolveXObject();
return; return;
} }
@ -1809,6 +1823,13 @@ class PartialEvaluator {
const localImage = localImageCache.getByName(cacheKey); const localImage = localImageCache.getByName(cacheKey);
if (localImage) { if (localImage) {
operatorList.addOp(localImage.fn, localImage.args); operatorList.addOp(localImage.fn, localImage.args);
if (
localImage.fn === OPS.paintImageMaskXObject &&
localImage.args[0] &&
localImage.args[0].count > 0
) {
localImage.args[0].count++;
}
args = null; args = null;
continue; continue;
} }

View File

@ -256,6 +256,8 @@ addState(
data: maskParams.data, data: maskParams.data,
width: maskParams.width, width: maskParams.width,
height: maskParams.height, height: maskParams.height,
interpolate: maskParams.interpolate,
count: maskParams.count,
transform: transformArgs, transform: transformArgs,
}); });
} }

View File

@ -364,6 +364,10 @@ class CachedCanvases {
return canvasEntry; return canvasEntry;
} }
delete(id) {
delete this.cache[id];
}
clear() { clear() {
for (const id in this.cache) { for (const id in this.cache) {
const canvasEntry = this.cache[id]; const canvasEntry = this.cache[id];
@ -1121,6 +1125,7 @@ class CanvasGraphics {
} }
this._cachedScaleForStroking = null; this._cachedScaleForStroking = null;
this._cachedGetSinglePixelWidth = null; this._cachedGetSinglePixelWidth = null;
this._cachedBitmapsMap = new Map();
} }
getObject(data, fallback = null) { getObject(data, fallback = null) {
@ -1156,7 +1161,7 @@ class CanvasGraphics {
"transparent", "transparent",
width, width,
height, height,
true /* trackTransform */ true
); );
this.compositeCtx = this.ctx; this.compositeCtx = this.ctx;
this.transparentCanvas = transparentCanvas.canvas; this.transparentCanvas = transparentCanvas.canvas;
@ -1275,6 +1280,19 @@ class CanvasGraphics {
this.cachedCanvases.clear(); this.cachedCanvases.clear();
this.cachedPatterns.clear(); this.cachedPatterns.clear();
for (const cache of this._cachedBitmapsMap.values()) {
for (const canvas of cache.values()) {
if (
typeof HTMLCanvasElement !== "undefined" &&
canvas instanceof HTMLCanvasElement
) {
canvas.width = canvas.height = 0;
}
}
cache.clear();
}
this._cachedBitmapsMap.clear();
if (this.imageLayer) { if (this.imageLayer) {
this.imageLayer.endLayout(); this.imageLayer.endLayout();
} }
@ -1316,7 +1334,8 @@ class CanvasGraphics {
tmpCanvas = this.cachedCanvases.getCanvas( tmpCanvas = this.cachedCanvases.getCanvas(
tmpCanvasId, tmpCanvasId,
newWidth, newWidth,
newHeight newHeight,
/* trackTransform */ false
); );
tmpCtx = tmpCanvas.context; tmpCtx = tmpCanvas.context;
tmpCtx.clearRect(0, 0, newWidth, newHeight); tmpCtx.clearRect(0, 0, newWidth, newHeight);
@ -1345,24 +1364,65 @@ class CanvasGraphics {
_createMaskCanvas(img) { _createMaskCanvas(img) {
const ctx = this.ctx; const ctx = this.ctx;
const width = img.width, const { width, height } = img;
height = img.height;
const fillColor = this.current.fillColor; const fillColor = this.current.fillColor;
const isPatternFill = this.current.patternFill; const isPatternFill = this.current.patternFill;
const maskCanvas = this.cachedCanvases.getCanvas( const currentTransform = ctx.mozCurrentTransform;
"maskCanvas",
width, let cache, cacheKey, scaled, maskCanvas;
height if ((img.bitmap || img.data) && img.count > 1) {
); const mainKey = img.bitmap || img.data.buffer;
const maskCtx = maskCanvas.context; // We're reusing the same image several times, so we can cache it.
putBinaryImageMask(maskCtx, img); // In case we've a pattern fill we just keep the scaled version of
// the image.
// Only the scaling part matters, the translation part is just used
// to compute offsets.
// TODO: handle the case of a pattern fill if it's possible.
const withoutTranslation = currentTransform.slice(0, 4);
cacheKey = JSON.stringify(
isPatternFill ? withoutTranslation : [withoutTranslation, fillColor]
);
cache = this._cachedBitmapsMap.get(mainKey);
if (!cache) {
cache = new Map();
this._cachedBitmapsMap.set(mainKey, cache);
}
const cachedImage = cache.get(cacheKey);
if (cachedImage && !isPatternFill) {
const offsetX = Math.round(
Math.min(currentTransform[0], currentTransform[2]) +
currentTransform[4]
);
const offsetY = Math.round(
Math.min(currentTransform[1], currentTransform[3]) +
currentTransform[5]
);
return {
canvas: cachedImage,
offsetX,
offsetY,
};
}
scaled = cachedImage;
}
if (!scaled) {
maskCanvas = this.cachedCanvases.getCanvas(
"maskCanvas",
width,
height,
/* trackTransform */ false
);
putBinaryImageMask(maskCanvas.context, img);
}
// Create the mask canvas at the size it will be drawn at and also set // Create the mask canvas at the size it will be drawn at and also set
// its transform to match the current transform so if there are any // its transform to match the current transform so if there are any
// patterns applied they will be applied relative to the correct // patterns applied they will be applied relative to the correct
// transform. // transform.
const objToCanvas = ctx.mozCurrentTransform;
let maskToCanvas = Util.transform(objToCanvas, [ let maskToCanvas = Util.transform(currentTransform, [
1 / width, 1 / width,
0, 0,
0, 0,
@ -1380,29 +1440,41 @@ class CanvasGraphics {
"fillCanvas", "fillCanvas",
drawnWidth, drawnWidth,
drawnHeight, drawnHeight,
true /* trackTransform */ true
); );
const fillCtx = fillCanvas.context; const fillCtx = fillCanvas.context;
// The offset will be the top-left cordinate mask. // The offset will be the top-left cordinate mask.
// If objToCanvas is [a,b,c,d,e,f] then:
// - offsetX = min(a, c) + e
// - offsetY = min(b, d) + f
const offsetX = Math.min(cord1[0], cord2[0]); const offsetX = Math.min(cord1[0], cord2[0]);
const offsetY = Math.min(cord1[1], cord2[1]); const offsetY = Math.min(cord1[1], cord2[1]);
fillCtx.translate(-offsetX, -offsetY); fillCtx.translate(-offsetX, -offsetY);
fillCtx.transform.apply(fillCtx, maskToCanvas); fillCtx.transform.apply(fillCtx, maskToCanvas);
// Pre-scale if needed to improve image smoothing.
const scaled = this._scaleImage( if (!scaled) {
maskCanvas.canvas, // Pre-scale if needed to improve image smoothing.
fillCtx.mozCurrentTransformInverse scaled = this._scaleImage(
); maskCanvas.canvas,
fillCtx.mozCurrentTransformInverse
);
scaled = scaled.img;
if (cache && isPatternFill) {
cache.set(cacheKey, scaled);
}
}
fillCtx.imageSmoothingEnabled = getImageSmoothingEnabled( fillCtx.imageSmoothingEnabled = getImageSmoothingEnabled(
fillCtx.mozCurrentTransform, fillCtx.mozCurrentTransform,
img.interpolate img.interpolate
); );
fillCtx.drawImage( fillCtx.drawImage(
scaled.img, scaled,
0, 0,
0, 0,
scaled.img.width, scaled.width,
scaled.img.height, scaled.height,
0, 0,
0, 0,
width, width,
@ -1424,6 +1496,13 @@ class CanvasGraphics {
fillCtx.fillRect(0, 0, width, height); fillCtx.fillRect(0, 0, width, height);
if (cache && !isPatternFill) {
// The fill canvas is put in the cache associated to the mask image
// so we must remove from the cached canvas: it mustn't be used again.
this.cachedCanvases.delete("fillCanvas");
cache.set(cacheKey, fillCanvas.canvas);
}
// Round the offsets to avoid drawing fractional pixels. // Round the offsets to avoid drawing fractional pixels.
return { return {
canvas: fillCanvas.canvas, canvas: fillCanvas.canvas,
@ -1555,7 +1634,7 @@ class CanvasGraphics {
cacheId, cacheId,
drawnWidth, drawnWidth,
drawnHeight, drawnHeight,
true /* trackTransform */ true
); );
this.suspendedCtx = this.ctx; this.suspendedCtx = this.ctx;
this.ctx = scratchCanvas.context; this.ctx = scratchCanvas.context;
@ -2097,7 +2176,8 @@ class CanvasGraphics {
const { context: ctx } = this.cachedCanvases.getCanvas( const { context: ctx } = this.cachedCanvases.getCanvas(
"isFontSubpixelAAEnabled", "isFontSubpixelAAEnabled",
10, 10,
10 10,
/* trackTransform */ false
); );
ctx.scale(1.5, 1); ctx.scale(1.5, 1);
ctx.fillText("I", 0, 10); ctx.fillText("I", 0, 10);
@ -2606,7 +2686,7 @@ class CanvasGraphics {
cacheId, cacheId,
drawnWidth, drawnWidth,
drawnHeight, drawnHeight,
true /* trackTransform */ true
); );
const groupCtx = scratchCanvas.context; const groupCtx = scratchCanvas.context;
@ -2768,7 +2848,9 @@ class CanvasGraphics {
return; return;
} }
const count = img.count;
img = this.getObject(img.data, img); img = this.getObject(img.data, img);
img.count = count;
const ctx = this.ctx; const ctx = this.ctx;
const width = img.width, const width = img.width,
@ -2854,7 +2936,8 @@ class CanvasGraphics {
const maskCanvas = this.cachedCanvases.getCanvas( const maskCanvas = this.cachedCanvases.getCanvas(
"maskCanvas", "maskCanvas",
width, width,
height height,
/* trackTransform */ false
); );
const maskCtx = maskCanvas.context; const maskCtx = maskCanvas.context;
maskCtx.save(); maskCtx.save();
@ -2945,7 +3028,8 @@ class CanvasGraphics {
const tmpCanvas = this.cachedCanvases.getCanvas( const tmpCanvas = this.cachedCanvases.getCanvas(
"inlineImage", "inlineImage",
width, width,
height height,
/* trackTransform */ false
); );
const tmpCtx = tmpCanvas.context; const tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
@ -2991,7 +3075,12 @@ class CanvasGraphics {
const w = imgData.width; const w = imgData.width;
const h = imgData.height; const h = imgData.height;
const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h); const tmpCanvas = this.cachedCanvases.getCanvas(
"inlineImage",
w,
h,
/* trackTransform */ false
);
const tmpCtx = tmpCanvas.context; const tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);

View File

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

View File

@ -6354,5 +6354,13 @@
"value": "Hello PDF.js World" "value": "Hello PDF.js World"
} }
} }
},
{ "id": "bug857031",
"file": "pdfs/bug857031.pdf",
"md5": "f11ecd7f75675e0cafbc9880c1a586c7",
"rounds": 1,
"link": true,
"lastPage": 1,
"type": "eq"
} }
] ]