Merge pull request #17574 from calixteman/improve_bezier_bbox

Reduce memory use and improve perfs when computing the bounding box of a bezier curve (bug 1875547)
This commit is contained in:
calixteman 2024-01-25 10:07:48 +01:00 committed by GitHub
commit bf92360095
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 103 deletions

View File

@ -1386,17 +1386,17 @@ class PartialEvaluator {
const y = args[1] + args[3]; const y = args[1] + args[3];
minMax = [ minMax = [
Math.min(args[0], x), Math.min(args[0], x),
Math.max(args[0], x),
Math.min(args[1], y), Math.min(args[1], y),
Math.max(args[0], x),
Math.max(args[1], y), Math.max(args[1], y),
]; ];
break; break;
case OPS.moveTo: case OPS.moveTo:
case OPS.lineTo: case OPS.lineTo:
minMax = [args[0], args[0], args[1], args[1]]; minMax = [args[0], args[1], args[0], args[1]];
break; break;
default: default:
minMax = [Infinity, -Infinity, Infinity, -Infinity]; minMax = [Infinity, Infinity, -Infinity, -Infinity];
break; break;
} }
operatorList.addOp(OPS.constructPath, [[fn], args, minMax]); operatorList.addOp(OPS.constructPath, [[fn], args, minMax]);
@ -1420,15 +1420,15 @@ class PartialEvaluator {
const x = args[0] + args[2]; const x = args[0] + args[2];
const y = args[1] + args[3]; const y = args[1] + args[3];
minMax[0] = Math.min(minMax[0], args[0], x); minMax[0] = Math.min(minMax[0], args[0], x);
minMax[1] = Math.max(minMax[1], args[0], x); minMax[1] = Math.min(minMax[1], args[1], y);
minMax[2] = Math.min(minMax[2], args[1], y); minMax[2] = Math.max(minMax[2], args[0], x);
minMax[3] = Math.max(minMax[3], args[1], y); minMax[3] = Math.max(minMax[3], args[1], y);
break; break;
case OPS.moveTo: case OPS.moveTo:
case OPS.lineTo: case OPS.lineTo:
minMax[0] = Math.min(minMax[0], args[0]); minMax[0] = Math.min(minMax[0], args[0]);
minMax[1] = Math.max(minMax[1], args[0]); minMax[1] = Math.min(minMax[1], args[1]);
minMax[2] = Math.min(minMax[2], args[1]); minMax[2] = Math.max(minMax[2], args[0]);
minMax[3] = Math.max(minMax[3], args[1]); minMax[3] = Math.max(minMax[3], args[1]);
break; break;
} }

View File

@ -529,18 +529,14 @@ class CanvasExtraState {
updateScalingPathMinMax(transform, minMax) { updateScalingPathMinMax(transform, minMax) {
Util.scaleMinMax(transform, minMax); Util.scaleMinMax(transform, minMax);
this.minX = Math.min(this.minX, minMax[0]); this.minX = Math.min(this.minX, minMax[0]);
this.maxX = Math.max(this.maxX, minMax[1]); this.minY = Math.min(this.minY, minMax[1]);
this.minY = Math.min(this.minY, minMax[2]); this.maxX = Math.max(this.maxX, minMax[2]);
this.maxY = Math.max(this.maxY, minMax[3]); this.maxY = Math.max(this.maxY, minMax[3]);
} }
updateCurvePathMinMax(transform, x0, y0, x1, y1, x2, y2, x3, y3, minMax) { updateCurvePathMinMax(transform, x0, y0, x1, y1, x2, y2, x3, y3, minMax) {
const box = Util.bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3); const box = Util.bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3, minMax);
if (minMax) { if (minMax) {
minMax[0] = Math.min(minMax[0], box[0], box[2]);
minMax[1] = Math.max(minMax[1], box[0], box[2]);
minMax[2] = Math.min(minMax[2], box[1], box[3]);
minMax[3] = Math.max(minMax[3], box[1], box[3]);
return; return;
} }
this.updateRectMinMax(transform, box); this.updateRectMinMax(transform, box);

View File

@ -651,52 +651,52 @@ class Util {
// Apply a scaling matrix to some min/max values. // Apply a scaling matrix to some min/max values.
// If a scaling factor is negative then min and max must be // If a scaling factor is negative then min and max must be
// swaped. // swapped.
static scaleMinMax(transform, minMax) { static scaleMinMax(transform, minMax) {
let temp; let temp;
if (transform[0]) { if (transform[0]) {
if (transform[0] < 0) { if (transform[0] < 0) {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
}
minMax[0] *= transform[0];
minMax[1] *= transform[0];
if (transform[3] < 0) {
temp = minMax[2];
minMax[2] = minMax[3];
minMax[3] = temp;
}
minMax[2] *= transform[3];
minMax[3] *= transform[3];
} else {
temp = minMax[0]; temp = minMax[0];
minMax[0] = minMax[2]; minMax[0] = minMax[2];
minMax[2] = temp; minMax[2] = temp;
}
minMax[0] *= transform[0];
minMax[2] *= transform[0];
if (transform[3] < 0) {
temp = minMax[1]; temp = minMax[1];
minMax[1] = minMax[3]; minMax[1] = minMax[3];
minMax[3] = temp; minMax[3] = temp;
}
if (transform[1] < 0) { minMax[1] *= transform[3];
minMax[3] *= transform[3];
} else {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
temp = minMax[2]; temp = minMax[2];
minMax[2] = minMax[3]; minMax[2] = minMax[3];
minMax[3] = temp; minMax[3] = temp;
if (transform[1] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
} }
minMax[2] *= transform[1]; minMax[1] *= transform[1];
minMax[3] *= transform[1]; minMax[3] *= transform[1];
if (transform[2] < 0) { if (transform[2] < 0) {
temp = minMax[0]; temp = minMax[0];
minMax[0] = minMax[1]; minMax[0] = minMax[2];
minMax[1] = temp; minMax[2] = temp;
} }
minMax[0] *= transform[2]; minMax[0] *= transform[2];
minMax[1] *= transform[2]; minMax[2] *= transform[2];
} }
minMax[0] += transform[4]; minMax[0] += transform[4];
minMax[1] += transform[4]; minMax[1] += transform[5];
minMax[2] += transform[5]; minMax[2] += transform[4];
minMax[3] += transform[5]; minMax[3] += transform[5];
} }
@ -822,77 +822,117 @@ class Util {
return [xLow, yLow, xHigh, yHigh]; return [xLow, yLow, xHigh, yHigh];
} }
// From https://github.com/adobe-webplatform/Snap.svg/blob/b365287722a72526000ac4bfcf0ce4cac2faa015/src/path.js#L852 static #getExtremumOnCurve(x0, x1, x2, x3, y0, y1, y2, y3, t, minMax) {
static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3) { if (t <= 0 || t >= 1) {
const tvalues = [], return;
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;
} }
const mt = 1 - t;
const tt = t * t;
const ttt = tt * t;
const x = mt * (mt * (mt * x0 + 3 * t * x1) + 3 * tt * x2) + ttt * x3;
const y = mt * (mt * (mt * y0 + 3 * t * y1) + 3 * tt * y2) + ttt * y3;
minMax[0] = Math.min(minMax[0], x);
minMax[1] = Math.min(minMax[1], y);
minMax[2] = Math.max(minMax[2], x);
minMax[3] = Math.max(minMax[3], y);
}
static #getExtremum(x0, x1, x2, x3, y0, y1, y2, y3, a, b, c, minMax) {
if (Math.abs(a) < 1e-12) { if (Math.abs(a) < 1e-12) {
if (Math.abs(b) < 1e-12) { if (Math.abs(b) >= 1e-12) {
continue; this.#getExtremumOnCurve(
} x0,
t = -c / b; x1,
if (0 < t && t < 1) { x2,
tvalues.push(t); x3,
} y0,
continue; y1,
} y2,
b2ac = b * b - 4 * c * a; y3,
sqrtb2ac = Math.sqrt(b2ac); -c / b,
if (b2ac < 0) { minMax
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);
} }
return;
} }
let j = tvalues.length, const delta = b ** 2 - 4 * c * a;
mt; if (delta < 0) {
const jlen = j; return;
while (j--) { }
t = tvalues[j]; const sqrtDelta = Math.sqrt(delta);
mt = 1 - t; const a2 = 2 * a;
bounds[0][j] = this.#getExtremumOnCurve(
mt * mt * mt * x0 + x0,
3 * mt * mt * t * x1 + x1,
3 * mt * t * t * x2 + x2,
t * t * t * x3; x3,
bounds[1][j] = y0,
mt * mt * mt * y0 + y1,
3 * mt * mt * t * y1 + y2,
3 * mt * t * t * y2 + y3,
t * t * t * y3; (-b + sqrtDelta) / a2,
minMax
);
this.#getExtremumOnCurve(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
(-b - sqrtDelta) / a2,
minMax
);
} }
bounds[0][jlen] = x0; // From https://github.com/adobe-webplatform/Snap.svg/blob/b365287722a72526000ac4bfcf0ce4cac2faa015/src/path.js#L852
bounds[1][jlen] = y0; static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3, minMax) {
bounds[0][jlen + 1] = x3; if (minMax) {
bounds[1][jlen + 1] = y3; minMax[0] = Math.min(minMax[0], x0, x3);
bounds[0].length = bounds[1].length = jlen + 2; minMax[1] = Math.min(minMax[1], y0, y3);
minMax[2] = Math.max(minMax[2], x0, x3);
return [ minMax[3] = Math.max(minMax[3], y0, y3);
Math.min(...bounds[0]), } else {
Math.min(...bounds[1]), minMax = [
Math.max(...bounds[0]), Math.min(x0, x3),
Math.max(...bounds[1]), Math.min(y0, y3),
Math.max(x0, x3),
Math.max(y0, y3),
]; ];
} }
this.#getExtremum(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
3 * (-x0 + 3 * (x1 - x2) + x3),
6 * (x0 - 2 * x1 + x2),
3 * (x1 - x0),
minMax
);
this.#getExtremum(
x0,
x1,
x2,
x3,
y0,
y1,
y2,
y3,
3 * (-y0 + 3 * (y1 - y2) + y3),
6 * (y0 - 2 * y1 + y2),
3 * (y1 - y0),
minMax
);
return minMax;
}
} }
const PDFStringTranslateTable = [ const PDFStringTranslateTable = [

View File

@ -4548,7 +4548,7 @@ describe("annotation", function () {
expect(opList.argsArray[5][0]).toEqual([OPS.moveTo, OPS.curveTo]); expect(opList.argsArray[5][0]).toEqual([OPS.moveTo, OPS.curveTo]);
expect(opList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); expect(opList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
// Min-max. // Min-max.
expect(opList.argsArray[5][2]).toEqual([1, 1, 2, 2]); expect(opList.argsArray[5][2]).toEqual([1, 2, 1, 2]);
}); });
}); });

View File

@ -771,7 +771,7 @@ describe("api", function () {
[ [
[OPS.moveTo, OPS.lineTo], [OPS.moveTo, OPS.lineTo],
[0, 9.75, 0.5, 9.75], [0, 9.75, 0.5, 9.75],
[0, 0.5, 9.75, 9.75], [0, 9.75, 0.5, 9.75],
], ],
null, null,
]); ]);