Merge pull request #14230 from brendandahl/reduce-gradient-size

Create shading patterns the size of the current path. (bug 1722807)
This commit is contained in:
Jonas Jenwald 2021-11-06 10:17:49 +01:00 committed by GitHub
commit 38efd13a54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 };