From 4ff582acbec780a3639f19e78fa8ff2293179073 Mon Sep 17 00:00:00 2001 From: calixteman Date: Tue, 23 Dec 2025 10:35:59 +0100 Subject: [PATCH] Avoid pattern creation with some basic gradients Some gradients are represented as patterns in PDF.js, because they mustn't be affected by the current transform. But in most of the cases, the gradient is attached to the origin and the current transform is very basic (dilatation + orthogonal + translation). In those cases, we can avoid creating the pattern because the gradient is transformed into another gradient when the inverse transform is applied. --- src/display/pattern_helper.js | 76 ++++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 11 deletions(-) diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 6884eca9c..d460de365 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -71,23 +71,51 @@ class RadialAxialShadingPattern extends BaseShadingPattern { this.matrix = null; } - _createGradient(ctx) { + isOriginBased() { + return ( + this._p0[0] === 0 && + this._p0[1] === 0 && + (!this.isRadial() || (this._p1[0] === 0 && this._p1[1] === 0)) + ); + } + + isRadial() { + return this._type === "radial"; + } + + _createGradient(ctx, transform = null) { let grad; + let firstPoint = this._p0; + let secondPoint = this._p1; + if (transform) { + firstPoint = firstPoint.slice(); + secondPoint = secondPoint.slice(); + Util.applyTransform(firstPoint, transform); + Util.applyTransform(secondPoint, transform); + } if (this._type === "axial") { grad = ctx.createLinearGradient( - this._p0[0], - this._p0[1], - this._p1[0], - this._p1[1] + firstPoint[0], + firstPoint[1], + secondPoint[0], + secondPoint[1] ); } else if (this._type === "radial") { + let r0 = this._r0; + let r1 = this._r1; + if (transform) { + const scale = new Float32Array(2); + Util.singularValueDecompose2dScale(transform, scale); + r0 *= scale[0]; + r1 *= scale[0]; + } grad = ctx.createRadialGradient( - this._p0[0], - this._p0[1], - this._r0, - this._p1[0], - this._p1[1], - this._r1 + firstPoint[0], + firstPoint[1], + r0, + secondPoint[0], + secondPoint[1], + r1 ); } @@ -100,6 +128,32 @@ class RadialAxialShadingPattern extends BaseShadingPattern { getPattern(ctx, owner, inverse, pathType) { let pattern; if (pathType === PathType.STROKE || pathType === PathType.FILL) { + if (this.isOriginBased()) { + let transf = Util.transform(inverse, owner.baseTransform); + if (this.matrix) { + transf = Util.transform(transf, this.matrix); + } + const precision = 1e-3; + const n1 = Math.hypot(transf[0], transf[1]); + const n2 = Math.hypot(transf[2], transf[3]); + const ps = (transf[0] * transf[2] + transf[1] * transf[3]) / (n1 * n2); + if (Math.abs(ps) < precision) { + // The images of the basis vectors are orthogonal. + if (this.isRadial()) { + // If the images of the basis vectors are a square then the + // circles are transformed to circles and we can use a gradient + // directly. + if (Math.abs(n1 - n2) < precision) { + return this._createGradient(ctx, transf); + } + } else { + // The rectangles are transformed to rectangles and we can use a + // gradient directly. + return this._createGradient(ctx, transf); + } + } + } + const ownerBBox = owner.current.getClippedPathBoundingBox( pathType, getCurrentTransform(ctx)