Merge pull request #13808 from brendandahl/pattern-cache-v2

Improve caching of shading patterns. (bug 1721949)
This commit is contained in:
Brendan Dahl 2021-07-28 11:17:16 -07:00 committed by GitHub
commit 4ad5c5d52a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 85 additions and 60 deletions

View File

@ -1315,20 +1315,17 @@ class PartialEvaluator {
} }
parseShading({ parseShading({
keyObj,
shading, shading,
resources, resources,
localColorSpaceCache, localColorSpaceCache,
localShadingPatternCache, localShadingPatternCache,
matrix = null,
}) { }) {
// Shadings and patterns may be referenced by the same name but the resource // 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. // 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) { if (!id) {
var shadingFill = Pattern.parseShading( var shadingFill = Pattern.parseShading(
shading, shading,
matrix,
this.xref, this.xref,
resources, resources,
this.handler, this.handler,
@ -1337,7 +1334,7 @@ class PartialEvaluator {
); );
const patternIR = shadingFill.getIR(); const patternIR = shadingFill.getIR();
id = `pattern_${this.idFactory.createObjId()}`; id = `pattern_${this.idFactory.createObjId()}`;
localShadingPatternCache.set(keyObj, id); localShadingPatternCache.set(shading, id);
this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]); this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]);
} }
return id; return id;
@ -1402,14 +1399,12 @@ class PartialEvaluator {
const shading = dict.get("Shading"); const shading = dict.get("Shading");
const matrix = dict.getArray("Matrix"); const matrix = dict.getArray("Matrix");
const objId = this.parseShading({ const objId = this.parseShading({
keyObj: pattern,
shading, shading,
matrix,
resources, resources,
localColorSpaceCache, localColorSpaceCache,
localShadingPatternCache, localShadingPatternCache,
}); });
operatorList.addOp(fn, ["Shading", objId]); operatorList.addOp(fn, ["Shading", objId, matrix]);
return undefined; return undefined;
} }
throw new FormatError(`Unknown PatternType: ${typeNum}`); throw new FormatError(`Unknown PatternType: ${typeNum}`);
@ -1942,7 +1937,6 @@ class PartialEvaluator {
throw new FormatError("No shading object found"); throw new FormatError("No shading object found");
} }
const patternId = self.parseShading({ const patternId = self.parseShading({
keyObj: shading,
shading, shading,
resources, resources,
localColorSpaceCache, localColorSpaceCache,

View File

@ -44,7 +44,6 @@ class Pattern {
static parseShading( static parseShading(
shading, shading,
matrix,
xref, xref,
res, res,
handler, handler,
@ -60,7 +59,6 @@ class Pattern {
case ShadingType.RADIAL: case ShadingType.RADIAL:
return new RadialAxialShading( return new RadialAxialShading(
dict, dict,
matrix,
xref, xref,
res, res,
pdfFunctionFactory, pdfFunctionFactory,
@ -72,7 +70,6 @@ class Pattern {
case ShadingType.TENSOR_PATCH_MESH: case ShadingType.TENSOR_PATCH_MESH:
return new MeshShading( return new MeshShading(
shading, shading,
matrix,
xref, xref,
res, res,
pdfFunctionFactory, pdfFunctionFactory,
@ -115,16 +112,8 @@ class BaseShading {
// Radial and axial shading have very similar implementations // Radial and axial shading have very similar implementations
// If needed, the implementations can be broken into two classes. // If needed, the implementations can be broken into two classes.
class RadialAxialShading extends BaseShading { class RadialAxialShading extends BaseShading {
constructor( constructor(dict, xref, resources, pdfFunctionFactory, localColorSpaceCache) {
dict,
matrix,
xref,
resources,
pdfFunctionFactory,
localColorSpaceCache
) {
super(); super();
this.matrix = matrix;
this.coordsArr = dict.getArray("Coords"); this.coordsArr = dict.getArray("Coords");
this.shadingType = dict.get("ShadingType"); this.shadingType = dict.get("ShadingType");
const cs = ColorSpace.parse({ const cs = ColorSpace.parse({
@ -244,17 +233,7 @@ class RadialAxialShading extends BaseShading {
unreachable(`getPattern type unknown: ${shadingType}`); unreachable(`getPattern type unknown: ${shadingType}`);
} }
return [ return ["RadialAxial", type, this.bbox, this.colorStops, p0, p1, r0, r1];
"RadialAxial",
type,
this.bbox,
this.colorStops,
p0,
p1,
r0,
r1,
this.matrix,
];
} }
} }
@ -418,7 +397,6 @@ class MeshShading extends BaseShading {
constructor( constructor(
stream, stream,
matrix,
xref, xref,
resources, resources,
pdfFunctionFactory, pdfFunctionFactory,
@ -429,7 +407,6 @@ class MeshShading extends BaseShading {
throw new FormatError("Mesh data is not a stream"); throw new FormatError("Mesh data is not a stream");
} }
const dict = stream.dict; const dict = stream.dict;
this.matrix = matrix;
this.shadingType = dict.get("ShadingType"); this.shadingType = dict.get("ShadingType");
const bbox = dict.getArray("BBox"); const bbox = dict.getArray("BBox");
if (Array.isArray(bbox) && bbox.length === 4) { if (Array.isArray(bbox) && bbox.length === 4) {
@ -942,7 +919,6 @@ class MeshShading extends BaseShading {
this.colors, this.colors,
this.figures, this.figures,
this.bounds, this.bounds,
this.matrix,
this.bbox, this.bbox,
this.background, this.background,
]; ];

View File

@ -37,6 +37,10 @@ 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
@ -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) { 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([
@ -866,6 +910,7 @@ 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
@ -1017,6 +1062,7 @@ class CanvasGraphics {
} }
this.cachedCanvases.clear(); this.cachedCanvases.clear();
this.cachedCanvasPatterns.clear();
this.cachedPatterns.clear(); this.cachedPatterns.clear();
if (this.imageLayer) { if (this.imageLayer) {
@ -2125,7 +2171,7 @@ class CanvasGraphics {
baseTransform baseTransform
); );
} else { } else {
pattern = this._getPattern(IR[1]); pattern = this._getPattern(IR[1], IR[2]);
} }
return pattern; return pattern;
} }
@ -2152,12 +2198,20 @@ class CanvasGraphics {
this.current.patternFill = false; this.current.patternFill = false;
} }
_getPattern(objId) { _getPattern(objId, matrix = null) {
let pattern;
if (this.cachedPatterns.has(objId)) { if (this.cachedPatterns.has(objId)) {
return this.cachedPatterns.get(objId); pattern = this.cachedPatterns.get(objId);
} } else {
const pattern = getShadingPattern(this.objs.get(objId)); pattern = getShadingPattern(
this.objs.get(objId),
this.cachedCanvasPatterns
);
this.cachedPatterns.set(objId, pattern); this.cachedPatterns.set(objId, pattern);
}
if (matrix) {
pattern.matrix = matrix;
}
return pattern; return pattern;
} }

View File

@ -46,7 +46,7 @@ class BaseShadingPattern {
} }
class RadialAxialShadingPattern extends BaseShadingPattern { class RadialAxialShadingPattern extends BaseShadingPattern {
constructor(IR) { constructor(IR, cachedCanvasPatterns) {
super(); super();
this._type = IR[1]; this._type = IR[1];
this._bbox = IR[2]; this._bbox = IR[2];
@ -55,8 +55,8 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
this._p1 = IR[5]; this._p1 = IR[5];
this._r0 = IR[6]; this._r0 = IR[6];
this._r1 = IR[7]; this._r1 = IR[7];
this._matrix = IR[8]; this.matrix = null;
this._patternCache = null; this.cachedCanvasPatterns = cachedCanvasPatterns;
} }
_createGradient(ctx) { _createGradient(ctx) {
@ -87,10 +87,10 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
getPattern(ctx, owner, inverse, shadingFill = false) { getPattern(ctx, owner, inverse, shadingFill = false) {
let pattern; 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( const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern", "pattern",
owner.ctx.canvas.width, owner.ctx.canvas.width,
@ -104,8 +104,8 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height); tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform); tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform);
if (this._matrix) { if (this.matrix) {
tmpCtx.transform.apply(tmpCtx, this._matrix); tmpCtx.transform.apply(tmpCtx, this.matrix);
} }
applyBoundingBox(tmpCtx, this._bbox); applyBoundingBox(tmpCtx, this._bbox);
@ -113,12 +113,13 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
tmpCtx.fill(); tmpCtx.fill();
pattern = ctx.createPattern(tmpCanvas.canvas, "repeat"); pattern = ctx.createPattern(tmpCanvas.canvas, "repeat");
this.cachedCanvasPatterns.set(this, pattern);
}
} else { } else {
// Don't bother caching gradients, they are quick to rebuild.
applyBoundingBox(ctx, this._bbox); applyBoundingBox(ctx, this._bbox);
pattern = this._createGradient(ctx); pattern = this._createGradient(ctx);
} }
this._patternCache = pattern;
}
if (!shadingFill) { if (!shadingFill) {
const domMatrix = new DOMMatrix(inverse); const domMatrix = new DOMMatrix(inverse);
try { try {
@ -305,9 +306,9 @@ class MeshShadingPattern extends BaseShadingPattern {
this._colors = IR[3]; this._colors = IR[3];
this._figures = IR[4]; this._figures = IR[4];
this._bounds = IR[5]; this._bounds = IR[5];
this._matrix = IR[6];
this._bbox = IR[7]; this._bbox = IR[7];
this._background = IR[8]; this._background = IR[8];
this.matrix = null;
} }
_createMeshCanvas(combinedScale, backgroundColor, cachedCanvases) { _createMeshCanvas(combinedScale, backgroundColor, cachedCanvases) {
@ -389,8 +390,8 @@ class MeshShadingPattern extends BaseShadingPattern {
} else { } else {
// Obtain scale from matrix and current transformation matrix. // Obtain scale from matrix and current transformation matrix.
scale = Util.singularValueDecompose2dScale(owner.baseTransform); scale = Util.singularValueDecompose2dScale(owner.baseTransform);
if (this._matrix) { if (this.matrix) {
const matrixScale = Util.singularValueDecompose2dScale(this._matrix); const matrixScale = Util.singularValueDecompose2dScale(this.matrix);
scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]]; scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]];
} }
} }
@ -405,8 +406,8 @@ class MeshShadingPattern extends BaseShadingPattern {
if (!shadingFill) { if (!shadingFill) {
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);
} }
} }
@ -426,10 +427,10 @@ class DummyShadingPattern extends BaseShadingPattern {
} }
} }
function getShadingPattern(IR) { function getShadingPattern(IR, cachedCanvasPatterns) {
switch (IR[0]) { switch (IR[0]) {
case "RadialAxial": case "RadialAxial":
return new RadialAxialShadingPattern(IR); return new RadialAxialShadingPattern(IR, cachedCanvasPatterns);
case "Mesh": case "Mesh":
return new MeshShadingPattern(IR); return new MeshShadingPattern(IR);
case "Dummy": case "Dummy":