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,
warn,
} 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";
// <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_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
// before it stops and shedules a continue of execution.
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) {
const POINT_TO_PROCESS_LIMIT = 1000;
const POINT_TYPES = new Uint8Array([
@ -639,8 +599,23 @@ class CanvasExtraState {
this.updatePathMinMax(transform, box[2], box[3]);
}
getPathBoundingBox() {
return [this.minX, this.minY, this.maxX, this.maxY];
getPathBoundingBox(pathType = PathType.FILL, transform = null) {
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() {
@ -656,8 +631,11 @@ class CanvasExtraState {
this.maxY = 0;
}
getClippedPathBoundingBox() {
return Util.intersect(this.clipBox, this.getPathBoundingBox());
getClippedPathBoundingBox(pathType = PathType.FILL, transform = null) {
return Util.intersect(
this.clipBox,
this.getPathBoundingBox(pathType, transform)
);
}
}
@ -1121,7 +1099,6 @@ class CanvasGraphics {
this.markedContentStack = [];
this.optionalContentConfig = optionalContentConfig;
this.cachedCanvases = new CachedCanvases(this.canvasFactory);
this.cachedCanvasPatterns = new LRUCache(MAX_CACHED_CANVAS_PATTERNS);
this.cachedPatterns = new Map();
if (canvasCtx) {
// NOTE: if mozCurrentTransform is polyfilled, then the current state of
@ -1273,7 +1250,6 @@ class CanvasGraphics {
}
this.cachedCanvases.clear();
this.cachedCanvasPatterns.clear();
this.cachedPatterns.clear();
if (this.imageLayer) {
@ -1420,7 +1396,7 @@ class CanvasGraphics {
-offsetY,
]);
fillCtx.fillStyle = isPatternFill
? fillColor.getPattern(ctx, this, inverse, false)
? fillColor.getPattern(ctx, this, inverse, PathType.FILL)
: fillColor;
fillCtx.fillRect(0, 0, width, height);
@ -1772,7 +1748,8 @@ class CanvasGraphics {
ctx.strokeStyle = strokeColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
ctx.mozCurrentTransformInverse,
PathType.STROKE
);
// Prevent drawing too thin lines by enforcing a minimum line width.
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth);
@ -1819,7 +1796,8 @@ class CanvasGraphics {
ctx.fillStyle = fillColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
ctx.mozCurrentTransformInverse,
PathType.FILL
);
needRestore = true;
}
@ -2161,7 +2139,8 @@ class CanvasGraphics {
const pattern = current.fillColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
ctx.mozCurrentTransformInverse,
PathType.FILL
);
patternTransform = ctx.mozCurrentTransform;
ctx.restore();
@ -2426,10 +2405,7 @@ class CanvasGraphics {
if (this.cachedPatterns.has(objId)) {
pattern = this.cachedPatterns.get(objId);
} else {
pattern = getShadingPattern(
this.objs.get(objId),
this.cachedCanvasPatterns
);
pattern = getShadingPattern(this.objs.get(objId));
this.cachedPatterns.set(objId, pattern);
}
if (matrix) {
@ -2450,7 +2426,7 @@ class CanvasGraphics {
ctx,
this,
ctx.mozCurrentTransformInverse,
true
PathType.SHADING
);
const inv = ctx.mozCurrentTransformInverse;
@ -2838,7 +2814,7 @@ class CanvasGraphics {
maskCtx,
this,
ctx.mozCurrentTransformInverse,
false
PathType.FILL
)
: fillColor;
maskCtx.fillRect(0, 0, width, height);

View File

@ -22,6 +22,12 @@ import {
warn,
} from "../shared/util.js";
const PathType = {
FILL: "Fill",
STROKE: "Stroke",
SHADING: "Shading",
};
function applyBoundingBox(ctx, bbox) {
if (!bbox || typeof Path2D === "undefined") {
return;
@ -46,7 +52,7 @@ class BaseShadingPattern {
}
class RadialAxialShadingPattern extends BaseShadingPattern {
constructor(IR, cachedCanvasPatterns) {
constructor(IR) {
super();
this._type = IR[1];
this._bbox = IR[2];
@ -56,7 +62,6 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
this._r0 = IR[6];
this._r1 = IR[7];
this.matrix = null;
this.cachedCanvasPatterns = cachedCanvasPatterns;
}
_createGradient(ctx) {
@ -85,42 +90,53 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
return grad;
}
getPattern(ctx, owner, inverse, shadingFill = false) {
getPattern(ctx, owner, inverse, pathType) {
let pattern;
if (!shadingFill) {
if (this.cachedCanvasPatterns.has(this)) {
pattern = this.cachedCanvasPatterns.get(this);
} else {
const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern",
owner.ctx.canvas.width,
owner.ctx.canvas.height,
true
);
if (pathType === PathType.STROKE || pathType === PathType.FILL) {
const ownerBBox = owner.current.getClippedPathBoundingBox(
pathType,
ctx.mozCurrentTransform
) || [0, 0, 0, 0];
// Create a canvas that is only as big as the current path. This doesn't
// allow us to cache the pattern, but it generally creates much smaller
// canvases and saves memory use. See bug 1722807 for an example.
const width = Math.ceil(ownerBBox[2] - ownerBBox[0]) || 1;
const height = Math.ceil(ownerBBox[3] - ownerBBox[1]) || 1;
const tmpCtx = tmpCanvas.context;
tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
tmpCtx.beginPath();
tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern",
width,
height,
true
);
tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform);
if (this.matrix) {
tmpCtx.transform.apply(tmpCtx, this.matrix);
}
applyBoundingBox(tmpCtx, this._bbox);
const tmpCtx = tmpCanvas.context;
tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
tmpCtx.beginPath();
tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
// 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.fill();
pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
this.cachedCanvasPatterns.set(this, pattern);
tmpCtx.transform.apply(tmpCtx, owner.baseTransform);
if (this.matrix) {
tmpCtx.transform.apply(tmpCtx, this.matrix);
}
} else {
// Don't bother caching gradients, they are quick to rebuild.
applyBoundingBox(ctx, this._bbox);
pattern = this._createGradient(ctx);
}
if (!shadingFill) {
applyBoundingBox(tmpCtx, this._bbox);
tmpCtx.fillStyle = this._createGradient(tmpCtx);
tmpCtx.fill();
pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
const domMatrix = new DOMMatrix(inverse);
try {
pattern.setTransform(domMatrix);
@ -129,6 +145,12 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
// and in Node.js (see issue 13724).
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;
}
@ -382,10 +404,10 @@ class MeshShadingPattern extends BaseShadingPattern {
};
}
getPattern(ctx, owner, inverse, shadingFill = false) {
getPattern(ctx, owner, inverse, pathType) {
applyBoundingBox(ctx, this._bbox);
let scale;
if (shadingFill) {
if (pathType === PathType.SHADING) {
scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
} else {
// Obtain scale from matrix and current transformation matrix.
@ -400,11 +422,11 @@ class MeshShadingPattern extends BaseShadingPattern {
// might cause OOM.
const temporaryPatternCanvas = this._createMeshCanvas(
scale,
shadingFill ? null : this._background,
pathType === PathType.SHADING ? null : this._background,
owner.cachedCanvases
);
if (!shadingFill) {
if (pathType !== PathType.SHADING) {
ctx.setTransform.apply(ctx, owner.baseTransform);
if (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]) {
case "RadialAxial":
return new RadialAxialShadingPattern(IR, cachedCanvasPatterns);
return new RadialAxialShadingPattern(IR);
case "Mesh":
return new MeshShadingPattern(IR);
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.
let matrix = inverse;
if (!shadingFill) {
if (pathType !== PathType.SHADING) {
matrix = Util.transform(matrix, owner.baseTransform);
if (this.matrix) {
matrix = Util.transform(matrix, this.matrix);
@ -657,4 +679,4 @@ class TilingPattern {
}
}
export { getShadingPattern, TilingPattern };
export { getShadingPattern, PathType, TilingPattern };