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:
commit
38efd13a54
@ -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);
|
||||
|
@ -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 };
|
||||
|
Loading…
x
Reference in New Issue
Block a user