Skip to content

Commit 9f71e42

Browse files
committed
Create shading patterns the size of the current path. (bug 1722807)
Previously, when we created a shading pattern canvas we created it as the same size as the page. This was good for caching if the same pattern was used over and over again, but when lots of different shadings are created that caused us to create many full page canvases. Instead of creating the full page canvses, instead create the canvas as the same size as the current path bounding box. This reduces memory consumption by a lot since most paths are pretty small. Also, in real world PDFs it's rare for a shading (non shading fill) to be reused over and over again. Bug 1721949 is an example where the same pattern is reused and it will be slightly slower than before.
1 parent c2f3351 commit 9f71e42

File tree

2 files changed

+98
-99
lines changed

2 files changed

+98
-99
lines changed

src/display/canvas.js

+38-61
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ import {
2727
Util,
2828
warn,
2929
} from "../shared/util.js";
30-
import { getShadingPattern, TilingPattern } from "./pattern_helper.js";
30+
import {
31+
getShadingPattern,
32+
PathType,
33+
TilingPattern,
34+
} from "./pattern_helper.js";
3135
import { PixelsPerInch } from "./display_utils.js";
3236

3337
// <canvas> contexts store most of the state we need natively.
@@ -38,10 +42,6 @@ const MIN_FONT_SIZE = 16;
3842
const MAX_FONT_SIZE = 100;
3943
const MAX_GROUP_SIZE = 4096;
4044

41-
// This value comes from sampling a few PDFs that re-use patterns, there doesn't
42-
// seem to be any that benefit from caching more than 2 patterns.
43-
const MAX_CACHED_CANVAS_PATTERNS = 2;
44-
4545
// Defines the time the `executeOperatorList`-method is going to be executing
4646
// before it stops and shedules a continue of execution.
4747
const EXECUTION_TIME = 15; // ms
@@ -366,46 +366,6 @@ class CachedCanvases {
366366
}
367367
}
368368

369-
/**
370-
* Least recently used cache implemented with a JS Map. JS Map keys are ordered
371-
* by last insertion.
372-
*/
373-
class LRUCache {
374-
constructor(maxSize = 0) {
375-
this._cache = new Map();
376-
this._maxSize = maxSize;
377-
}
378-
379-
has(key) {
380-
return this._cache.has(key);
381-
}
382-
383-
get(key) {
384-
if (this._cache.has(key)) {
385-
// Delete and set the value so it's moved to the end of the map iteration.
386-
const value = this._cache.get(key);
387-
this._cache.delete(key);
388-
this._cache.set(key, value);
389-
}
390-
return this._cache.get(key);
391-
}
392-
393-
set(key, value) {
394-
if (this._maxSize <= 0) {
395-
return;
396-
}
397-
if (this._cache.size + 1 > this._maxSize) {
398-
// Delete the least recently used.
399-
this._cache.delete(this._cache.keys().next().value);
400-
}
401-
this._cache.set(key, value);
402-
}
403-
404-
clear() {
405-
this._cache.clear();
406-
}
407-
}
408-
409369
function compileType3Glyph(imgData) {
410370
const POINT_TO_PROCESS_LIMIT = 1000;
411371
const POINT_TYPES = new Uint8Array([
@@ -639,8 +599,24 @@ class CanvasExtraState {
639599
this.updatePathMinMax(transform, box[2], box[3]);
640600
}
641601

642-
getPathBoundingBox() {
643-
return [this.minX, this.minY, this.maxX, this.maxY];
602+
getPathBoundingBox(pathType = PathType.FILL, transform = null) {
603+
const box = [this.minX, this.minY, this.maxX, this.maxY];
604+
if (pathType === PathType.STROKE) {
605+
if (!transform) {
606+
throw new Error("Stroke bounding box must include transform.");
607+
}
608+
// Stroked paths can be outside of the path bounding box by 1/2 the line
609+
// width.
610+
const scale = Util.singularValueDecompose2dScale(transform);
611+
const xStrokePad = (scale[0] * this.lineWidth) / 2;
612+
const yStrokePad = (scale[1] * this.lineWidth) / 2;
613+
// console.log(`increasing bounding box ${xStrokePad} ${yStrokePad}`);
614+
box[0] -= xStrokePad;
615+
box[1] -= yStrokePad;
616+
box[2] += xStrokePad;
617+
box[3] += yStrokePad;
618+
}
619+
return box;
644620
}
645621

646622
updateClipFromPath() {
@@ -656,8 +632,11 @@ class CanvasExtraState {
656632
this.maxY = 0;
657633
}
658634

659-
getClippedPathBoundingBox() {
660-
return Util.intersect(this.clipBox, this.getPathBoundingBox());
635+
getClippedPathBoundingBox(pathType = PathType.FILL, transform = null) {
636+
return Util.intersect(
637+
this.clipBox,
638+
this.getPathBoundingBox(pathType, transform)
639+
);
661640
}
662641
}
663642

@@ -1121,7 +1100,6 @@ class CanvasGraphics {
11211100
this.markedContentStack = [];
11221101
this.optionalContentConfig = optionalContentConfig;
11231102
this.cachedCanvases = new CachedCanvases(this.canvasFactory);
1124-
this.cachedCanvasPatterns = new LRUCache(MAX_CACHED_CANVAS_PATTERNS);
11251103
this.cachedPatterns = new Map();
11261104
if (canvasCtx) {
11271105
// NOTE: if mozCurrentTransform is polyfilled, then the current state of
@@ -1273,7 +1251,6 @@ class CanvasGraphics {
12731251
}
12741252

12751253
this.cachedCanvases.clear();
1276-
this.cachedCanvasPatterns.clear();
12771254
this.cachedPatterns.clear();
12781255

12791256
if (this.imageLayer) {
@@ -1420,7 +1397,7 @@ class CanvasGraphics {
14201397
-offsetY,
14211398
]);
14221399
fillCtx.fillStyle = isPatternFill
1423-
? fillColor.getPattern(ctx, this, inverse, false)
1400+
? fillColor.getPattern(ctx, this, inverse, PathType.FILL)
14241401
: fillColor;
14251402

14261403
fillCtx.fillRect(0, 0, width, height);
@@ -1772,7 +1749,8 @@ class CanvasGraphics {
17721749
ctx.strokeStyle = strokeColor.getPattern(
17731750
ctx,
17741751
this,
1775-
ctx.mozCurrentTransformInverse
1752+
ctx.mozCurrentTransformInverse,
1753+
PathType.STROKE
17761754
);
17771755
// Prevent drawing too thin lines by enforcing a minimum line width.
17781756
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth);
@@ -1819,7 +1797,8 @@ class CanvasGraphics {
18191797
ctx.fillStyle = fillColor.getPattern(
18201798
ctx,
18211799
this,
1822-
ctx.mozCurrentTransformInverse
1800+
ctx.mozCurrentTransformInverse,
1801+
PathType.FILL
18231802
);
18241803
needRestore = true;
18251804
}
@@ -2154,7 +2133,8 @@ class CanvasGraphics {
21542133
const pattern = current.fillColor.getPattern(
21552134
ctx,
21562135
this,
2157-
ctx.mozCurrentTransformInverse
2136+
ctx.mozCurrentTransformInverse,
2137+
PathType.FILL
21582138
);
21592139
patternTransform = ctx.mozCurrentTransform;
21602140
ctx.restore();
@@ -2427,10 +2407,7 @@ class CanvasGraphics {
24272407
if (this.cachedPatterns.has(objId)) {
24282408
pattern = this.cachedPatterns.get(objId);
24292409
} else {
2430-
pattern = getShadingPattern(
2431-
this.objs.get(objId),
2432-
this.cachedCanvasPatterns
2433-
);
2410+
pattern = getShadingPattern(this.objs.get(objId));
24342411
this.cachedPatterns.set(objId, pattern);
24352412
}
24362413
if (matrix) {
@@ -2451,7 +2428,7 @@ class CanvasGraphics {
24512428
ctx,
24522429
this,
24532430
ctx.mozCurrentTransformInverse,
2454-
true
2431+
PathType.SHADING
24552432
);
24562433

24572434
const inv = ctx.mozCurrentTransformInverse;
@@ -2839,7 +2816,7 @@ class CanvasGraphics {
28392816
maskCtx,
28402817
this,
28412818
ctx.mozCurrentTransformInverse,
2842-
false
2819+
PathType.FILL
28432820
)
28442821
: fillColor;
28452822
maskCtx.fillRect(0, 0, width, height);

src/display/pattern_helper.js

+60-38
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ import {
2222
warn,
2323
} from "../shared/util.js";
2424

25+
const PathType = {
26+
FILL: "Fill",
27+
STROKE: "Stroke",
28+
SHADING: "Shading",
29+
};
30+
2531
function applyBoundingBox(ctx, bbox) {
2632
if (!bbox || typeof Path2D === "undefined") {
2733
return;
@@ -46,7 +52,7 @@ class BaseShadingPattern {
4652
}
4753

4854
class RadialAxialShadingPattern extends BaseShadingPattern {
49-
constructor(IR, cachedCanvasPatterns) {
55+
constructor(IR) {
5056
super();
5157
this._type = IR[1];
5258
this._bbox = IR[2];
@@ -56,7 +62,6 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
5662
this._r0 = IR[6];
5763
this._r1 = IR[7];
5864
this.matrix = null;
59-
this.cachedCanvasPatterns = cachedCanvasPatterns;
6065
}
6166

6267
_createGradient(ctx) {
@@ -85,42 +90,59 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
8590
return grad;
8691
}
8792

88-
getPattern(ctx, owner, inverse, shadingFill = false) {
93+
getPattern(ctx, owner, inverse, pathType) {
8994
let pattern;
90-
if (!shadingFill) {
91-
if (this.cachedCanvasPatterns.has(this)) {
92-
pattern = this.cachedCanvasPatterns.get(this);
93-
} else {
94-
const tmpCanvas = owner.cachedCanvases.getCanvas(
95-
"pattern",
96-
owner.ctx.canvas.width,
97-
owner.ctx.canvas.height,
98-
true
99-
);
100-
101-
const tmpCtx = tmpCanvas.context;
102-
tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
103-
tmpCtx.beginPath();
104-
tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
95+
if (pathType === PathType.STROKE || pathType === PathType.FILL) {
96+
const ownerBBox = owner.current.getClippedPathBoundingBox(
97+
pathType,
98+
ctx.mozCurrentTransform
99+
);
100+
// Create a canvas that is only as big as the current path. This doesn't
101+
// allow us to cache the pattern, but it generally creates much smaller
102+
// canvases and saves memory use. See bug 1722807 for an example.
103+
const width = Math.ceil(ownerBBox[2] - ownerBBox[0]) || 1;
104+
const height = Math.ceil(ownerBBox[3] - ownerBBox[1]) || 1;
105+
106+
const tmpCanvas = owner.cachedCanvases.getCanvas(
107+
"pattern",
108+
width,
109+
height,
110+
true
111+
);
105112

106-
tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform);
107-
if (this.matrix) {
108-
tmpCtx.transform.apply(tmpCtx, this.matrix);
109-
}
110-
applyBoundingBox(tmpCtx, this._bbox);
113+
const tmpCtx = tmpCanvas.context;
114+
tmpCtx.clearRect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
115+
tmpCtx.beginPath();
116+
tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);
117+
// Non shading fill patterns are positioned relative to the base transform
118+
// (usually the page's initial transform), but we may have created a
119+
// smaller canvas based on the path, so we must account for the shift.
120+
tmpCtx.translate(-ownerBBox[0], -ownerBBox[1]);
121+
inverse = Util.transform(inverse, [
122+
1,
123+
0,
124+
0,
125+
1,
126+
ownerBBox[0],
127+
ownerBBox[1],
128+
]);
129+
130+
tmpCtx.transform.apply(tmpCtx, owner.baseTransform);
131+
if (this.matrix) {
132+
tmpCtx.transform.apply(tmpCtx, this.matrix);
133+
}
134+
applyBoundingBox(tmpCtx, this._bbox);
111135

112-
tmpCtx.fillStyle = this._createGradient(tmpCtx);
113-
tmpCtx.fill();
136+
tmpCtx.fillStyle = this._createGradient(tmpCtx);
137+
tmpCtx.fill();
114138

115-
pattern = ctx.createPattern(tmpCanvas.canvas, "repeat");
116-
this.cachedCanvasPatterns.set(this, pattern);
117-
}
139+
pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
118140
} else {
119141
// Don't bother caching gradients, they are quick to rebuild.
120142
applyBoundingBox(ctx, this._bbox);
121143
pattern = this._createGradient(ctx);
122144
}
123-
if (!shadingFill) {
145+
if (pathType !== PathType.SHADING) {
124146
const domMatrix = new DOMMatrix(inverse);
125147
try {
126148
pattern.setTransform(domMatrix);
@@ -382,10 +404,10 @@ class MeshShadingPattern extends BaseShadingPattern {
382404
};
383405
}
384406

385-
getPattern(ctx, owner, inverse, shadingFill = false) {
407+
getPattern(ctx, owner, inverse, pathType) {
386408
applyBoundingBox(ctx, this._bbox);
387409
let scale;
388-
if (shadingFill) {
410+
if (pathType === PathType.SHADING) {
389411
scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
390412
} else {
391413
// Obtain scale from matrix and current transformation matrix.
@@ -400,11 +422,11 @@ class MeshShadingPattern extends BaseShadingPattern {
400422
// might cause OOM.
401423
const temporaryPatternCanvas = this._createMeshCanvas(
402424
scale,
403-
shadingFill ? null : this._background,
425+
pathType === PathType.SHADING ? null : this._background,
404426
owner.cachedCanvases
405427
);
406428

407-
if (!shadingFill) {
429+
if (pathType !== PathType.SHADING) {
408430
ctx.setTransform.apply(ctx, owner.baseTransform);
409431
if (this.matrix) {
410432
ctx.transform.apply(ctx, this.matrix);
@@ -427,10 +449,10 @@ class DummyShadingPattern extends BaseShadingPattern {
427449
}
428450
}
429451

430-
function getShadingPattern(IR, cachedCanvasPatterns) {
452+
function getShadingPattern(IR) {
431453
switch (IR[0]) {
432454
case "RadialAxial":
433-
return new RadialAxialShadingPattern(IR, cachedCanvasPatterns);
455+
return new RadialAxialShadingPattern(IR);
434456
case "Mesh":
435457
return new MeshShadingPattern(IR);
436458
case "Dummy":
@@ -621,10 +643,10 @@ class TilingPattern {
621643
}
622644
}
623645

624-
getPattern(ctx, owner, inverse, shadingFill = false) {
646+
getPattern(ctx, owner, inverse, pathType) {
625647
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
626648
let matrix = inverse;
627-
if (!shadingFill) {
649+
if (pathType !== PathType.SHADING) {
628650
matrix = Util.transform(matrix, owner.baseTransform);
629651
if (this.matrix) {
630652
matrix = Util.transform(matrix, this.matrix);
@@ -657,4 +679,4 @@ class TilingPattern {
657679
}
658680
}
659681

660-
export { getShadingPattern, TilingPattern };
682+
export { getShadingPattern, PathType, TilingPattern };

0 commit comments

Comments
 (0)