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, 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 };