Reduce memory use and improve perfs when computing the bounding box of a bezier curve (bug 1875547)

It isn't really a fix for the mentioned bug but it slightly improve things.
In reducing the memory use, the time spent in the GC is reduced either.
The algorithm to compute the bounding box is the same as before but it has just
been rewritten to be more efficient.
This commit is contained in:
Calixte Denizet 2024-01-24 21:16:58 +01:00
parent a5d4660a75
commit 7f2428a77e
5 changed files with 139 additions and 103 deletions

View File

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

View File

@ -529,18 +529,14 @@ class CanvasExtraState {
updateScalingPathMinMax(transform, minMax) {
Util.scaleMinMax(transform, minMax);
this.minX = Math.min(this.minX, minMax[0]);
this.maxX = Math.max(this.maxX, minMax[1]);
this.minY = Math.min(this.minY, minMax[2]);
this.minY = Math.min(this.minY, minMax[1]);
this.maxX = Math.max(this.maxX, minMax[2]);
this.maxY = Math.max(this.maxY, minMax[3]);
}
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) {
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;
}
this.updateRectMinMax(transform, box);

View File

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

View File

@ -4548,7 +4548,7 @@ describe("annotation", function () {
expect(opList.argsArray[5][0]).toEqual([OPS.moveTo, OPS.curveTo]);
expect(opList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
// 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],
[0, 9.75, 0.5, 9.75],
[0, 0.5, 9.75, 9.75],
[0, 9.75, 0.5, 9.75],
],
null,
]);