diff --git a/src/core/annotation.js b/src/core/annotation.js index 55762a939..4fc2d2aac 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -623,10 +623,9 @@ function getQuadPoints(dict, rect) { function getTransformMatrix(rect, bbox, matrix) { // 12.5.5: Algorithm: Appearance streams - const [minX, minY, maxX, maxY] = Util.getAxialAlignedBoundingBox( - bbox, - matrix - ); + const minMax = new Float32Array([Infinity, Infinity, -Infinity, -Infinity]); + Util.axialAlignedBoundingBox(bbox, matrix, minMax); + const [minX, minY, maxX, maxY] = minMax; if (minX === maxX || minY === maxY) { // From real-life file, bbox was [0, 0, 0, 0]. In this case, // just apply the transform for rect diff --git a/src/display/canvas.js b/src/display/canvas.js index 694aec0a6..8fc6e8d7d 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -63,6 +63,14 @@ const SCALE_MATRIX = new DOMMatrix(); // Used to get some coordinates. const XY = new Float32Array(2); +// Initial rectangle values for the minMax array. +const MIN_MAX_INIT = new Float32Array([ + Infinity, + Infinity, + -Infinity, + -Infinity, +]); + /** * Overrides certain methods on a 2d ctx so that when they are called they * will also call the same method on the destCtx. The methods that are @@ -330,40 +338,19 @@ class CanvasExtraState { this.activeSMask = null; this.transferMaps = "none"; - this.startNewPathAndClipBox([0, 0, width, height]); + this.clipBox = new Float32Array([0, 0, width, height]); + this.minMax = MIN_MAX_INIT.slice(); } clone() { const clone = Object.create(this); clone.clipBox = this.clipBox.slice(); + clone.minMax = this.minMax.slice(); return clone; } - updateRectMinMax([m0, m1, m2, m3, m4, m5], [r0, r1, r2, r3]) { - const m0r0m4 = m0 * r0 + m4; - const m0r2m4 = m0 * r2 + m4; - const m1r0m5 = m1 * r0 + m5; - const m1r2m5 = m1 * r2 + m5; - const m2r1 = m2 * r1; - const m2r3 = m2 * r3; - const m3r1 = m3 * r1; - const m3r3 = m3 * r3; - const a0 = m0r0m4 + m2r1; - const a1 = m0r2m4 + m2r3; - const a2 = m0r0m4 + m2r3; - const a3 = m0r2m4 + m2r1; - const b0 = m1r0m5 + m3r1; - const b1 = m1r2m5 + m3r3; - const b2 = m1r0m5 + m3r3; - const b3 = m1r2m5 + m3r1; - this.minX = Math.min(this.minX, a0, a1, a2, a3); - this.maxX = Math.max(this.maxX, a0, a1, a2, a3); - this.minY = Math.min(this.minY, b0, b1, b2, b3); - this.maxY = Math.max(this.maxY, b0, b1, b2, b3); - } - getPathBoundingBox(pathType = PathType.FILL, transform = null) { - const box = [this.minX, this.minY, this.maxX, this.maxY]; + const box = this.minMax.slice(); if (pathType === PathType.STROKE) { if (!transform) { unreachable("Stroke bounding box must include transform."); @@ -387,15 +374,12 @@ class CanvasExtraState { } isEmptyClip() { - return this.minX === Infinity; + return this.minMax[0] === Infinity; } startNewPathAndClipBox(box) { - this.clipBox = box; - this.minX = Infinity; - this.minY = Infinity; - this.maxX = 0; - this.maxY = 0; + this.clipBox.set(box, 0); + this.minMax.set(MIN_MAX_INIT, 0); } getClippedPathBoundingBox(pathType = PathType.FILL, transform = null) { @@ -1014,10 +998,9 @@ class CanvasGraphics { 0, ]); maskToCanvas = Util.transform(maskToCanvas, [1, 0, 0, 1, 0, -height]); - const [minX, minY, maxX, maxY] = Util.getAxialAlignedBoundingBox( - [0, 0, width, height], - maskToCanvas - ); + const minMax = MIN_MAX_INIT.slice(); + Util.axialAlignedBoundingBox([0, 0, width, height], maskToCanvas, minMax); + const [minX, minY, maxX, maxY] = minMax; const drawnWidth = Math.round(maxX - minX) || 1; const drawnHeight = Math.round(maxY - minY) || 1; const fillCanvas = this.cachedCanvases.getCanvas( @@ -1458,7 +1441,11 @@ class CanvasGraphics { } path = path2d; } - this.current.updateRectMinMax(getCurrentTransform(this.ctx), minMax); + Util.axialAlignedBoundingBox( + minMax, + getCurrentTransform(this.ctx), + this.current.minMax + ); this[op](path); } @@ -2240,10 +2227,9 @@ class CanvasGraphics { const inv = getCurrentTransformInverse(ctx); if (inv) { const { width, height } = ctx.canvas; - const [x0, y0, x1, y1] = Util.getAxialAlignedBoundingBox( - [0, 0, width, height], - inv - ); + const minMax = MIN_MAX_INIT.slice(); + Util.axialAlignedBoundingBox([0, 0, width, height], inv, minMax); + const [x0, y0, x1, y1] = minMax; this.ctx.fillRect(x0, y0, x1 - x0, y1 - y0); } else { @@ -2282,7 +2268,11 @@ class CanvasGraphics { this.baseTransform = getCurrentTransform(this.ctx); if (bbox) { - this.current.updateRectMinMax(this.baseTransform, bbox); + Util.axialAlignedBoundingBox( + bbox, + this.baseTransform, + this.current.minMax + ); const [x0, y0, x1, y1] = bbox; const clip = new Path2D(); clip.rect(x0, y0, x1 - x0, y1 - y0); @@ -2346,10 +2336,13 @@ class CanvasGraphics { // Based on the current transform figure out how big the bounding box // will actually be. - let bounds = Util.getAxialAlignedBoundingBox( + let bounds = MIN_MAX_INIT.slice(); + Util.axialAlignedBoundingBox( group.bbox, - getCurrentTransform(currentCtx) + getCurrentTransform(currentCtx), + bounds ); + // Clip the bounding box to the current canvas. const canvasBounds = [ 0, @@ -2448,9 +2441,11 @@ class CanvasGraphics { this.restore(); this.ctx.save(); this.ctx.setTransform(...currentMtx); - const dirtyBox = Util.getAxialAlignedBoundingBox( + const dirtyBox = MIN_MAX_INIT.slice(); + Util.axialAlignedBoundingBox( [0, 0, groupCtx.canvas.width, groupCtx.canvas.height], - currentMtx + currentMtx, + dirtyBox ); this.ctx.drawImage(groupCtx.canvas, 0, 0); this.ctx.restore(); diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index d3aa1d561..6e0d39588 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -680,12 +680,11 @@ class TilingPattern { const bboxWidth = x1 - x0; const bboxHeight = y1 - y0; graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); - graphics.current.updateRectMinMax(getCurrentTransform(graphics.ctx), [ - x0, - y0, - x1, - y1, - ]); + Util.axialAlignedBoundingBox( + [x0, y0, x1, y1], + getCurrentTransform(graphics.ctx), + graphics.current.minMax + ); graphics.clip(); graphics.endPath(); } diff --git a/src/shared/util.js b/src/shared/util.js index 3e3c66409..013bdfffa 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -742,13 +742,20 @@ class Util { // For 2d affine transforms static applyTransform(p, m) { - const [p0, p1] = p; + const p0 = p[0]; + const p1 = p[1]; p[0] = p0 * m[0] + p1 * m[2] + m[4]; p[1] = p0 * m[1] + p1 * m[3] + m[5]; } // For 2d affine transforms - static applyTransformToBezier(p, [m0, m1, m2, m3, m4, m5]) { + static applyTransformToBezier(p, transform) { + const m0 = transform[0]; + const m1 = transform[1]; + const m2 = transform[2]; + const m3 = transform[3]; + const m4 = transform[4]; + const m5 = transform[5]; for (let i = 0; i < 6; i += 2) { const pI = p[i]; const pI1 = p[i + 1]; @@ -758,7 +765,8 @@ class Util { } static applyInverseTransform(p, m) { - const [p0, p1] = p; + const p0 = p[0]; + const p1 = p[1]; const d = m[0] * m[3] - m[1] * m[2]; p[0] = (p0 * m[3] - p1 * m[2] + m[2] * m[5] - m[4] * m[3]) / d; p[1] = (-p0 * m[1] + p1 * m[0] + m[4] * m[1] - m[5] * m[0]) / d; @@ -766,21 +774,47 @@ class Util { // Applies the transform to the rectangle and finds the minimum axially // aligned bounding box. - static getAxialAlignedBoundingBox(r, m) { - const p1 = [r[0], r[1]]; - Util.applyTransform(p1, m); - const p2 = [r[2], r[3]]; - Util.applyTransform(p2, m); - const p3 = [r[0], r[3]]; - Util.applyTransform(p3, m); - const p4 = [r[2], r[1]]; - Util.applyTransform(p4, m); - return [ - Math.min(p1[0], p2[0], p3[0], p4[0]), - Math.min(p1[1], p2[1], p3[1], p4[1]), - Math.max(p1[0], p2[0], p3[0], p4[0]), - Math.max(p1[1], p2[1], p3[1], p4[1]), - ]; + static axialAlignedBoundingBox(rect, transform, output) { + const m0 = transform[0]; + const m1 = transform[1]; + const m2 = transform[2]; + const m3 = transform[3]; + const m4 = transform[4]; + const m5 = transform[5]; + const r0 = rect[0]; + const r1 = rect[1]; + const r2 = rect[2]; + const r3 = rect[3]; + + let a0 = m0 * r0 + m4; + let a2 = a0; + let a1 = m0 * r2 + m4; + let a3 = a1; + let b0 = m3 * r1 + m5; + let b2 = b0; + let b1 = m3 * r3 + m5; + let b3 = b1; + + if (m1 !== 0 || m2 !== 0) { + // Non-scaling matrix: shouldn't be frequent. + const m1r0 = m1 * r0; + const m1r2 = m1 * r2; + const m2r1 = m2 * r1; + const m2r3 = m2 * r3; + a0 += m2r1; + a3 += m2r1; + a1 += m2r3; + a2 += m2r3; + b0 += m1r0; + b3 += m1r0; + b1 += m1r2; + b2 += m1r2; + } + + output[0] = Math.min(output[0], a0, a1, a2, a3); + output[1] = Math.min(output[1], b0, b1, b2, b3); + output[2] = Math.max(output[2], a0, a1, a2, a3); + output[3] = Math.max(output[3], b0, b1, b2, b3); } static inverseTransform(m) { @@ -798,7 +832,11 @@ class Util { // This calculation uses Singular Value Decomposition. // The SVD can be represented with formula A = USV. We are interested in the // matrix S here because it represents the scale values. - static singularValueDecompose2dScale([m0, m1, m2, m3], output) { + static singularValueDecompose2dScale(matrix, output) { + const m0 = matrix[0]; + const m1 = matrix[1]; + const m2 = matrix[2]; + const m3 = matrix[3]; // Multiply matrix m with its transpose. const a = m0 ** 2 + m1 ** 2; const b = m0 * m2 + m1 * m3;