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

View File

@ -872,6 +872,78 @@ class Util {
return result; 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 = [ const PDFStringTranslateTable = [