From c610f44952dddde2775667f443ddb60cd881bbfc Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 13 Mar 2026 10:20:55 +0100 Subject: [PATCH] Fix the rendering of the radial gradient when a center is outside of the other circle and there's no extend It fixes #20851. --- src/core/pattern.js | 13 ------- src/display/pattern_helper.js | 65 +++++++++++++++++++++++++++++++++ test/pdfs/.gitignore | 1 + test/pdfs/radial_gradients.pdf | Bin 0 -> 11851 bytes test/test_manifest.json | 17 ++++++++- 5 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 test/pdfs/radial_gradients.pdf 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 0000000000000000000000000000000000000000..edbf57481f7e349ecfa2cdbb38ca3665c6a217cd GIT binary patch literal 11851 zcmd5?+iu%B7Jc_u?3V&5(AJ{vDw^}qOFDydPl7WKMITgKc2zi1Msj+YpPYyJgP9+h zwMj{qNGiutjF|?A!_egB;@VeIdi2}ti>Kn5zj*ZT|NQe`3qcN*A3iN!yrAXvx7&=C zuhMuNuObpsBvTTT%fw1;AHP4Q=VbfyC4gh} zmk_yvBwRoj4!O!wwLS88g4|Bk!X;PfdXf@Z@2A@&JKb?c>XF--A8gcB_Gj5+KyIgQ zSgAXvZy1H!DI8YeDUDksCge$+Y#Fc%uL}gnn+78~9cx`2_%qjrT^v}0S&Xs@B$)-c6vso(cK`emAF!8AEY zsuNgVmAkUKx=mNeyRWm)`6|17`;xoMidPdsKp_$fQc%|vyljh_5Y9MYFW-Dc{#(<# zeRY=+e+&@xQ_wU7UZfBpE(cEAoDP6pVkQOP+XV#yn2l94nZZCmeotfoWd;KX1M)$J zYEEaM9}1@dXf+Q4T5}NWpl2t=|B zo2LTN#DRta*1`P< z{s90tj{sm&@PKFt1nvO=H}6L&*f= zECzGe>&4k6F2&Lvy{+(9UZH)-F*hK2G@+|~d0W#{y1-o;y~?ZAF2gs)Cg0vwDIZMh z$8`N^@#Y#AiQ5H&iXY;Ncz6?iy;+%=G!5&#{Y3VVPGmRnZrP(};>bzXleb6hGm*asj{g4wvW zQtKH2_1ThHuVa>5R&==eXqf#g?pUij!+k`teNLNl1+!VfWS~&R?f@XaUju~zg7~C0dINgn^bB9vcq0;Z!sw`wPU(qca=)>G6G!UQ0eHe7(=*ofM`kcvNB#c><3bT+zvcDZNK#-Q>Gn4Ld++;&?R7r>0x91~8IQ9Rs;3LfOEy#54ro zR5301y!et@^%gw!fpp{eK z&Z$w>fj>=v6_L3QoMnuCxDLN`HpJozbFS-}dvkpg zpBn@?Lu&FuWJJ9$b`1Wo1RiM&=Xy?v=PX*ENX$NuU)9sb`GF}fh|O^kznrVhjeWB( z!CO4qI7DHCd*3zn>wBTWi!UFA4%hWVe&1H>i}3tI?~8FN*ZKm-G4>lcmT?Kc$2nXh zj7)q{h$k`HxWtWn``A}$zRN243iwt2j|~2X*Qd*JFW+U8Zv)r+wAxE=6i(ve-SJ0n Ie!W=y7g$-EQ~&?~ literal 0 HcmV?d00001 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" } ]