Skip to content

Commit 8f7a615

Browse files
authored
Merge pull request #20860 from calixteman/radial_gradients
Fix the rendering of the radial gradient when a center is outside of the other circle and there's no extend
2 parents 820b70e + c610f44 commit 8f7a615

5 files changed

Lines changed: 81 additions & 15 deletions

File tree

src/core/pattern.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -163,19 +163,6 @@ class RadialAxialShading extends BaseShading {
163163
[extendStart, extendEnd] = extendArr;
164164
}
165165

166-
if (
167-
this.shadingType === ShadingType.RADIAL &&
168-
(!extendStart || !extendEnd)
169-
) {
170-
// Radial gradient only currently works if either circle is fully within
171-
// the other circle.
172-
const [x1, y1, r1, x2, y2, r2] = this.coordsArr;
173-
const distance = Math.hypot(x1 - x2, y1 - y2);
174-
if (r1 <= r2 + distance && r2 <= r1 + distance) {
175-
warn("Unsupported radial gradient.");
176-
}
177-
}
178-
179166
this.extendStart = extendStart;
180167
this.extendEnd = extendEnd;
181168

src/display/pattern_helper.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,23 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
8383
return this._type === "radial";
8484
}
8585

86+
// Returns true when the smaller circle's center (p0 when r0 ≤ r1) lies
87+
// outside the larger circle. In that case the canvas radial gradient picks
88+
// t > 1 solutions for points inside the outer circle and maps them to the
89+
// transparent stop we append for extendEnd=false, making the gradient
90+
// invisible. A two-pass draw (reversed first, normal on top) fixes this
91+
// (see #20851).
92+
_isCircleCenterOutside() {
93+
if (!this.isRadial() || this._r0 > this._r1) {
94+
return false;
95+
}
96+
const dist = Math.hypot(
97+
this._p0[0] - this._p1[0],
98+
this._p0[1] - this._p1[1]
99+
);
100+
return dist > this._r1;
101+
}
102+
86103
_createGradient(ctx, transform = null) {
87104
let grad;
88105
let firstPoint = this._p0;
@@ -125,6 +142,41 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
125142
return grad;
126143
}
127144

145+
_createReversedGradient(ctx, transform = null) {
146+
// Swapped circles: (p1, r1) → (p0, r0), with color stops reversed.
147+
let firstPoint = this._p1;
148+
let secondPoint = this._p0;
149+
if (transform) {
150+
firstPoint = firstPoint.slice();
151+
secondPoint = secondPoint.slice();
152+
Util.applyTransform(firstPoint, transform);
153+
Util.applyTransform(secondPoint, transform);
154+
}
155+
let r0 = this._r1;
156+
let r1 = this._r0;
157+
if (transform) {
158+
const scale = new Float32Array(2);
159+
Util.singularValueDecompose2dScale(transform, scale);
160+
r0 *= scale[0];
161+
r1 *= scale[0];
162+
}
163+
const grad = ctx.createRadialGradient(
164+
firstPoint[0],
165+
firstPoint[1],
166+
r0,
167+
secondPoint[0],
168+
secondPoint[1],
169+
r1
170+
);
171+
const reversedStops = this._colorStops
172+
.map(([t, c]) => [1 - t, c])
173+
.reverse();
174+
for (const [t, c] of reversedStops) {
175+
grad.addColorStop(t, c);
176+
}
177+
return grad;
178+
}
179+
128180
getPattern(ctx, owner, inverse, pathType) {
129181
let pattern;
130182
if (pathType === PathType.STROKE || pathType === PathType.FILL) {
@@ -193,6 +245,10 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
193245
}
194246
applyBoundingBox(tmpCtx, this._bbox);
195247

248+
if (this._isCircleCenterOutside()) {
249+
tmpCtx.fillStyle = this._createReversedGradient(tmpCtx);
250+
tmpCtx.fill();
251+
}
196252
tmpCtx.fillStyle = this._createGradient(tmpCtx);
197253
tmpCtx.fill();
198254

@@ -203,6 +259,15 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
203259
// Shading fills are applied relative to the current matrix which is also
204260
// how canvas gradients work, so there's no need to do anything special
205261
// here.
262+
if (this._isCircleCenterOutside()) {
263+
// Draw the reversed gradient first so the normal gradient can
264+
// correctly overlay it (see _isCircleCenterOutside for details).
265+
ctx.save();
266+
applyBoundingBox(ctx, this._bbox);
267+
ctx.fillStyle = this._createReversedGradient(ctx);
268+
ctx.fillRect(-1e10, -1e10, 2e10, 2e10);
269+
ctx.restore();
270+
}
206271
applyBoundingBox(ctx, this._bbox);
207272
pattern = this._createGradient(ctx);
208273
}

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,3 +883,4 @@
883883
!nested_outline.pdf
884884
!form_two_pages.pdf
885885
!outlines_se.pdf
886+
!radial_gradients.pdf

test/pdfs/radial_gradients.pdf

11.6 KB
Binary file not shown.

test/test_manifest.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13079,7 +13079,11 @@
1307913079
"rounds": 1,
1308013080
"type": "extract",
1308113081
"includePages": [0, 2, 12],
13082-
"pageMapping": { "1": 1, "3": 2, "13": 3 }
13082+
"pageMapping": {
13083+
"1": 1,
13084+
"3": 2,
13085+
"13": 3
13086+
}
1308313087
},
1308413088
{
1308513089
"id": "bug900822-encrypted-extract_0",
@@ -13088,7 +13092,9 @@
1308813092
"rounds": 1,
1308913093
"type": "extract",
1309013094
"includePages": [0],
13091-
"pageMapping": { "1": 1 }
13095+
"pageMapping": {
13096+
"1": 1
13097+
}
1309213098
},
1309313099
{
1309413100
"id": "xfa_bug1998843",
@@ -13985,5 +13991,12 @@
1398513991
"md5": "7072f6763bf2f0d6df14d5fc86962c5a",
1398613992
"rounds": 1,
1398713993
"type": "eq"
13994+
},
13995+
{
13996+
"id": "radial_gradients",
13997+
"file": "pdfs/radial_gradients.pdf",
13998+
"md5": "80e8bed66b83928698f008c33de47edd",
13999+
"rounds": 1,
14000+
"type": "eq"
1398814001
}
1398914002
]

0 commit comments

Comments
 (0)