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:
parent
b73a6cc213
commit
f62d961dfe
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
2
test/pdfs/bug857031.pdf.link
Normal file
2
test/pdfs/bug857031.pdf.link
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
https://bug857031.bmoattachments.org/attachment.cgi?id=732270
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user