Create shading patterns the size of the current path. (bug 1722807)

Previously, when we created a shading pattern canvas we created it
as the same size as the page. This was good for caching if the same
pattern was used over and over again, but when lots of different
shadings are created that caused us to create many full page
canvases.

Instead of creating the full page canvses, create the canvas
as the same size as the current path bounding box. This reduces memory
consumption by a lot since most paths are pretty small. Also, in real world
PDFs it's rare for a shading (non shading fill) to be reused over and over again.
Bug 1721949 is an example where the same pattern is reused and it will be slightly
slower than before.
This commit is contained in:
Brendan Dahl 2021-11-03 14:21:15 -07:00
parent 3b5a463357
commit b56cca0324
2 changed files with 101 additions and 103 deletions

View File

@ -27,7 +27,11 @@ import {
Util, Util,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { getShadingPattern, TilingPattern } from "./pattern_helper.js"; import {
getShadingPattern,
PathType,
TilingPattern,
} from "./pattern_helper.js";
import { PixelsPerInch } from "./display_utils.js"; import { PixelsPerInch } from "./display_utils.js";
// <canvas> contexts store most of the state we need natively. // <canvas> contexts store most of the state we need natively.
@ -38,10 +42,6 @@ const MIN_FONT_SIZE = 16;
const MAX_FONT_SIZE = 100; const MAX_FONT_SIZE = 100;
const MAX_GROUP_SIZE = 4096; const MAX_GROUP_SIZE = 4096;
// This value comes from sampling a few PDFs that re-use patterns, there doesn't
// seem to be any that benefit from caching more than 2 patterns.
const MAX_CACHED_CANVAS_PATTERNS = 2;
// Defines the time the `executeOperatorList`-method is going to be executing // Defines the time the `executeOperatorList`-method is going to be executing
// before it stops and shedules a continue of execution. // before it stops and shedules a continue of execution.
const EXECUTION_TIME = 15; // ms const EXECUTION_TIME = 15; // ms
@ -366,46 +366,6 @@ class CachedCanvases {
} }
} }
/**
* Least recently used cache implemented with a JS Map. JS Map keys are ordered
* by last insertion.
*/
class LRUCache {
constructor(maxSize = 0) {
this._cache = new Map();
this._maxSize = maxSize;
}
has(key) {
return this._cache.has(key);
}
get(key) {
if (this._cache.has(key)) {
// Delete and set the value so it's moved to the end of the map iteration.
const value = this._cache.get(key);
this._cache.delete(key);
this._cache.set(key, value);
}
return this._cache.get(key);
}
set(key, value) {
if (this._maxSize <= 0) {
return;
}
if (this._cache.size + 1 > this._maxSize) {
// Delete the least recently used.
this._cache.delete(this._cache.keys().next().value);
}
this._cache.set(key, value);
}
clear() {
this._cache.clear();
}
}
function compileType3Glyph(imgData) { function compileType3Glyph(imgData) {
const POINT_TO_PROCESS_LIMIT = 1000; const POINT_TO_PROCESS_LIMIT = 1000;
const POINT_TYPES = new Uint8Array([ const POINT_TYPES = new Uint8Array([
@ -639,8 +599,23 @@ class CanvasExtraState {
this.updatePathMinMax(transform, box[2], box[3]); this.updatePathMinMax(transform, box[2], box[3]);
} }
getPathBoundingBox() { getPathBoundingBox(pathType = PathType.FILL, transform = null) {
return [this.minX, this.minY, this.maxX, this.maxY]; const box = [this.minX, this.minY, this.maxX, this.maxY];
if (pathType === PathType.STROKE) {
if (!transform) {
unreachable("Stroke bounding box must include transform.");
}
// Stroked paths can be outside of the path bounding box by 1/2 the line
// width.
const scale = Util.singularValueDecompose2dScale(transform);
const xStrokePad = (scale[0] * this.lineWidth) / 2;
const yStrokePad = (scale[1] * this.lineWidth) / 2;
box[0] -= xStrokePad;
box[1] -= yStrokePad;
box[2] += xStrokePad;
box[3] += yStrokePad;
}
return box;
} }
updateClipFromPath() { updateClipFromPath() {
@ -656,8 +631,11 @@ class CanvasExtraState {
this.maxY = 0; this.maxY = 0;
} }
getClippedPathBoundingBox() { getClippedPathBoundingBox(pathType = PathType.FILL, transform = null) {
return Util.intersect(this.clipBox, this.getPathBoundingBox()); return Util.intersect(
this.clipBox,
this.getPathBoundingBox(pathType, transform)
);
} }
} }
@ -1121,7 +1099,6 @@ class CanvasGraphics {
this.markedContentStack = []; this.markedContentStack = [];
this.optionalContentConfig = optionalContentConfig; this.optionalContentConfig = optionalContentConfig;
this.cachedCanvases = new CachedCanvases(this.canvasFactory); this.cachedCanvases = new CachedCanvases(this.canvasFactory);
this.cachedCanvasPatterns = new LRUCache(MAX_CACHED_CANVAS_PATTERNS);
this.cachedPatterns = new Map(); this.cachedPatterns = new Map();
if (canvasCtx) { if (canvasCtx) {
// NOTE: if mozCurrentTransform is polyfilled, then the current state of // NOTE: if mozCurrentTransform is polyfilled, then the current state of
@ -1273,7 +1250,6 @@ class CanvasGraphics {
} }
this.cachedCanvases.clear(); this.cachedCanvases.clear();
this.cachedCanvasPatterns.clear();
this.cachedPatterns.clear(); this.cachedPatterns.clear();
if (this.imageLayer) { if (this.imageLayer) {
@ -1420,7 +1396,7 @@ class CanvasGraphics {
-offsetY, -offsetY,
]); ]);
fillCtx.fillStyle = isPatternFill fillCtx.fillStyle = isPatternFill
? fillColor.getPattern(ctx, this, inverse, false) ? fillColor.getPattern(ctx, this, inverse, PathType.FILL)
: fillColor; : fillColor;
fillCtx.fillRect(0, 0, width, height); fillCtx.fillRect(0, 0, width, height);
@ -1772,7 +1748,8 @@ class CanvasGraphics {
ctx.strokeStyle = strokeColor.getPattern( ctx.strokeStyle = strokeColor.getPattern(
ctx, ctx,
this, this,
ctx.mozCurrentTransformInverse ctx.mozCurrentTransformInverse,
PathType.STROKE
); );
// Prevent drawing too thin lines by enforcing a minimum line width. // Prevent drawing too thin lines by enforcing a minimum line width.
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth); ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth);
@ -1819,7 +1796,8 @@ class CanvasGraphics {
ctx.fillStyle = fillColor.getPattern( ctx.fillStyle = fillColor.getPattern(
ctx, ctx,
this, this,
ctx.mozCurrentTransformInverse ctx.mozCurrentTransformInverse,
PathType.FILL
); );
needRestore = true; needRestore = true;
} }
@ -2161,7 +2139,8 @@ class CanvasGraphics {
const pattern = current.fillColor.getPattern( const pattern = current.fillColor.getPattern(
ctx, ctx,
this, this,
ctx.mozCurrentTransformInverse ctx.mozCurrentTransformInverse,
PathType.FILL
); );
patternTransform = ctx.mozCurrentTransform; patternTransform = ctx.mozCurrentTransform;
ctx.restore(); ctx.restore();
@ -2426,10 +2405,7 @@ class CanvasGraphics {
if (this.cachedPatterns.has(objId)) { if (this.cachedPatterns.has(objId)) {
pattern = this.cachedPatterns.get(objId); pattern = this.cachedPatterns.get(objId);
} else { } else {
pattern = getShadingPattern( pattern = getShadingPattern(this.objs.get(objId));
this.objs.get(objId),
this.cachedCanvasPatterns
);
this.cachedPatterns.set(objId, pattern); this.cachedPatterns.set(objId, pattern);
} }
if (matrix) { if (matrix) {
@ -2450,7 +2426,7 @@ class CanvasGraphics {
ctx, ctx,
this, this,
ctx.mozCurrentTransformInverse, ctx.mozCurrentTransformInverse,
true PathType.SHADING
); );
const inv = ctx.mozCurrentTransformInverse; const inv = ctx.mozCurrentTransformInverse;
@ -2838,7 +2814,7 @@ class CanvasGraphics {
maskCtx, maskCtx,
this, this,
ctx.mozCurrentTransformInverse, ctx.mozCurrentTransformInverse,
false PathType.FILL
) )
: fillColor; : fillColor;
maskCtx.fillRect(0, 0, width, height); maskCtx.fillRect(0, 0, width, height);

View File

@ -22,6 +22,12 @@ import {
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
const PathType = {
FILL: "Fill",
STROKE: "Stroke",
SHADING: "Shading",
};
function applyBoundingBox(ctx, bbox) { function applyBoundingBox(ctx, bbox) {
if (!bbox || typeof Path2D === "undefined") { if (!bbox || typeof Path2D === "undefined") {
return; return;
@ -46,7 +52,7 @@ class BaseShadingPattern {
} }
class RadialAxialShadingPattern extends BaseShadingPattern { class RadialAxialShadingPattern extends BaseShadingPattern {
constructor(IR, cachedCanvasPatterns) { constructor(IR) {
super(); super();
this._type = IR[1]; this._type = IR[1];
this._bbox = IR[2]; this._bbox = IR[2];
@ -56,7 +62,6 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
this._r0 = IR[6]; this._r0 = IR[6];
this._r1 = IR[7]; this._r1 = IR[7];
this.matrix = null; this.matrix = null;
this.cachedCanvasPatterns = cachedCanvasPatterns;
} }
_createGradient(ctx) { _createGradient(ctx) {
@ -85,42 +90,53 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
return grad; return grad;
} }
getPattern(ctx, owner, inverse, shadingFill = false) { getPattern(ctx, owner, inverse, pathType) {
let pattern; let pattern;
if (!shadingFill) { if (pathType === PathType.STROKE || pathType === PathType.FILL) {
if (this.cachedCanvasPatterns.has(this)) { const ownerBBox = owner.current.getClippedPathBoundingBox(
pattern = this.cachedCanvasPatterns.get(this); pathType,
} else { ctx.mozCurrentTransform
const tmpCanvas = owner.cachedCanvases.getCanvas( ) || [0, 0, 0, 0];
"pattern", // Create a canvas that is only as big as the current path. This doesn't
owner.ctx.canvas.width, // allow us to cache the pattern, but it generally creates much smaller
owner.ctx.canvas.height, // canvases and saves memory use. See bug 1722807 for an example.
true const width = Math.ceil(ownerBBox[2] - ownerBBox[0]) || 1;
); const height = Math.ceil(ownerBBox[3] - ownerBBox[1]) || 1;
const tmpCtx = tmpCanvas.context; const tmpCanvas = owner.cachedCanvases.getCanvas(
tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height); "pattern",
tmpCtx.beginPath(); width,
tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height); height,
true
);
tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform); const tmpCtx = tmpCanvas.context;
if (this.matrix) { tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
tmpCtx.transform.apply(tmpCtx, this.matrix); tmpCtx.beginPath();
} tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
applyBoundingBox(tmpCtx, this._bbox); // Non shading fill patterns are positioned relative to the base transform
// (usually the page's initial transform), but we may have created a
// smaller canvas based on the path, so we must account for the shift.
tmpCtx.translate(-ownerBBox[0], -ownerBBox[1]);
inverse = Util.transform(inverse, [
1,
0,
0,
1,
ownerBBox[0],
ownerBBox[1],
]);
tmpCtx.fillStyle = this._createGradient(tmpCtx); tmpCtx.transform.apply(tmpCtx, owner.baseTransform);
tmpCtx.fill(); if (this.matrix) {
tmpCtx.transform.apply(tmpCtx, this.matrix);
pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
this.cachedCanvasPatterns.set(this, pattern);
} }
} else { applyBoundingBox(tmpCtx, this._bbox);
// Don't bother caching gradients, they are quick to rebuild.
applyBoundingBox(ctx, this._bbox); tmpCtx.fillStyle = this._createGradient(tmpCtx);
pattern = this._createGradient(ctx); tmpCtx.fill();
}
if (!shadingFill) { pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
const domMatrix = new DOMMatrix(inverse); const domMatrix = new DOMMatrix(inverse);
try { try {
pattern.setTransform(domMatrix); pattern.setTransform(domMatrix);
@ -129,6 +145,12 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
// and in Node.js (see issue 13724). // and in Node.js (see issue 13724).
warn(`RadialAxialShadingPattern.getPattern: "${ex?.message}".`); warn(`RadialAxialShadingPattern.getPattern: "${ex?.message}".`);
} }
} else {
// Shading fills are applied relative to the current matrix which is also
// how canvas gradients work, so there's no need to do anything special
// here.
applyBoundingBox(ctx, this._bbox);
pattern = this._createGradient(ctx);
} }
return pattern; return pattern;
} }
@ -382,10 +404,10 @@ class MeshShadingPattern extends BaseShadingPattern {
}; };
} }
getPattern(ctx, owner, inverse, shadingFill = false) { getPattern(ctx, owner, inverse, pathType) {
applyBoundingBox(ctx, this._bbox); applyBoundingBox(ctx, this._bbox);
let scale; let scale;
if (shadingFill) { if (pathType === PathType.SHADING) {
scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform); scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
} else { } else {
// Obtain scale from matrix and current transformation matrix. // Obtain scale from matrix and current transformation matrix.
@ -400,11 +422,11 @@ class MeshShadingPattern extends BaseShadingPattern {
// might cause OOM. // might cause OOM.
const temporaryPatternCanvas = this._createMeshCanvas( const temporaryPatternCanvas = this._createMeshCanvas(
scale, scale,
shadingFill ? null : this._background, pathType === PathType.SHADING ? null : this._background,
owner.cachedCanvases owner.cachedCanvases
); );
if (!shadingFill) { if (pathType !== PathType.SHADING) {
ctx.setTransform.apply(ctx, owner.baseTransform); ctx.setTransform.apply(ctx, owner.baseTransform);
if (this.matrix) { if (this.matrix) {
ctx.transform.apply(ctx, this.matrix); ctx.transform.apply(ctx, this.matrix);
@ -427,10 +449,10 @@ class DummyShadingPattern extends BaseShadingPattern {
} }
} }
function getShadingPattern(IR, cachedCanvasPatterns) { function getShadingPattern(IR) {
switch (IR[0]) { switch (IR[0]) {
case "RadialAxial": case "RadialAxial":
return new RadialAxialShadingPattern(IR, cachedCanvasPatterns); return new RadialAxialShadingPattern(IR);
case "Mesh": case "Mesh":
return new MeshShadingPattern(IR); return new MeshShadingPattern(IR);
case "Dummy": case "Dummy":
@ -621,10 +643,10 @@ class TilingPattern {
} }
} }
getPattern(ctx, owner, inverse, shadingFill = false) { getPattern(ctx, owner, inverse, pathType) {
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix. // PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = inverse; let matrix = inverse;
if (!shadingFill) { if (pathType !== PathType.SHADING) {
matrix = Util.transform(matrix, owner.baseTransform); matrix = Util.transform(matrix, owner.baseTransform);
if (this.matrix) { if (this.matrix) {
matrix = Util.transform(matrix, this.matrix); matrix = Util.transform(matrix, this.matrix);
@ -657,4 +679,4 @@ class TilingPattern {
} }
} }
export { getShadingPattern, TilingPattern }; export { getShadingPattern, PathType, TilingPattern };