diff --git a/src/core/pattern.js b/src/core/pattern.js index 68042e58ddfd5..6059fdfcf8f4a 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -163,19 +163,6 @@ class RadialAxialShading extends BaseShading { [extendStart, extendEnd] = extendArr; } - if ( - this.shadingType === ShadingType.RADIAL && - (!extendStart || !extendEnd) - ) { - // Radial gradient only currently works if either circle is fully within - // the other circle. - const [x1, y1, r1, x2, y2, r2] = this.coordsArr; - const distance = Math.hypot(x1 - x2, y1 - y2); - if (r1 <= r2 + distance && r2 <= r1 + distance) { - warn("Unsupported radial gradient."); - } - } - this.extendStart = extendStart; this.extendEnd = extendEnd; diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index d460de365637e..5a8da30a57b64 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -83,6 +83,23 @@ class RadialAxialShadingPattern extends BaseShadingPattern { return this._type === "radial"; } + // Returns true when the smaller circle's center (p0 when r0 ≤ r1) lies + // outside the larger circle. In that case the canvas radial gradient picks + // t > 1 solutions for points inside the outer circle and maps them to the + // transparent stop we append for extendEnd=false, making the gradient + // invisible. A two-pass draw (reversed first, normal on top) fixes this + // (see #20851). + _isCircleCenterOutside() { + if (!this.isRadial() || this._r0 > this._r1) { + return false; + } + const dist = Math.hypot( + this._p0[0] - this._p1[0], + this._p0[1] - this._p1[1] + ); + return dist > this._r1; + } + _createGradient(ctx, transform = null) { let grad; let firstPoint = this._p0; @@ -125,6 +142,41 @@ class RadialAxialShadingPattern extends BaseShadingPattern { return grad; } + _createReversedGradient(ctx, transform = null) { + // Swapped circles: (p1, r1) → (p0, r0), with color stops reversed. + let firstPoint = this._p1; + let secondPoint = this._p0; + if (transform) { + firstPoint = firstPoint.slice(); + secondPoint = secondPoint.slice(); + Util.applyTransform(firstPoint, transform); + Util.applyTransform(secondPoint, transform); + } + let r0 = this._r1; + let r1 = this._r0; + if (transform) { + const scale = new Float32Array(2); + Util.singularValueDecompose2dScale(transform, scale); + r0 *= scale[0]; + r1 *= scale[0]; + } + const grad = ctx.createRadialGradient( + firstPoint[0], + firstPoint[1], + r0, + secondPoint[0], + secondPoint[1], + r1 + ); + const reversedStops = this._colorStops + .map(([t, c]) => [1 - t, c]) + .reverse(); + for (const [t, c] of reversedStops) { + grad.addColorStop(t, c); + } + return grad; + } + getPattern(ctx, owner, inverse, pathType) { let pattern; if (pathType === PathType.STROKE || pathType === PathType.FILL) { @@ -193,6 +245,10 @@ class RadialAxialShadingPattern extends BaseShadingPattern { } applyBoundingBox(tmpCtx, this._bbox); + if (this._isCircleCenterOutside()) { + tmpCtx.fillStyle = this._createReversedGradient(tmpCtx); + tmpCtx.fill(); + } tmpCtx.fillStyle = this._createGradient(tmpCtx); tmpCtx.fill(); @@ -203,6 +259,15 @@ class RadialAxialShadingPattern extends BaseShadingPattern { // Shading fills are applied relative to the current matrix which is also // how canvas gradients work, so there's no need to do anything special // here. + if (this._isCircleCenterOutside()) { + // Draw the reversed gradient first so the normal gradient can + // correctly overlay it (see _isCircleCenterOutside for details). + ctx.save(); + applyBoundingBox(ctx, this._bbox); + ctx.fillStyle = this._createReversedGradient(ctx); + ctx.fillRect(-1e10, -1e10, 2e10, 2e10); + ctx.restore(); + } applyBoundingBox(ctx, this._bbox); pattern = this._createGradient(ctx); } diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 3f85ddfda2aa6..8e4b0a508fc9f 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -883,3 +883,4 @@ !nested_outline.pdf !form_two_pages.pdf !outlines_se.pdf +!radial_gradients.pdf diff --git a/test/pdfs/radial_gradients.pdf b/test/pdfs/radial_gradients.pdf new file mode 100644 index 0000000000000..edbf57481f7e3 Binary files /dev/null and b/test/pdfs/radial_gradients.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 9060f2481f8b5..96245b451bed9 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -13079,7 +13079,11 @@ "rounds": 1, "type": "extract", "includePages": [0, 2, 12], - "pageMapping": { "1": 1, "3": 2, "13": 3 } + "pageMapping": { + "1": 1, + "3": 2, + "13": 3 + } }, { "id": "bug900822-encrypted-extract_0", @@ -13088,7 +13092,9 @@ "rounds": 1, "type": "extract", "includePages": [0], - "pageMapping": { "1": 1 } + "pageMapping": { + "1": 1 + } }, { "id": "xfa_bug1998843", @@ -13985,5 +13991,12 @@ "md5": "7072f6763bf2f0d6df14d5fc86962c5a", "rounds": 1, "type": "eq" + }, + { + "id": "radial_gradients", + "file": "pdfs/radial_gradients.pdf", + "md5": "80e8bed66b83928698f008c33de47edd", + "rounds": 1, + "type": "eq" } ]