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