Merge pull request #20966 from calixteman/rm_canvas_cache

Remove the canvases cache
This commit is contained in:
calixteman 2026-03-25 07:50:55 +01:00 committed by GitHub
commit ae84c662b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 187 additions and 165 deletions

View File

@ -204,36 +204,6 @@ function mirrorContextOperations(ctx, destCtx) {
};
}
class CachedCanvases {
#cache = new Map();
constructor(canvasFactory) {
this.canvasFactory = canvasFactory;
}
getCanvas(id, width, height) {
let canvasEntry = this.#cache.get(id);
if (canvasEntry) {
this.canvasFactory.reset(canvasEntry, width, height);
} else {
canvasEntry = this.canvasFactory.create(width, height);
this.#cache.set(id, canvasEntry);
}
return canvasEntry;
}
delete(id) {
this.#cache.delete(id);
}
clear() {
for (const canvasEntry of this.#cache.values()) {
this.canvasFactory.destroy(canvasEntry);
}
this.#cache.clear();
}
}
function drawImageAtIntegerCoords(
ctx,
srcImg,
@ -681,11 +651,11 @@ class CanvasGraphics {
this.smaskStack = [];
this.smaskCounter = 0;
this.tempSMask = null;
this.smaskGroupCanvases = [];
this.suspendedCtx = null;
this.contentVisible = true;
this.markedContentStack = markedContentStack || [];
this.optionalContentConfig = optionalContentConfig;
this.cachedCanvases = new CachedCanvases(this.canvasFactory);
this.cachedPatterns = new Map();
this.annotationCanvasMap = annotationCanvasMap;
this.viewportScale = 1;
@ -731,14 +701,11 @@ class CanvasGraphics {
this.ctx.fillStyle = savedFillStyle;
if (transparency) {
const transparentCanvas = this.cachedCanvases.getCanvas(
"transparent",
width,
height
);
const transparentCanvas = (this.transparentCanvasEntry =
this.canvasFactory.create(width, height));
this.compositeCtx = this.ctx;
this.transparentCanvas = transparentCanvas.canvas;
this.ctx = transparentCanvas.context;
({ canvas: this.transparentCanvas, context: this.ctx } =
transparentCanvas);
this.ctx.save();
// The transform can be applied before rendering, transferring it to
// the new canvas.
@ -862,14 +829,26 @@ class CanvasGraphics {
this.ctx.setTransform(1, 0, 0, 1, 0, 0); // Avoid apply transform twice
this.ctx.drawImage(this.transparentCanvas, 0, 0);
this.ctx.restore();
this.canvasFactory.destroy(this.transparentCanvasEntry);
this.transparentCanvas = null;
this.transparentCanvasEntry = null;
}
}
endDrawing() {
this.#restoreInitialState();
this.cachedCanvases.clear();
// Destroy all smask group canvases now that rendering is complete.
// These cannot be destroyed eagerly because activeSMask is part of
// CanvasExtraState and is shared (via Object.create prototype chain) across
// save/restore state copies.
for (const canvas of this.smaskGroupCanvases) {
this.canvasFactory.destroy(canvas);
}
this.smaskGroupCanvases.length = 0;
this.tempSMask = null;
this.smaskStack.length = 0;
this.cachedPatterns.clear();
for (const cache of this._cachedBitmapsMap.values()) {
@ -910,52 +889,80 @@ class CanvasGraphics {
// displayWidth and displayHeight are used for VideoFrame.
const width = img.width ?? img.displayWidth;
const height = img.height ?? img.displayHeight;
let widthScale = Math.max(
const widthScale = Math.max(
Math.hypot(inverseTransform[0], inverseTransform[1]),
1
);
let heightScale = Math.max(
const heightScale = Math.max(
Math.hypot(inverseTransform[2], inverseTransform[3]),
1
);
let paintWidth = width,
paintHeight = height;
let tmpCanvasId = "prescale1";
let tmpCanvas, tmpCtx;
while (
(widthScale > 2 && paintWidth > 1) ||
(heightScale > 2 && paintHeight > 1)
) {
let newWidth = paintWidth,
newHeight = paintHeight;
if (widthScale > 2 && paintWidth > 1) {
// Pre-compute each step's output dimensions.
const scaleSteps = [];
let ws = widthScale,
hs = heightScale,
pw = width,
ph = height;
while ((ws > 2 && pw > 1) || (hs > 2 && ph > 1)) {
let nw = pw,
nh = ph;
if (ws > 2 && pw > 1) {
// See bug 1820511 (Windows specific bug).
// TODO: once the above bug is fixed we could revert to:
// newWidth = Math.ceil(paintWidth / 2);
newWidth =
paintWidth >= 16384
? Math.floor(paintWidth / 2) - 1 || 1
: Math.ceil(paintWidth / 2);
widthScale /= paintWidth / newWidth;
// nw = Math.ceil(pw / 2);
nw = pw >= 16384 ? Math.floor(pw / 2) - 1 || 1 : Math.ceil(pw / 2);
ws /= pw / nw;
}
if (heightScale > 2 && paintHeight > 1) {
if (hs > 2 && ph > 1) {
// TODO: see the comment above.
newHeight =
paintHeight >= 16384
? Math.floor(paintHeight / 2) - 1 || 1
: Math.ceil(paintHeight) / 2;
heightScale /= paintHeight / newHeight;
nh = ph >= 16384 ? Math.floor(ph / 2) - 1 || 1 : Math.ceil(ph) / 2;
hs /= ph / nh;
}
tmpCanvas = this.cachedCanvases.getCanvas(
tmpCanvasId,
scaleSteps.push({ newWidth: nw, newHeight: nh });
pw = nw;
ph = nh;
}
if (scaleSteps.length === 0) {
return { img, paintWidth: width, paintHeight: height, tmpCanvas: null };
}
if (scaleSteps.length === 1) {
const { newWidth, newHeight } = scaleSteps[0];
const tmpCanvas = this.canvasFactory.create(newWidth, newHeight);
tmpCanvas.context.drawImage(
img,
0,
0,
width,
height,
0,
0,
newWidth,
newHeight
);
tmpCtx = tmpCanvas.context;
tmpCtx.clearRect(0, 0, newWidth, newHeight);
tmpCtx.drawImage(
img,
return {
img: tmpCanvas.canvas,
paintWidth: newWidth,
paintHeight: newHeight,
tmpCanvas,
};
}
// More than 2 steps: ping-pong between two reused canvas entries.
// canvasFactory.reset() resizes (and implicitly clears) a canvas without
// creating a new JS object or calling getContext() again.
let readEntry = this.canvasFactory.create(1, 1);
let writeEntry = this.canvasFactory.create(1, 1);
let paintWidth = width,
paintHeight = height;
let source = img;
for (const { newWidth, newHeight } of scaleSteps) {
this.canvasFactory.reset(writeEntry, newWidth, newHeight);
writeEntry.context.drawImage(
source,
0,
0,
paintWidth,
@ -965,15 +972,19 @@ class CanvasGraphics {
newWidth,
newHeight
);
img = tmpCanvas.canvas;
[readEntry, writeEntry] = [writeEntry, readEntry];
source = readEntry.canvas;
paintWidth = newWidth;
paintHeight = newHeight;
tmpCanvasId = tmpCanvasId === "prescale1" ? "prescale2" : "prescale1";
}
// writeEntry is now the stale buffer — destroy it.
this.canvasFactory.destroy(writeEntry);
return {
img,
img: readEntry.canvas,
paintWidth,
paintHeight,
tmpCanvas: readEntry,
};
}
@ -1025,7 +1036,7 @@ class CanvasGraphics {
}
if (!scaled) {
maskCanvas = this.cachedCanvases.getCanvas("maskCanvas", width, height);
maskCanvas = this.canvasFactory.create(width, height);
putBinaryImageMask(maskCanvas.context, img);
}
@ -1048,11 +1059,7 @@ class CanvasGraphics {
const [minX, minY, maxX, maxY] = minMax;
const drawnWidth = Math.round(maxX - minX) || 1;
const drawnHeight = Math.round(maxY - minY) || 1;
const fillCanvas = this.cachedCanvases.getCanvas(
"fillCanvas",
drawnWidth,
drawnHeight
);
const fillCanvas = this.canvasFactory.create(drawnWidth, drawnHeight);
const fillCtx = fillCanvas.context;
// The offset will be the top-left coordinate mask.
@ -1064,15 +1071,24 @@ class CanvasGraphics {
fillCtx.translate(-offsetX, -offsetY);
fillCtx.transform(...maskToCanvas);
let scaledEntry = null;
if (!scaled) {
// Pre-scale if needed to improve image smoothing.
scaled = this._scaleImage(
const scaleResult = this._scaleImage(
maskCanvas.canvas,
getCurrentTransformInverse(fillCtx)
);
scaled = scaled.img;
scaled = scaleResult.img;
scaledEntry = scaleResult.tmpCanvas;
if (scaled !== maskCanvas.canvas) {
// _scaleImage created a new canvas; maskCanvas is no longer needed.
this.canvasFactory.destroy(maskCanvas);
maskCanvas = null;
}
if (cache && isPatternFill) {
cache.set(cacheKey, scaled);
scaledEntry = null; // bitmap cache owns the canvas now
maskCanvas = null; // bitmap cache may own maskCanvas.canvas (= scaled)
}
}
@ -1093,6 +1109,13 @@ class CanvasGraphics {
width,
height
);
if (scaledEntry) {
this.canvasFactory.destroy(scaledEntry);
}
if (maskCanvas) {
// scaled === maskCanvas.canvas and not owned by the bitmap cache.
this.canvasFactory.destroy(maskCanvas);
}
fillCtx.globalCompositeOperation = "source-in";
const inverse = Util.transform(getCurrentTransformInverse(fillCtx), [
@ -1111,8 +1134,7 @@ class CanvasGraphics {
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");
// so it mustn't be used again.
cache.set(cacheKey, fillCanvas.canvas);
}
@ -1124,6 +1146,8 @@ class CanvasGraphics {
// Round the offsets to avoid drawing fractional pixels.
return {
canvas: fillCanvas.canvas,
// canvasEntry is null when the bitmap cache owns the canvas.
canvasEntry: cache && !isPatternFill ? null : fillCanvas,
offsetX: Math.round(offsetX),
offsetY: Math.round(offsetY),
};
@ -1257,12 +1281,8 @@ class CanvasGraphics {
}
const drawnWidth = this.ctx.canvas.width;
const drawnHeight = this.ctx.canvas.height;
const cacheId = "smaskGroupAt" + this.groupLevel;
const scratchCanvas = this.cachedCanvases.getCanvas(
cacheId,
drawnWidth,
drawnHeight
);
const scratchCanvas = this.canvasFactory.create(drawnWidth, drawnHeight);
this.smaskScratchCanvas = scratchCanvas;
this.suspendedCtx = this.ctx;
const ctx = (this.ctx = scratchCanvas.context);
ctx.setTransform(this.suspendedCtx.getTransform());
@ -1283,6 +1303,8 @@ class CanvasGraphics {
this.ctx = this.suspendedCtx;
this.suspendedCtx = null;
this.canvasFactory.destroy(this.smaskScratchCanvas);
this.smaskScratchCanvas = null;
}
compose(dirtyBox) {
@ -1356,6 +1378,7 @@ class CanvasGraphics {
let maskX = layerOffsetX - maskOffsetX;
let maskY = layerOffsetY - maskOffsetY;
let maskExtensionEntry = null;
if (backdrop) {
if (
maskX < 0 ||
@ -1363,19 +1386,15 @@ class CanvasGraphics {
maskX + width > maskCanvas.width ||
maskY + height > maskCanvas.height
) {
const canvas = this.cachedCanvases.getCanvas(
"maskExtension",
width,
height
);
const ctx = canvas.context;
maskExtensionEntry = this.canvasFactory.create(width, height);
const ctx = maskExtensionEntry.context;
ctx.drawImage(maskCanvas, -maskX, -maskY);
ctx.globalCompositeOperation = "destination-atop";
ctx.fillStyle = backdrop;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = "source-over";
maskCanvas = canvas.canvas;
maskCanvas = maskExtensionEntry.canvas;
maskX = maskY = 0;
} else {
maskCtx.save();
@ -1417,6 +1436,9 @@ class CanvasGraphics {
height
);
layerCtx.restore();
if (maskExtensionEntry) {
this.canvasFactory.destroy(maskExtensionEntry);
}
}
save(opIdx) {
@ -1992,14 +2014,12 @@ class CanvasGraphics {
get isFontSubpixelAAEnabled() {
// Checks if anti-aliasing is enabled when scaled text is painted.
// On Windows GDI scaled fonts looks bad.
const { context: ctx } = this.cachedCanvases.getCanvas(
"isFontSubpixelAAEnabled",
10,
10
);
const tmpCanvas = this.canvasFactory.create(10, 10);
const ctx = tmpCanvas.context;
ctx.scale(1.5, 1);
ctx.fillText("I", 0, 10);
const data = ctx.getImageData(0, 0, 10, 10).data;
this.canvasFactory.destroy(tmpCanvas);
let enabled = false;
for (let i = 3; i < data.length; i += 4) {
if (data[i] > 0 && data[i] < 255) {
@ -2610,16 +2630,13 @@ class CanvasGraphics {
this.current.startNewPathAndClipBox([0, 0, drawnWidth, drawnHeight]);
let cacheId = "groupAt" + this.groupLevel;
if (group.smask) {
// Using two cache entries is case if masks are used one after another.
cacheId += "_smask_" + (this.smaskCounter++ % 2);
this.smaskCounter++;
}
const scratchCanvas = this.canvasFactory.create(drawnWidth, drawnHeight);
if (group.smask) {
this.smaskGroupCanvases.push(scratchCanvas);
}
const scratchCanvas = this.cachedCanvases.getCanvas(
cacheId,
drawnWidth,
drawnHeight
);
const groupCtx = scratchCanvas.context;
// Since we created a new canvas that is just the size of the bounding box
@ -2721,6 +2738,10 @@ class CanvasGraphics {
);
this.ctx.drawImage(groupCtx.canvas, 0, 0);
this.ctx.restore();
this.canvasFactory.destroy({
canvas: groupCtx.canvas,
context: groupCtx,
});
this.compose(dirtyBox);
}
}
@ -2837,6 +2858,9 @@ class CanvasGraphics {
)
.recordOperation(opIdx);
ctx.restore();
if (mask.canvasEntry) {
this.canvasFactory.destroy(mask.canvasEntry);
}
this.compose();
}
@ -2893,6 +2917,9 @@ class CanvasGraphics {
);
}
ctx.restore();
if (mask.canvasEntry) {
this.canvasFactory.destroy(mask.canvasEntry);
}
this.compose();
this.dependencyTracker?.recordOperation(opIdx);
@ -2914,11 +2941,7 @@ class CanvasGraphics {
for (const image of images) {
const { data, width, height, transform } = image;
const maskCanvas = this.cachedCanvases.getCanvas(
"maskCanvas",
width,
height
);
const maskCanvas = this.canvasFactory.create(width, height);
const maskCtx = maskCanvas.context;
maskCtx.save();
@ -2955,6 +2978,7 @@ class CanvasGraphics {
1,
1
);
this.canvasFactory.destroy(maskCanvas);
this.dependencyTracker?.recordBBox(opIdx, ctx, 0, width, 0, height);
ctx.restore();
@ -3012,20 +3036,16 @@ class CanvasGraphics {
applyTransferMapsToBitmap(imgData) {
if (this.current.transferMaps === "none") {
return imgData.bitmap;
return { img: imgData.bitmap, canvasEntry: null };
}
const { bitmap, width, height } = imgData;
const tmpCanvas = this.cachedCanvases.getCanvas(
"inlineImage",
width,
height
);
const tmpCanvas = this.canvasFactory.create(width, height);
const tmpCtx = tmpCanvas.context;
tmpCtx.filter = this.current.transferMaps;
tmpCtx.drawImage(bitmap, 0, 0);
tmpCtx.filter = "none";
return tmpCanvas.canvas;
return { img: tmpCanvas.canvas, canvasEntry: tmpCanvas };
}
paintInlineImageXObject(opIdx, imgData) {
@ -3051,8 +3071,11 @@ class CanvasGraphics {
ctx.scale(1 / width, -1 / height);
let imgToPaint;
let inlineImgCanvas = null;
if (imgData.bitmap) {
imgToPaint = this.applyTransferMapsToBitmap(imgData);
const result = this.applyTransferMapsToBitmap(imgData);
imgToPaint = result.img;
inlineImgCanvas = result.canvasEntry;
} else if (
(typeof HTMLElement === "function" && imgData instanceof HTMLElement) ||
!imgData.data
@ -3060,14 +3083,10 @@ class CanvasGraphics {
// typeof check is needed due to node.js support, see issue #8489
imgToPaint = imgData;
} else {
const tmpCanvas = this.cachedCanvases.getCanvas(
"inlineImage",
width,
height
);
const tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData);
imgToPaint = this.applyTransferMapsToCanvas(tmpCtx);
const tmpCanvas = this.canvasFactory.create(width, height);
putBinaryImageData(tmpCanvas.context, imgData);
imgToPaint = this.applyTransferMapsToCanvas(tmpCanvas.context);
inlineImgCanvas = tmpCanvas;
}
const scaled = this._scaleImage(
@ -3105,6 +3124,12 @@ class CanvasGraphics {
width,
height
);
if (scaled.tmpCanvas) {
this.canvasFactory.destroy(scaled.tmpCanvas);
}
if (inlineImgCanvas) {
this.canvasFactory.destroy(inlineImgCanvas);
}
this.compose();
this.restore(opIdx);
}
@ -3115,16 +3140,17 @@ class CanvasGraphics {
}
const ctx = this.ctx;
let imgToPaint;
let inlineImgCanvas = null;
if (imgData.bitmap) {
imgToPaint = imgData.bitmap;
} else {
const w = imgData.width;
const h = imgData.height;
const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h);
const tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData);
imgToPaint = this.applyTransferMapsToCanvas(tmpCtx);
const tmpCanvas = this.canvasFactory.create(w, h);
putBinaryImageData(tmpCanvas.context, imgData);
imgToPaint = this.applyTransferMapsToCanvas(tmpCanvas.context);
inlineImgCanvas = tmpCanvas;
}
this.dependencyTracker?.resetBBox(opIdx);
@ -3148,6 +3174,9 @@ class CanvasGraphics {
this.dependencyTracker?.recordBBox(opIdx, ctx, 0, 1, -1, 0);
ctx.restore();
}
if (inlineImgCanvas) {
this.canvasFactory.destroy(inlineImgCanvas);
}
this.dependencyTracker?.recordOperation(opIdx);
this.compose();
}

View File

@ -41,25 +41,25 @@ class BaseCanvasFactory {
};
}
reset(canvasAndContext, width, height) {
if (!canvasAndContext.canvas) {
reset({ canvas }, width, height) {
if (!canvas) {
throw new Error("Canvas is not specified");
}
if (width <= 0 || height <= 0) {
throw new Error("Invalid canvas size");
}
canvasAndContext.canvas.width = width;
canvasAndContext.canvas.height = height;
canvas.width = width;
canvas.height = height;
}
destroy(canvasAndContext) {
if (!canvasAndContext.canvas) {
const { canvas } = canvasAndContext;
if (!canvas) {
throw new Error("Canvas is not specified");
}
// Zeroing the width and height cause Firefox to release graphics
// resources immediately, which can greatly reduce memory consumption.
canvasAndContext.canvas.width = 0;
canvasAndContext.canvas.height = 0;
canvas.width = canvas.height = 0;
canvasAndContext.canvas = null;
canvasAndContext.context = null;
}

View File

@ -217,11 +217,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
const width = Math.ceil(ownerBBox[2] - ownerBBox[0]) || 1;
const height = Math.ceil(ownerBBox[3] - ownerBBox[1]) || 1;
const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern",
width,
height
);
const tmpCanvas = owner.canvasFactory.create(width, height);
const tmpCtx = tmpCanvas.context;
tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
@ -254,6 +250,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
tmpCtx.fill();
pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
owner.canvasFactory.destroy(tmpCanvas);
const domMatrix = new DOMMatrix(inverse);
pattern.setTransform(domMatrix);
} else {
@ -448,7 +445,7 @@ class MeshShadingPattern extends BaseShadingPattern {
this.matrix = null;
}
_createMeshCanvas(combinedScale, backgroundColor, cachedCanvases) {
_createMeshCanvas(combinedScale, backgroundColor, canvasFactory) {
// we will increase scale on some weird factor to let antialiasing take
// care of "rough" edges
const EXPECTED_SCALE = 1.1;
@ -489,12 +486,7 @@ class MeshShadingPattern extends BaseShadingPattern {
const paddedWidth = width + BORDER_SIZE * 2;
const paddedHeight = height + BORDER_SIZE * 2;
const tmpCanvas = cachedCanvases.getCanvas(
"mesh",
paddedWidth,
paddedHeight
);
const tmpCanvas = canvasFactory.create(paddedWidth, paddedHeight);
if (isWebGPUMeshReady()) {
tmpCanvas.context.drawImage(
@ -560,7 +552,7 @@ class MeshShadingPattern extends BaseShadingPattern {
const temporaryPatternCanvas = this._createMeshCanvas(
scale,
pathType === PathType.SHADING ? null : this._background,
owner.cachedCanvases
owner.canvasFactory
);
if (pathType !== PathType.SHADING) {
@ -576,7 +568,12 @@ class MeshShadingPattern extends BaseShadingPattern {
);
ctx.scale(temporaryPatternCanvas.scaleX, temporaryPatternCanvas.scaleY);
return ctx.createPattern(temporaryPatternCanvas.canvas, "no-repeat");
const pattern = ctx.createPattern(
temporaryPatternCanvas.canvas,
"no-repeat"
);
owner.canvasFactory.destroy(temporaryPatternCanvas);
return pattern;
}
}
@ -704,11 +701,7 @@ class TilingPattern {
combinedScaleY
);
const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern",
dimx.size,
dimy.size
);
const tmpCanvas = owner.canvasFactory.create(dimx.size, dimy.size);
const tmpCtx = tmpCanvas.context;
const graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx, opIdx);
graphics.groupLevel = owner.groupLevel;
@ -775,11 +768,7 @@ class TilingPattern {
const xSize = dimx2.size;
const ySize = dimy2.size;
const tmpCanvas2 = owner.cachedCanvases.getCanvas(
"pattern-workaround",
xSize,
ySize
);
const tmpCanvas2 = owner.canvasFactory.create(xSize, ySize);
const tmpCtx2 = tmpCanvas2.context;
const ii = redrawHorizontally ? Math.floor(width / xstep) : 0;
const jj = redrawVertically ? Math.floor(height / ystep) : 0;
@ -800,8 +789,10 @@ class TilingPattern {
);
}
}
owner.canvasFactory.destroy(tmpCanvas);
return {
canvas: tmpCanvas2.canvas,
canvasEntry: tmpCanvas2,
scaleX: dimx2.scale,
scaleY: dimy2.scale,
offsetX: x0,
@ -811,6 +802,7 @@ class TilingPattern {
return {
canvas: tmpCanvas.canvas,
canvasEntry: tmpCanvas,
scaleX: dimx.scale,
scaleY: dimy.scale,
offsetX: x0,
@ -894,6 +886,7 @@ class TilingPattern {
);
const pattern = ctx.createPattern(temporaryPatternCanvas.canvas, "repeat");
owner.canvasFactory.destroy(temporaryPatternCanvas.canvasEntry);
pattern.setTransform(domMatrix);
return pattern;