diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 9d111b31e..29120b259 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -1315,20 +1315,17 @@ class PartialEvaluator { } parseShading({ - keyObj, shading, resources, localColorSpaceCache, localShadingPatternCache, - matrix = null, }) { // Shadings and patterns may be referenced by the same name but the resource // dictionary could be different so we can't use the name for the cache key. - let id = localShadingPatternCache.get(keyObj); + let id = localShadingPatternCache.get(shading); if (!id) { var shadingFill = Pattern.parseShading( shading, - matrix, this.xref, resources, this.handler, @@ -1337,7 +1334,7 @@ class PartialEvaluator { ); const patternIR = shadingFill.getIR(); id = `pattern_${this.idFactory.createObjId()}`; - localShadingPatternCache.set(keyObj, id); + localShadingPatternCache.set(shading, id); this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); } return id; @@ -1402,14 +1399,12 @@ class PartialEvaluator { const shading = dict.get("Shading"); const matrix = dict.getArray("Matrix"); const objId = this.parseShading({ - keyObj: pattern, shading, - matrix, resources, localColorSpaceCache, localShadingPatternCache, }); - operatorList.addOp(fn, ["Shading", objId]); + operatorList.addOp(fn, ["Shading", objId, matrix]); return undefined; } throw new FormatError(`Unknown PatternType: ${typeNum}`); @@ -1942,7 +1937,6 @@ class PartialEvaluator { throw new FormatError("No shading object found"); } const patternId = self.parseShading({ - keyObj: shading, shading, resources, localColorSpaceCache, diff --git a/src/core/pattern.js b/src/core/pattern.js index d6a3e29e2..113f402e4 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -44,7 +44,6 @@ class Pattern { static parseShading( shading, - matrix, xref, res, handler, @@ -60,7 +59,6 @@ class Pattern { case ShadingType.RADIAL: return new RadialAxialShading( dict, - matrix, xref, res, pdfFunctionFactory, @@ -72,7 +70,6 @@ class Pattern { case ShadingType.TENSOR_PATCH_MESH: return new MeshShading( shading, - matrix, xref, res, pdfFunctionFactory, @@ -115,16 +112,8 @@ class BaseShading { // Radial and axial shading have very similar implementations // If needed, the implementations can be broken into two classes. class RadialAxialShading extends BaseShading { - constructor( - dict, - matrix, - xref, - resources, - pdfFunctionFactory, - localColorSpaceCache - ) { + constructor(dict, xref, resources, pdfFunctionFactory, localColorSpaceCache) { super(); - this.matrix = matrix; this.coordsArr = dict.getArray("Coords"); this.shadingType = dict.get("ShadingType"); const cs = ColorSpace.parse({ @@ -244,17 +233,7 @@ class RadialAxialShading extends BaseShading { unreachable(`getPattern type unknown: ${shadingType}`); } - return [ - "RadialAxial", - type, - this.bbox, - this.colorStops, - p0, - p1, - r0, - r1, - this.matrix, - ]; + return ["RadialAxial", type, this.bbox, this.colorStops, p0, p1, r0, r1]; } } @@ -418,7 +397,6 @@ class MeshShading extends BaseShading { constructor( stream, - matrix, xref, resources, pdfFunctionFactory, @@ -429,7 +407,6 @@ class MeshShading extends BaseShading { throw new FormatError("Mesh data is not a stream"); } const dict = stream.dict; - this.matrix = matrix; this.shadingType = dict.get("ShadingType"); const bbox = dict.getArray("BBox"); if (Array.isArray(bbox) && bbox.length === 4) { @@ -942,7 +919,6 @@ class MeshShading extends BaseShading { this.colors, this.figures, this.bounds, - this.matrix, this.bbox, this.background, ]; diff --git a/src/display/canvas.js b/src/display/canvas.js index 1cf328966..2938e0b99 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -37,6 +37,10 @@ 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 @@ -231,6 +235,46 @@ 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([ @@ -866,6 +910,7 @@ 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 @@ -1017,6 +1062,7 @@ class CanvasGraphics { } this.cachedCanvases.clear(); + this.cachedCanvasPatterns.clear(); this.cachedPatterns.clear(); if (this.imageLayer) { @@ -2125,7 +2171,7 @@ class CanvasGraphics { baseTransform ); } else { - pattern = this._getPattern(IR[1]); + pattern = this._getPattern(IR[1], IR[2]); } return pattern; } @@ -2152,12 +2198,20 @@ class CanvasGraphics { this.current.patternFill = false; } - _getPattern(objId) { + _getPattern(objId, matrix = null) { + let pattern; if (this.cachedPatterns.has(objId)) { - return this.cachedPatterns.get(objId); + pattern = this.cachedPatterns.get(objId); + } else { + pattern = getShadingPattern( + this.objs.get(objId), + this.cachedCanvasPatterns + ); + this.cachedPatterns.set(objId, pattern); + } + if (matrix) { + pattern.matrix = matrix; } - const pattern = getShadingPattern(this.objs.get(objId)); - this.cachedPatterns.set(objId, pattern); return pattern; } diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 72103d377..e6e112fe9 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -46,7 +46,7 @@ class BaseShadingPattern { } class RadialAxialShadingPattern extends BaseShadingPattern { - constructor(IR) { + constructor(IR, cachedCanvasPatterns) { super(); this._type = IR[1]; this._bbox = IR[2]; @@ -55,8 +55,8 @@ class RadialAxialShadingPattern extends BaseShadingPattern { this._p1 = IR[5]; this._r0 = IR[6]; this._r1 = IR[7]; - this._matrix = IR[8]; - this._patternCache = null; + this.matrix = null; + this.cachedCanvasPatterns = cachedCanvasPatterns; } _createGradient(ctx) { @@ -87,10 +87,10 @@ class RadialAxialShadingPattern extends BaseShadingPattern { getPattern(ctx, owner, inverse, shadingFill = false) { let pattern; - if (this._patternCache) { - pattern = this._patternCache; - } else { - if (!shadingFill) { + if (!shadingFill) { + if (this.cachedCanvasPatterns.has(this)) { + pattern = this.cachedCanvasPatterns.get(this); + } else { const tmpCanvas = owner.cachedCanvases.getCanvas( "pattern", owner.ctx.canvas.width, @@ -104,8 +104,8 @@ class RadialAxialShadingPattern extends BaseShadingPattern { tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height); tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform); - if (this._matrix) { - tmpCtx.transform.apply(tmpCtx, this._matrix); + if (this.matrix) { + tmpCtx.transform.apply(tmpCtx, this.matrix); } applyBoundingBox(tmpCtx, this._bbox); @@ -113,11 +113,12 @@ class RadialAxialShadingPattern extends BaseShadingPattern { tmpCtx.fill(); pattern = ctx.createPattern(tmpCanvas.canvas, "repeat"); - } else { - applyBoundingBox(ctx, this._bbox); - pattern = this._createGradient(ctx); + this.cachedCanvasPatterns.set(this, pattern); } - this._patternCache = pattern; + } else { + // Don't bother caching gradients, they are quick to rebuild. + applyBoundingBox(ctx, this._bbox); + pattern = this._createGradient(ctx); } if (!shadingFill) { const domMatrix = new DOMMatrix(inverse); @@ -305,9 +306,9 @@ class MeshShadingPattern extends BaseShadingPattern { this._colors = IR[3]; this._figures = IR[4]; this._bounds = IR[5]; - this._matrix = IR[6]; this._bbox = IR[7]; this._background = IR[8]; + this.matrix = null; } _createMeshCanvas(combinedScale, backgroundColor, cachedCanvases) { @@ -389,8 +390,8 @@ class MeshShadingPattern extends BaseShadingPattern { } else { // Obtain scale from matrix and current transformation matrix. scale = Util.singularValueDecompose2dScale(owner.baseTransform); - if (this._matrix) { - const matrixScale = Util.singularValueDecompose2dScale(this._matrix); + if (this.matrix) { + const matrixScale = Util.singularValueDecompose2dScale(this.matrix); scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]]; } } @@ -405,8 +406,8 @@ class MeshShadingPattern extends BaseShadingPattern { if (!shadingFill) { ctx.setTransform.apply(ctx, owner.baseTransform); - if (this._matrix) { - ctx.transform.apply(ctx, this._matrix); + if (this.matrix) { + ctx.transform.apply(ctx, this.matrix); } } @@ -426,10 +427,10 @@ class DummyShadingPattern extends BaseShadingPattern { } } -function getShadingPattern(IR) { +function getShadingPattern(IR, cachedCanvasPatterns) { switch (IR[0]) { case "RadialAxial": - return new RadialAxialShadingPattern(IR); + return new RadialAxialShadingPattern(IR, cachedCanvasPatterns); case "Mesh": return new MeshShadingPattern(IR); case "Dummy":