Skip to content

Create shading patterns the size of the current path. (bug 1722807) #14230

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 37 additions & 61 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import {
Util,
warn,
} from "../shared/util.js";
import { getShadingPattern, TilingPattern } from "./pattern_helper.js";
import {
getShadingPattern,
PathType,
TilingPattern,
} from "./pattern_helper.js";
import { PixelsPerInch } from "./display_utils.js";

// <canvas> contexts store most of the state we need natively.
Expand All @@ -38,10 +42,6 @@ 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
Expand Down Expand Up @@ -366,46 +366,6 @@ 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([
Expand Down Expand Up @@ -639,8 +599,23 @@ class CanvasExtraState {
this.updatePathMinMax(transform, box[2], box[3]);
}

getPathBoundingBox() {
return [this.minX, this.minY, this.maxX, this.maxY];
getPathBoundingBox(pathType = PathType.FILL, transform = null) {
const box = [this.minX, this.minY, this.maxX, this.maxY];
if (pathType === PathType.STROKE) {
if (!transform) {
unreachable("Stroke bounding box must include transform.");
}
// Stroked paths can be outside of the path bounding box by 1/2 the line
// width.
const scale = Util.singularValueDecompose2dScale(transform);
const xStrokePad = (scale[0] * this.lineWidth) / 2;
const yStrokePad = (scale[1] * this.lineWidth) / 2;
box[0] -= xStrokePad;
box[1] -= yStrokePad;
box[2] += xStrokePad;
box[3] += yStrokePad;
}
return box;
}

updateClipFromPath() {
Expand All @@ -656,8 +631,11 @@ class CanvasExtraState {
this.maxY = 0;
}

getClippedPathBoundingBox() {
return Util.intersect(this.clipBox, this.getPathBoundingBox());
getClippedPathBoundingBox(pathType = PathType.FILL, transform = null) {
return Util.intersect(
this.clipBox,
this.getPathBoundingBox(pathType, transform)
);
}
}

Expand Down Expand Up @@ -1121,7 +1099,6 @@ 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
Expand Down Expand Up @@ -1273,7 +1250,6 @@ class CanvasGraphics {
}

this.cachedCanvases.clear();
this.cachedCanvasPatterns.clear();
this.cachedPatterns.clear();

if (this.imageLayer) {
Expand Down Expand Up @@ -1420,7 +1396,7 @@ class CanvasGraphics {
-offsetY,
]);
fillCtx.fillStyle = isPatternFill
? fillColor.getPattern(ctx, this, inverse, false)
? fillColor.getPattern(ctx, this, inverse, PathType.FILL)
: fillColor;

fillCtx.fillRect(0, 0, width, height);
Expand Down Expand Up @@ -1772,7 +1748,8 @@ class CanvasGraphics {
ctx.strokeStyle = strokeColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
ctx.mozCurrentTransformInverse,
PathType.STROKE
);
// Prevent drawing too thin lines by enforcing a minimum line width.
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth);
Expand Down Expand Up @@ -1819,7 +1796,8 @@ class CanvasGraphics {
ctx.fillStyle = fillColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
ctx.mozCurrentTransformInverse,
PathType.FILL
);
needRestore = true;
}
Expand Down Expand Up @@ -2161,7 +2139,8 @@ class CanvasGraphics {
const pattern = current.fillColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
ctx.mozCurrentTransformInverse,
PathType.FILL
);
patternTransform = ctx.mozCurrentTransform;
ctx.restore();
Expand Down Expand Up @@ -2426,10 +2405,7 @@ class CanvasGraphics {
if (this.cachedPatterns.has(objId)) {
pattern = this.cachedPatterns.get(objId);
} else {
pattern = getShadingPattern(
this.objs.get(objId),
this.cachedCanvasPatterns
);
pattern = getShadingPattern(this.objs.get(objId));
this.cachedPatterns.set(objId, pattern);
}
if (matrix) {
Expand All @@ -2450,7 +2426,7 @@ class CanvasGraphics {
ctx,
this,
ctx.mozCurrentTransformInverse,
true
PathType.SHADING
);

const inv = ctx.mozCurrentTransformInverse;
Expand Down Expand Up @@ -2838,7 +2814,7 @@ class CanvasGraphics {
maskCtx,
this,
ctx.mozCurrentTransformInverse,
false
PathType.FILL
)
: fillColor;
maskCtx.fillRect(0, 0, width, height);
Expand Down
108 changes: 65 additions & 43 deletions src/display/pattern_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import {
warn,
} from "../shared/util.js";

const PathType = {
FILL: "Fill",
STROKE: "Stroke",
SHADING: "Shading",
};

function applyBoundingBox(ctx, bbox) {
if (!bbox || typeof Path2D === "undefined") {
return;
Expand All @@ -46,7 +52,7 @@ class BaseShadingPattern {
}

class RadialAxialShadingPattern extends BaseShadingPattern {
constructor(IR, cachedCanvasPatterns) {
constructor(IR) {
super();
this._type = IR[1];
this._bbox = IR[2];
Expand All @@ -56,7 +62,6 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
this._r0 = IR[6];
this._r1 = IR[7];
this.matrix = null;
this.cachedCanvasPatterns = cachedCanvasPatterns;
}

_createGradient(ctx) {
Expand Down Expand Up @@ -85,42 +90,53 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
return grad;
}

getPattern(ctx, owner, inverse, shadingFill = false) {
getPattern(ctx, owner, inverse, pathType) {
let pattern;
if (!shadingFill) {
if (this.cachedCanvasPatterns.has(this)) {
pattern = this.cachedCanvasPatterns.get(this);
} else {
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 (pathType === PathType.STROKE || pathType === PathType.FILL) {
const ownerBBox = owner.current.getClippedPathBoundingBox(
pathType,
ctx.mozCurrentTransform
) || [0, 0, 0, 0];
// Create a canvas that is only as big as the current path. This doesn't
// allow us to cache the pattern, but it generally creates much smaller
// canvases and saves memory use. See bug 1722807 for an example.
const width = Math.ceil(ownerBBox[2] - ownerBBox[0]) || 1;
const height = Math.ceil(ownerBBox[3] - ownerBBox[1]) || 1;

const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern",
width,
height,
true
);

tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform);
if (this.matrix) {
tmpCtx.transform.apply(tmpCtx, this.matrix);
}
applyBoundingBox(tmpCtx, this._bbox);
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);
// Non shading fill patterns are positioned relative to the base transform
// (usually the page's initial transform), but we may have created a
// smaller canvas based on the path, so we must account for the shift.
tmpCtx.translate(-ownerBBox[0], -ownerBBox[1]);
inverse = Util.transform(inverse, [
1,
0,
0,
1,
ownerBBox[0],
ownerBBox[1],
]);

tmpCtx.transform.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.fillStyle = this._createGradient(tmpCtx);
tmpCtx.fill();

pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
this.cachedCanvasPatterns.set(this, pattern);
}
} else {
// Don't bother caching gradients, they are quick to rebuild.
applyBoundingBox(ctx, this._bbox);
pattern = this._createGradient(ctx);
}
if (!shadingFill) {
pattern = ctx.createPattern(tmpCanvas.canvas, "no-repeat");
const domMatrix = new DOMMatrix(inverse);
try {
pattern.setTransform(domMatrix);
Expand All @@ -129,6 +145,12 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
// and in Node.js (see issue 13724).
warn(`RadialAxialShadingPattern.getPattern: "${ex?.message}".`);
}
} else {
// Shading fills are applied relative to the current matrix which is also
// how canvas gradients work, so there's no need to do anything special
// here.
applyBoundingBox(ctx, this._bbox);
pattern = this._createGradient(ctx);
}
return pattern;
}
Expand Down Expand Up @@ -382,10 +404,10 @@ class MeshShadingPattern extends BaseShadingPattern {
};
}

getPattern(ctx, owner, inverse, shadingFill = false) {
getPattern(ctx, owner, inverse, pathType) {
applyBoundingBox(ctx, this._bbox);
let scale;
if (shadingFill) {
if (pathType === PathType.SHADING) {
scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform);
} else {
// Obtain scale from matrix and current transformation matrix.
Expand All @@ -400,11 +422,11 @@ class MeshShadingPattern extends BaseShadingPattern {
// might cause OOM.
const temporaryPatternCanvas = this._createMeshCanvas(
scale,
shadingFill ? null : this._background,
pathType === PathType.SHADING ? null : this._background,
owner.cachedCanvases
);

if (!shadingFill) {
if (pathType !== PathType.SHADING) {
ctx.setTransform.apply(ctx, owner.baseTransform);
if (this.matrix) {
ctx.transform.apply(ctx, this.matrix);
Expand All @@ -427,10 +449,10 @@ class DummyShadingPattern extends BaseShadingPattern {
}
}

function getShadingPattern(IR, cachedCanvasPatterns) {
function getShadingPattern(IR) {
switch (IR[0]) {
case "RadialAxial":
return new RadialAxialShadingPattern(IR, cachedCanvasPatterns);
return new RadialAxialShadingPattern(IR);
case "Mesh":
return new MeshShadingPattern(IR);
case "Dummy":
Expand Down Expand Up @@ -621,10 +643,10 @@ class TilingPattern {
}
}

getPattern(ctx, owner, inverse, shadingFill = false) {
getPattern(ctx, owner, inverse, pathType) {
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = inverse;
if (!shadingFill) {
if (pathType !== PathType.SHADING) {
matrix = Util.transform(matrix, owner.baseTransform);
if (this.matrix) {
matrix = Util.transform(matrix, this.matrix);
Expand Down Expand Up @@ -657,4 +679,4 @@ class TilingPattern {
}
}

export { getShadingPattern, TilingPattern };
export { getShadingPattern, PathType, TilingPattern };