diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 0f6330c2f..517056d23 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -1314,6 +1314,35 @@ 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); + if (!id) { + var shadingFill = Pattern.parseShading( + shading, + matrix, + this.xref, + resources, + this.handler, + this._pdfFunctionFactory, + localColorSpaceCache + ); + const patternIR = shadingFill.getIR(); + id = `pattern_${this.idFactory.createObjId()}`; + localShadingPatternCache.set(keyObj, id); + this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); + } + return id; + } + handleColorN( operatorList, fn, @@ -1323,7 +1352,8 @@ class PartialEvaluator { resources, task, localColorSpaceCache, - localTilingPatternCache + localTilingPatternCache, + localShadingPatternCache ) { // compile tiling patterns const patternName = args.pop(); @@ -1350,7 +1380,7 @@ class PartialEvaluator { // if and only if there are PDF documents where doing so would // significantly improve performance. - let pattern = patterns.get(name); + const pattern = patterns.get(name); if (pattern) { const dict = isStream(pattern) ? pattern.dict : pattern; const typeNum = dict.get("PatternType"); @@ -1371,16 +1401,15 @@ class PartialEvaluator { } else if (typeNum === PatternType.SHADING) { const shading = dict.get("Shading"); const matrix = dict.getArray("Matrix"); - pattern = Pattern.parseShading( + const objId = this.parseShading({ + keyObj: pattern, shading, matrix, - this.xref, resources, - this.handler, - this._pdfFunctionFactory, - localColorSpaceCache - ); - operatorList.addOp(fn, pattern.getIR()); + localColorSpaceCache, + localShadingPatternCache, + }); + operatorList.addOp(fn, ["Shading", objId]); return undefined; } throw new FormatError(`Unknown PatternType: ${typeNum}`); @@ -1513,6 +1542,7 @@ class PartialEvaluator { const localColorSpaceCache = new LocalColorSpaceCache(); const localGStateCache = new LocalGStateCache(); const localTilingPatternCache = new LocalTilingPatternCache(); + const localShadingPatternCache = new Map(); const xobjs = resources.get("XObject") || Dict.empty; const patterns = resources.get("Pattern") || Dict.empty; @@ -1869,7 +1899,8 @@ class PartialEvaluator { resources, task, localColorSpaceCache, - localTilingPatternCache + localTilingPatternCache, + localShadingPatternCache ) ); return; @@ -1890,7 +1921,8 @@ class PartialEvaluator { resources, task, localColorSpaceCache, - localTilingPatternCache + localTilingPatternCache, + localShadingPatternCache ) ); return; @@ -1909,18 +1941,14 @@ class PartialEvaluator { if (!shading) { throw new FormatError("No shading object found"); } - - var shadingFill = Pattern.parseShading( + const patternId = self.parseShading({ + keyObj: shading, shading, - null, - xref, resources, - self.handler, - self._pdfFunctionFactory, - localColorSpaceCache - ); - var patternIR = shadingFill.getIR(); - args = [patternIR]; + localColorSpaceCache, + localShadingPatternCache, + }); + args = [patternId]; fn = OPS.shadingFill; break; case OPS.setGState: diff --git a/src/display/api.js b/src/display/api.js index 45f40df3a..44ada821b 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -2707,6 +2707,9 @@ class WorkerTransport { pageProxy.cleanupAfterRender = true; } break; + case "Pattern": + pageProxy.objs.resolve(id, imageData); + break; default: throw new Error(`Got unknown object type ${type}`); } diff --git a/src/display/canvas.js b/src/display/canvas.js index 6fa9338ea..1cd7095cf 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -872,6 +872,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { this.markedContentStack = []; this.optionalContentConfig = optionalContentConfig; this.cachedCanvases = new CachedCanvases(this.canvasFactory); + this.cachedPatterns = new Map(); if (canvasCtx) { // NOTE: if mozCurrentTransform is polyfilled, then the current state of // the transformation must already be set in canvasCtx._transformMatrix. @@ -1025,6 +1026,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { } this.cachedCanvases.clear(); + this.cachedPatterns.clear(); if (this.imageLayer) { this.imageLayer.endLayout(); @@ -2132,7 +2134,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { baseTransform ); } else { - pattern = getShadingPattern(IR); + pattern = this._getPattern(IR[1]); } return pattern; } @@ -2159,14 +2161,23 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { this.current.patternFill = false; } - shadingFill(patternIR) { + _getPattern(objId) { + if (this.cachedPatterns.has(objId)) { + return this.cachedPatterns.get(objId); + } + const pattern = getShadingPattern(this.objs.get(objId)); + this.cachedPatterns.set(objId, pattern); + return pattern; + } + + shadingFill(objId) { if (!this.contentVisible) { return; } const ctx = this.ctx; this.save(); - const pattern = getShadingPattern(patternIR); + const pattern = this._getPattern(objId); ctx.fillStyle = pattern.getPattern( ctx, this, diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index d4321cb02..c4d4308bf 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -56,41 +56,20 @@ class RadialAxialShadingPattern extends BaseShadingPattern { this._r0 = IR[6]; this._r1 = IR[7]; this._matrix = IR[8]; + this._patternCache = null; } - getPattern(ctx, owner, inverse, shadingFill = false) { - const tmpCanvas = owner.cachedCanvases.getCanvas( - "pattern", - owner.ctx.canvas.width, - owner.ctx.canvas.height, - true - ); - - 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); - - if (!shadingFill) { - tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform); - if (this._matrix) { - tmpCtx.transform.apply(tmpCtx, this._matrix); - } - } else { - tmpCtx.setTransform.apply(tmpCtx, ctx.mozCurrentTransform); - } - applyBoundingBox(tmpCtx, this._bbox); - + _createGradient(ctx) { let grad; if (this._type === "axial") { - grad = tmpCtx.createLinearGradient( + grad = ctx.createLinearGradient( this._p0[0], this._p0[1], this._p1[0], this._p1[1] ); } else if (this._type === "radial") { - grad = tmpCtx.createRadialGradient( + grad = ctx.createRadialGradient( this._p0[0], this._p0[1], this._r0, @@ -103,18 +82,52 @@ class RadialAxialShadingPattern extends BaseShadingPattern { for (const colorStop of this._colorStops) { grad.addColorStop(colorStop[0], colorStop[1]); } - tmpCtx.fillStyle = grad; - tmpCtx.fill(); + return grad; + } - const domMatrix = new DOMMatrix(inverse); + getPattern(ctx, owner, inverse, shadingFill = false) { + let pattern; + if (this._patternCache) { + pattern = this._patternCache; + } else { + if (!shadingFill) { + const tmpCanvas = owner.cachedCanvases.getCanvas( + "pattern", + owner.ctx.canvas.width, + owner.ctx.canvas.height, + true + ); - const pattern = ctx.createPattern(tmpCanvas.canvas, "repeat"); - try { - pattern.setTransform(domMatrix); - } catch (ex) { - // Avoid rendering breaking completely in Firefox 78 ESR, - // and in Node.js (see issue 13724). - warn(`RadialAxialShadingPattern.getPattern: "${ex?.message}".`); + 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); + + tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform); + if (this._matrix) { + tmpCtx.transform.apply(tmpCtx, this._matrix); + } + applyBoundingBox(tmpCtx, this._bbox); + + tmpCtx.fillStyle = this._createGradient(tmpCtx); + tmpCtx.fill(); + + pattern = ctx.createPattern(tmpCanvas.canvas, "repeat"); + } else { + applyBoundingBox(ctx, this._bbox); + pattern = this._createGradient(ctx); + } + this._patternCache = pattern; + } + if (!shadingFill) { + const domMatrix = new DOMMatrix(inverse); + try { + pattern.setTransform(domMatrix); + } catch (ex) { + // Avoid rendering breaking completely in Firefox 78 ESR, + // and in Node.js (see issue 13724). + warn(`RadialAxialShadingPattern.getPattern: "${ex?.message}".`); + } } return pattern; } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index c3185e171..608bab487 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -62,6 +62,7 @@ !issue7872.pdf !issue7901.pdf !issue8061.pdf +!bug1721218_reduced.pdf !issue8088.pdf !issue8125.pdf !issue8229.pdf diff --git a/test/pdfs/bug1721218_reduced.pdf b/test/pdfs/bug1721218_reduced.pdf new file mode 100644 index 000000000..2e65de9ab Binary files /dev/null and b/test/pdfs/bug1721218_reduced.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 7090fe9eb..afdba3c79 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -5623,6 +5623,12 @@ "rounds": 1, "type": "eq" }, + { "id": "bug1721218_reduced", + "file": "pdfs/bug1721218_reduced.pdf", + "md5": "20ee3e9500d8f5f5e532ec2edfaaaaaa", + "rounds": 1, + "type": "eq" + }, { "id": "pr8491", "file": "pdfs/pr8491.pdf", "md5": "36ea2e28cd77e9e70731f574ab27cbe0",