Improve performance of reused patterns.

Bug 1721218 has a shading pattern that was used thousands of times.
To improve performance of this PDF:
 - add a cache for patterns in the evaluator and only send the IR form once
   to the main thread (this also makes caching in canvas easier)
 - cache the created canvas radial/axial patterns
 - for shading fill radial/axial use the pattern directly instead of creating temporary
   canvas
This commit is contained in:
Brendan Dahl 2021-07-21 12:27:39 -07:00
parent 2cf90cd9ad
commit da1af02ac8
7 changed files with 121 additions and 59 deletions

View File

@ -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( handleColorN(
operatorList, operatorList,
fn, fn,
@ -1323,7 +1352,8 @@ class PartialEvaluator {
resources, resources,
task, task,
localColorSpaceCache, localColorSpaceCache,
localTilingPatternCache localTilingPatternCache,
localShadingPatternCache
) { ) {
// compile tiling patterns // compile tiling patterns
const patternName = args.pop(); const patternName = args.pop();
@ -1350,7 +1380,7 @@ class PartialEvaluator {
// if and only if there are PDF documents where doing so would // if and only if there are PDF documents where doing so would
// significantly improve performance. // significantly improve performance.
let pattern = patterns.get(name); const pattern = patterns.get(name);
if (pattern) { if (pattern) {
const dict = isStream(pattern) ? pattern.dict : pattern; const dict = isStream(pattern) ? pattern.dict : pattern;
const typeNum = dict.get("PatternType"); const typeNum = dict.get("PatternType");
@ -1371,16 +1401,15 @@ class PartialEvaluator {
} else if (typeNum === PatternType.SHADING) { } else if (typeNum === PatternType.SHADING) {
const shading = dict.get("Shading"); const shading = dict.get("Shading");
const matrix = dict.getArray("Matrix"); const matrix = dict.getArray("Matrix");
pattern = Pattern.parseShading( const objId = this.parseShading({
keyObj: pattern,
shading, shading,
matrix, matrix,
this.xref,
resources, resources,
this.handler, localColorSpaceCache,
this._pdfFunctionFactory, localShadingPatternCache,
localColorSpaceCache });
); operatorList.addOp(fn, ["Shading", objId]);
operatorList.addOp(fn, pattern.getIR());
return undefined; return undefined;
} }
throw new FormatError(`Unknown PatternType: ${typeNum}`); throw new FormatError(`Unknown PatternType: ${typeNum}`);
@ -1513,6 +1542,7 @@ class PartialEvaluator {
const localColorSpaceCache = new LocalColorSpaceCache(); const localColorSpaceCache = new LocalColorSpaceCache();
const localGStateCache = new LocalGStateCache(); const localGStateCache = new LocalGStateCache();
const localTilingPatternCache = new LocalTilingPatternCache(); const localTilingPatternCache = new LocalTilingPatternCache();
const localShadingPatternCache = new Map();
const xobjs = resources.get("XObject") || Dict.empty; const xobjs = resources.get("XObject") || Dict.empty;
const patterns = resources.get("Pattern") || Dict.empty; const patterns = resources.get("Pattern") || Dict.empty;
@ -1869,7 +1899,8 @@ class PartialEvaluator {
resources, resources,
task, task,
localColorSpaceCache, localColorSpaceCache,
localTilingPatternCache localTilingPatternCache,
localShadingPatternCache
) )
); );
return; return;
@ -1890,7 +1921,8 @@ class PartialEvaluator {
resources, resources,
task, task,
localColorSpaceCache, localColorSpaceCache,
localTilingPatternCache localTilingPatternCache,
localShadingPatternCache
) )
); );
return; return;
@ -1909,18 +1941,14 @@ class PartialEvaluator {
if (!shading) { if (!shading) {
throw new FormatError("No shading object found"); throw new FormatError("No shading object found");
} }
const patternId = self.parseShading({
var shadingFill = Pattern.parseShading( keyObj: shading,
shading, shading,
null,
xref,
resources, resources,
self.handler, localColorSpaceCache,
self._pdfFunctionFactory, localShadingPatternCache,
localColorSpaceCache });
); args = [patternId];
var patternIR = shadingFill.getIR();
args = [patternIR];
fn = OPS.shadingFill; fn = OPS.shadingFill;
break; break;
case OPS.setGState: case OPS.setGState:

View File

@ -2707,6 +2707,9 @@ class WorkerTransport {
pageProxy.cleanupAfterRender = true; pageProxy.cleanupAfterRender = true;
} }
break; break;
case "Pattern":
pageProxy.objs.resolve(id, imageData);
break;
default: default:
throw new Error(`Got unknown object type ${type}`); throw new Error(`Got unknown object type ${type}`);
} }

View File

@ -872,6 +872,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
this.markedContentStack = []; this.markedContentStack = [];
this.optionalContentConfig = optionalContentConfig; this.optionalContentConfig = optionalContentConfig;
this.cachedCanvases = new CachedCanvases(this.canvasFactory); this.cachedCanvases = new CachedCanvases(this.canvasFactory);
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
// the transformation must already be set in canvasCtx._transformMatrix. // the transformation must already be set in canvasCtx._transformMatrix.
@ -1025,6 +1026,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
} }
this.cachedCanvases.clear(); this.cachedCanvases.clear();
this.cachedPatterns.clear();
if (this.imageLayer) { if (this.imageLayer) {
this.imageLayer.endLayout(); this.imageLayer.endLayout();
@ -2132,7 +2134,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
baseTransform baseTransform
); );
} else { } else {
pattern = getShadingPattern(IR); pattern = this._getPattern(IR[1]);
} }
return pattern; return pattern;
} }
@ -2159,14 +2161,23 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
this.current.patternFill = false; 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) { if (!this.contentVisible) {
return; return;
} }
const ctx = this.ctx; const ctx = this.ctx;
this.save(); this.save();
const pattern = getShadingPattern(patternIR); const pattern = this._getPattern(objId);
ctx.fillStyle = pattern.getPattern( ctx.fillStyle = pattern.getPattern(
ctx, ctx,
this, this,

View File

@ -56,41 +56,20 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
this._r0 = IR[6]; this._r0 = IR[6];
this._r1 = IR[7]; this._r1 = IR[7];
this._matrix = IR[8]; this._matrix = IR[8];
this._patternCache = null;
} }
getPattern(ctx, owner, inverse, shadingFill = false) { _createGradient(ctx) {
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);
let grad; let grad;
if (this._type === "axial") { if (this._type === "axial") {
grad = tmpCtx.createLinearGradient( grad = ctx.createLinearGradient(
this._p0[0], this._p0[0],
this._p0[1], this._p0[1],
this._p1[0], this._p1[0],
this._p1[1] this._p1[1]
); );
} else if (this._type === "radial") { } else if (this._type === "radial") {
grad = tmpCtx.createRadialGradient( grad = ctx.createRadialGradient(
this._p0[0], this._p0[0],
this._p0[1], this._p0[1],
this._r0, this._r0,
@ -103,12 +82,45 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
for (const colorStop of this._colorStops) { for (const colorStop of this._colorStops) {
grad.addColorStop(colorStop[0], colorStop[1]); grad.addColorStop(colorStop[0], colorStop[1]);
} }
tmpCtx.fillStyle = grad; return grad;
}
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 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(); 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); const domMatrix = new DOMMatrix(inverse);
const pattern = ctx.createPattern(tmpCanvas.canvas, "repeat");
try { try {
pattern.setTransform(domMatrix); pattern.setTransform(domMatrix);
} catch (ex) { } catch (ex) {
@ -116,6 +128,7 @@ 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}".`);
} }
}
return pattern; return pattern;
} }
} }

View File

@ -62,6 +62,7 @@
!issue7872.pdf !issue7872.pdf
!issue7901.pdf !issue7901.pdf
!issue8061.pdf !issue8061.pdf
!bug1721218_reduced.pdf
!issue8088.pdf !issue8088.pdf
!issue8125.pdf !issue8125.pdf
!issue8229.pdf !issue8229.pdf

Binary file not shown.

View File

@ -5623,6 +5623,12 @@
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
}, },
{ "id": "bug1721218_reduced",
"file": "pdfs/bug1721218_reduced.pdf",
"md5": "20ee3e9500d8f5f5e532ec2edfaaaaaa",
"rounds": 1,
"type": "eq"
},
{ "id": "pr8491", { "id": "pr8491",
"file": "pdfs/pr8491.pdf", "file": "pdfs/pr8491.pdf",
"md5": "36ea2e28cd77e9e70731f574ab27cbe0", "md5": "36ea2e28cd77e9e70731f574ab27cbe0",