diff --git a/src/display/canvas.js b/src/display/canvas.js index a04b50b66..713dc9dd9 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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"; // 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); diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 39ff3e572..bc32894f7 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -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 };