Track the clipping box and bounding box of the path.

This allows us to compose much smaller regions of soft
mask making them much faster. This should also allow
for further optimizations in the pattern code.

For example locally I see issue #6573 go from 55s
to 5s with this change.

Fixes #6573
This commit is contained in:
Brendan Dahl 2021-10-21 15:22:11 -07:00
parent 2d1f9ff7a3
commit 82681ea20c
2 changed files with 196 additions and 16 deletions

View File

@ -579,7 +579,7 @@ function compileType3Glyph(imgData) {
}
class CanvasExtraState {
constructor() {
constructor(width, height) {
// Are soft masks and alpha values shapes or opacities?
this.alphaIsShape = false;
this.fontSize = 0;
@ -610,16 +610,55 @@ class CanvasExtraState {
this.lineWidth = 1;
this.activeSMask = null;
this.transferMaps = null;
this.startNewPathAndClipBox([0, 0, width, height]);
}
clone() {
return Object.create(this);
const clone = Object.create(this);
clone.clipBox = this.clipBox.slice();
return clone;
}
setCurrentPoint(x, y) {
this.x = x;
this.y = y;
}
updatePathMinMax(transform, x, y) {
[x, y] = Util.applyTransform([x, y], transform);
this.minX = Math.min(this.minX, x);
this.minY = Math.min(this.minY, y);
this.maxX = Math.max(this.maxX, x);
this.maxY = Math.max(this.maxY, y);
}
updateCurvePathMinMax(transform, x0, y0, x1, y1, x2, y2, x3, y3) {
const box = Util.bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3);
this.updatePathMinMax(transform, box[0], box[1]);
this.updatePathMinMax(transform, box[2], box[3]);
}
getPathBoundingBox() {
return [this.minX, this.minY, this.maxX, this.maxY];
}
updateClipFromPath() {
const intersect = Util.intersect(this.clipBox, this.getPathBoundingBox());
this.startNewPathAndClipBox(intersect || [0, 0, 0, 0]);
}
startNewPathAndClipBox(box) {
this.clipBox = box;
this.minX = Infinity;
this.minY = Infinity;
this.maxX = 0;
this.maxY = 0;
}
getClippedPathBoundingBox() {
return Util.intersect(this.clipBox, this.getPathBoundingBox());
}
}
function putBinaryImageData(ctx, imgData, transferMaps = null) {
@ -995,6 +1034,9 @@ function composeSMask(ctx, smask, layerCtx, layerBox) {
const layerOffsetY = layerBox[1];
const layerWidth = layerBox[2] - layerOffsetX;
const layerHeight = layerBox[3] - layerOffsetY;
if (layerWidth === 0 || layerHeight === 0) {
return;
}
genericComposeSMask(
smask.context,
layerCtx,
@ -1051,7 +1093,10 @@ class CanvasGraphics {
optionalContentConfig
) {
this.ctx = canvasCtx;
this.current = new CanvasExtraState();
this.current = new CanvasExtraState(
this.ctx.canvas.width,
this.ctx.canvas.height
);
this.stateStack = [];
this.pendingClip = null;
this.pendingEOFill = false;
@ -1538,8 +1583,14 @@ class CanvasGraphics {
if (!this.current.activeSMask) {
return;
}
if (!dirtyBox) {
dirtyBox = [0, 0, this.ctx.canvas.width, this.ctx.canvas.height];
} else {
dirtyBox[0] = Math.floor(dirtyBox[0]);
dirtyBox[1] = Math.floor(dirtyBox[1]);
dirtyBox[2] = Math.ceil(dirtyBox[2]);
dirtyBox[3] = Math.ceil(dirtyBox[3]);
}
const smask = this.current.activeSMask;
const suspendedCtx = this.suspendedCtx;
@ -1589,6 +1640,7 @@ class CanvasGraphics {
const current = this.current;
let x = current.x,
y = current.y;
let startX, startY;
for (let i = 0, j = 0, ii = ops.length; i < ii; i++) {
switch (ops[i] | 0) {
case OPS.rectangle:
@ -1607,20 +1659,25 @@ class CanvasGraphics {
ctx.lineTo(xw, yh);
ctx.lineTo(x, yh);
}
current.updatePathMinMax(ctx.mozCurrentTransform, x, y);
current.updatePathMinMax(ctx.mozCurrentTransform, xw, yh);
ctx.closePath();
break;
case OPS.moveTo:
x = args[j++];
y = args[j++];
ctx.moveTo(x, y);
current.updatePathMinMax(ctx.mozCurrentTransform, x, y);
break;
case OPS.lineTo:
x = args[j++];
y = args[j++];
ctx.lineTo(x, y);
current.updatePathMinMax(ctx.mozCurrentTransform, x, y);
break;
case OPS.curveTo:
startX = x;
startY = y;
x = args[j + 4];
y = args[j + 5];
ctx.bezierCurveTo(
@ -1631,9 +1688,22 @@ class CanvasGraphics {
x,
y
);
current.updateCurvePathMinMax(
ctx.mozCurrentTransform,
startX,
startY,
args[j],
args[j + 1],
args[j + 2],
args[j + 3],
x,
y
);
j += 6;
break;
case OPS.curveTo2:
startX = x;
startY = y;
ctx.bezierCurveTo(
x,
y,
@ -1642,14 +1712,38 @@ class CanvasGraphics {
args[j + 2],
args[j + 3]
);
current.updateCurvePathMinMax(
ctx.mozCurrentTransform,
startX,
startY,
x,
y,
args[j],
args[j + 1],
args[j + 2],
args[j + 3]
);
x = args[j + 2];
y = args[j + 3];
j += 4;
break;
case OPS.curveTo3:
startX = x;
startY = y;
x = args[j + 2];
y = args[j + 3];
ctx.bezierCurveTo(args[j], args[j + 1], x, y, x, y);
current.updateCurvePathMinMax(
ctx.mozCurrentTransform,
startX,
startY,
args[j],
args[j + 1],
x,
y,
x,
y
);
j += 4;
break;
case OPS.closePath:
@ -1702,7 +1796,7 @@ class CanvasGraphics {
}
}
if (consumePath) {
this.consumePath();
this.consumePath(this.current.getClippedPathBoundingBox());
}
// Restore the global alpha to the fill alpha
ctx.globalAlpha = this.current.fillAlpha;
@ -1730,7 +1824,8 @@ class CanvasGraphics {
needRestore = true;
}
if (this.contentVisible) {
const intersect = this.current.getClippedPathBoundingBox();
if (this.contentVisible && intersect !== null) {
if (this.pendingEOFill) {
ctx.fill("evenodd");
this.pendingEOFill = false;
@ -1743,7 +1838,7 @@ class CanvasGraphics {
ctx.restore();
}
if (consumePath) {
this.consumePath();
this.consumePath(intersect);
}
}
@ -2360,7 +2455,6 @@ class CanvasGraphics {
);
const inv = ctx.mozCurrentTransformInverse;
let dirtyBox = [0, 0, ctx.canvas.width, ctx.canvas.height];
if (inv) {
const canvas = ctx.canvas;
const width = canvas.width;
@ -2375,10 +2469,6 @@ class CanvasGraphics {
const y0 = Math.min(bl[1], br[1], ul[1], ur[1]);
const x1 = Math.max(bl[0], br[0], ul[0], ur[0]);
const y1 = Math.max(bl[1], br[1], ul[1], ur[1]);
dirtyBox = Util.getAxialAlignedBoundingBox(
[x0, y0, x1, y1],
ctx.mozCurrentTransform
);
this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0);
} else {
@ -2391,7 +2481,7 @@ class CanvasGraphics {
this.ctx.fillRect(-1e10, -1e10, 2e10, 2e10);
}
this.compose(dirtyBox);
this.compose(this.current.getClippedPathBoundingBox());
this.restore();
}
@ -2421,6 +2511,16 @@ class CanvasGraphics {
const width = bbox[2] - bbox[0];
const height = bbox[3] - bbox[1];
this.ctx.rect(bbox[0], bbox[1], width, height);
this.current.updatePathMinMax(
this.ctx.mozCurrentTransform,
bbox[0],
bbox[1]
);
this.current.updatePathMinMax(
this.ctx.mozCurrentTransform,
bbox[2],
bbox[3]
);
this.clip();
this.endPath();
}
@ -2511,6 +2611,8 @@ class CanvasGraphics {
drawnHeight = MAX_GROUP_SIZE;
}
this.current.startNewPathAndClipBox([0, 0, drawnWidth, drawnHeight]);
let cacheId = "groupAt" + this.groupLevel;
if (group.smask) {
// Using two cache entries is case if masks are used one after another.
@ -2617,7 +2719,10 @@ class CanvasGraphics {
beginAnnotation(id, rect, transform, matrix) {
this.save();
resetCtxToDefault(this.ctx);
this.current = new CanvasExtraState();
this.current = new CanvasExtraState(
this.ctx.canvas.width,
this.ctx.canvas.height
);
if (Array.isArray(rect) && rect.length === 4) {
const width = rect[2] - rect[0];
@ -2950,9 +3055,12 @@ class CanvasGraphics {
// Helper functions
consumePath() {
consumePath(clipBox) {
if (this.pendingClip) {
this.current.updateClipFromPath();
}
if (!this.pendingClip) {
this.compose();
this.compose(clipBox);
}
const ctx = this.ctx;
if (this.pendingClip) {

View File

@ -872,6 +872,78 @@ class Util {
return result;
}
// From https://github.com/adobe-webplatform/Snap.svg/blob/b365287722a72526000ac4bfcf0ce4cac2faa015/src/path.js#L852
static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3) {
const tvalues = [],
bounds = [[], []];
let a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (let i = 0; i < 2; ++i) {
if (i === 0) {
b = 6 * x0 - 12 * x1 + 6 * x2;
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
c = 3 * x1 - 3 * x0;
} else {
b = 6 * y0 - 12 * y1 + 6 * y2;
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
c = 3 * y1 - 3 * y0;
}
if (Math.abs(a) < 1e-12) {
if (Math.abs(b) < 1e-12) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = Math.sqrt(b2ac);
if (b2ac < 0) {
continue;
}
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
}
let j = tvalues.length,
mt;
const jlen = j;
while (j--) {
t = tvalues[j];
mt = 1 - t;
bounds[0][j] =
mt * mt * mt * x0 +
3 * mt * mt * t * x1 +
3 * mt * t * t * x2 +
t * t * t * x3;
bounds[1][j] =
mt * mt * mt * y0 +
3 * mt * mt * t * y1 +
3 * mt * t * t * y2 +
t * t * t * y3;
}
bounds[0][jlen] = x0;
bounds[1][jlen] = y0;
bounds[0][jlen + 1] = x3;
bounds[1][jlen + 1] = y3;
bounds[0].length = bounds[1].length = jlen + 2;
return [
Math.min(...bounds[0]),
Math.min(...bounds[1]),
Math.max(...bounds[0]),
Math.max(...bounds[1]),
];
}
}
const PDFStringTranslateTable = [