From 7f2428a77e9add2a3348e8759c3c4569c0d0a7a3 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 24 Jan 2024 21:16:58 +0100 Subject: [PATCH] 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. --- src/core/evaluator.js | 14 +-- src/display/canvas.js | 10 +- src/shared/util.js | 214 +++++++++++++++++++++-------------- test/unit/annotation_spec.js | 2 +- test/unit/api_spec.js | 2 +- 5 files changed, 139 insertions(+), 103 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 4ccb5e0b7..5d11b3dff 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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; } diff --git a/src/display/canvas.js b/src/display/canvas.js index 808c9c693..384b9fd51 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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); diff --git a/src/shared/util.js b/src/shared/util.js index 77a1592d5..32e450b59 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -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] = minMax[2]; + minMax[2] = temp; } minMax[0] *= transform[0]; - minMax[1] *= transform[0]; + minMax[2] *= transform[0]; if (transform[3] < 0) { - temp = minMax[2]; - minMax[2] = minMax[3]; + temp = minMax[1]; + minMax[1] = minMax[3]; minMax[3] = temp; } - minMax[2] *= transform[3]; + minMax[1] *= transform[3]; minMax[3] *= transform[3]; } else { temp = minMax[0]; - minMax[0] = minMax[2]; - minMax[2] = temp; - temp = minMax[1]; - minMax[1] = minMax[3]; + minMax[0] = minMax[1]; + minMax[1] = temp; + temp = minMax[2]; + minMax[2] = minMax[3]; minMax[3] = temp; if (transform[1] < 0) { - temp = minMax[2]; - minMax[2] = minMax[3]; + 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,76 +822,116 @@ class Util { return [xLow, yLow, xHigh, yHigh]; } + 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) { + this.#getExtremumOnCurve( + x0, + x1, + x2, + x3, + y0, + y1, + y2, + y3, + -c / b, + minMax + ); + } + return; + } + + 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 + ); + } + // 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); - } + 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), + ]; } - - 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]), - ]; + 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; } } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index e1bbd0d77..98362f4bf 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -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]); }); }); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index ad15bff75..666ada005 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -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, ]);