Skip to content

Commit 8db3cf7

Browse files
fix: apply opacity to currentColor for fill and stroke on Apple platforms (#2923)
# Summary Fixes #1802 on iOS/macOS. When `currentColor` is used as a fill or stroke brush on Apple platforms, the `fillOpacity` and `strokeOpacity` values are ignored — the color is passed directly to `CGContextSetFillColorWithColor`/`CGContextSetStrokeColorWithColor` without applying the opacity multiplier. This fix creates a copy of the color with the correct alpha using `CGColorCreateCopyWithAlpha(currentColor, opacity * CGColorGetAlpha(currentColor))`, and properly releases the created color to avoid memory leaks. Related: #2080 (Android-only opacity fix) Affected files: `RNSVGContextBrush.mm`, `RNSVGRenderable.mm`, `RNSVGTSpan.mm` ## Test Plan Added E2E test case (`e2e/cases/4.svg`) covering all combinations of `currentColor` with `fill-opacity` and `stroke-opacity`: fill only, stroke only, and fill + stroke. ### What's required for testing (prerequisites)? An iOS or macOS device/simulator. ### What are the steps to reproduce (after prerequisites)? Render an SVG element that uses `currentColor` for fill or stroke with `fill-opacity` or `stroke-opacity` set to a value less than 1. Without this fix, the opacity is ignored and the element renders at full opacity. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | MacOS | ✅ | | Android | ❌ | | Web | ❌ | ## Checklist - [x] I have tested this on a device and a simulator - [ ] I added documentation in `README.md` - [ ] I updated the typed files (typescript) - [x] I added a test for the API in the `__tests__` folder ## Screenshots | Before | After | -|- | <img width="320" alt="react-native-svg-current-color-opacity-before" src="https://github.com/user-attachments/assets/edd2336e-abed-4a67-86de-75771927d008" /> | <img width="320" alt="react-native-svg-current-color-opacity-after" src="https://github.com/user-attachments/assets/c5ff6394-48bc-4239-9f2c-42c6588eaacf" /> | --------- Co-authored-by: Jakub Grzywacz <kontakt@jakubgrzywacz.pl>
1 parent 833e601 commit 8db3cf7

8 files changed

Lines changed: 179 additions & 6 deletions

File tree

apple/Brushes/RNSVGContextBrush.mm

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ - (BOOL)applyFillColor:(CGContextRef)context opacity:(CGFloat)opacity
4949
BOOL fillColor;
5050

5151
if (brush.class == RNSVGBrush.class) {
52-
CGContextSetFillColorWithColor(context, [element getCurrentColor]);
52+
CGColorRef currentColor = [element getCurrentColor];
53+
CGColorRef colorWithOpacity = CGColorCreateCopyWithAlpha(currentColor, opacity * CGColorGetAlpha(currentColor));
54+
CGContextSetFillColorWithColor(context, colorWithOpacity);
55+
CGColorRelease(colorWithOpacity);
5356
fillColor = YES;
5457
} else {
5558
fillColor = [brush applyFillColor:context opacity:opacity];
@@ -70,7 +73,10 @@ - (BOOL)applyStrokeColor:(CGContextRef)context opacity:(CGFloat)opacity
7073
BOOL strokeColor;
7174

7275
if (brush.class == RNSVGBrush.class) {
73-
CGContextSetStrokeColorWithColor(context, [element getCurrentColor]);
76+
CGColorRef currentColor = [element getCurrentColor];
77+
CGColorRef colorWithOpacity = CGColorCreateCopyWithAlpha(currentColor, opacity * CGColorGetAlpha(currentColor));
78+
CGContextSetStrokeColorWithColor(context, colorWithOpacity);
79+
CGColorRelease(colorWithOpacity);
7480
strokeColor = YES;
7581
} else {
7682
strokeColor = [brush applyStrokeColor:context opacity:opacity];

apple/RNSVGRenderable.mm

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,11 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
555555

556556
if (self.fill) {
557557
if (self.fill.class == RNSVGBrush.class) {
558-
CGContextSetFillColorWithColor(context, [self getCurrentColor]);
558+
CGColorRef currentColor = [self getCurrentColor];
559+
CGColorRef colorWithOpacity =
560+
CGColorCreateCopyWithAlpha(currentColor, self.fillOpacity * CGColorGetAlpha(currentColor));
561+
CGContextSetFillColorWithColor(context, colorWithOpacity);
562+
CGColorRelease(colorWithOpacity);
559563
fillColor = YES;
560564
} else {
561565
fillColor = [self.fill applyFillColor:context opacity:self.fillOpacity];
@@ -603,7 +607,11 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
603607
BOOL strokeColor;
604608

605609
if (self.stroke.class == RNSVGBrush.class) {
606-
CGContextSetStrokeColorWithColor(context, [self getCurrentColor]);
610+
CGColorRef currentColor = [self getCurrentColor];
611+
CGColorRef colorWithOpacity =
612+
CGColorCreateCopyWithAlpha(currentColor, self.strokeOpacity * CGColorGetAlpha(currentColor));
613+
CGContextSetStrokeColorWithColor(context, colorWithOpacity);
614+
CGColorRelease(colorWithOpacity);
607615
strokeColor = YES;
608616
} else {
609617
strokeColor = [self.stroke applyStrokeColor:context opacity:self.strokeOpacity];

apple/Text/RNSVGTSpan.mm

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,10 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
134134
if (self.inlineSize != nil && self.inlineSize.value != 0) {
135135
if (self.fill) {
136136
if (self.fill.class == RNSVGBrush.class) {
137-
CGColorRef color = [self getCurrentColor];
137+
CGColorRef currentColor = [self getCurrentColor];
138+
CGColorRef color = CGColorCreateCopyWithAlpha(currentColor, self.fillOpacity * CGColorGetAlpha(currentColor));
138139
[self drawWrappedText:context gc:gc rect:rect color:color];
140+
CGColorRelease(color);
139141
} else {
140142
CGColorRef color = [self.fill getColorWithOpacity:self.fillOpacity];
141143
[self drawWrappedText:context gc:gc rect:rect color:color];
@@ -144,8 +146,11 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
144146
}
145147
if (self.stroke) {
146148
if (self.stroke.class == RNSVGBrush.class) {
147-
CGColorRef color = [self getCurrentColor];
149+
CGColorRef currentColor = [self getCurrentColor];
150+
CGColorRef color =
151+
CGColorCreateCopyWithAlpha(currentColor, self.strokeOpacity * CGColorGetAlpha(currentColor));
148152
[self drawWrappedText:context gc:gc rect:rect color:color];
153+
CGColorRelease(color);
149154
} else {
150155
CGColorRef color = [self.stroke getColorWithOpacity:self.strokeOpacity];
151156
[self drawWrappedText:context gc:gc rect:rect color:color];

apps/common/test/Test2923.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as React from 'react';
2+
import {View} from 'react-native';
3+
import Svg, {Rect, G} from 'react-native-svg';
4+
5+
export default function Test2923() {
6+
return (
7+
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
8+
<Svg width="200" height="200" viewBox="0 0 200 200">
9+
{/* Columns: fill only | stroke only | fill + stroke */}
10+
<G strokeWidth={4}>
11+
{/* Row 1: no opacity set */}
12+
<Rect
13+
x="5"
14+
y="5"
15+
width="55"
16+
height="40"
17+
color="blue"
18+
fill="currentColor"
19+
/>
20+
<Rect
21+
x="72"
22+
y="5"
23+
width="55"
24+
height="40"
25+
color="red"
26+
fill="none"
27+
stroke="currentColor"
28+
/>
29+
<Rect
30+
x="139"
31+
y="5"
32+
width="55"
33+
height="40"
34+
color="green"
35+
fill="currentColor"
36+
stroke="currentColor"
37+
/>
38+
{/* Row 2: fill-opacity=0.5 */}
39+
<Rect
40+
x="5"
41+
y="53"
42+
width="55"
43+
height="40"
44+
color="blue"
45+
fill="currentColor"
46+
fillOpacity={0.5}
47+
/>
48+
<Rect
49+
x="72"
50+
y="53"
51+
width="55"
52+
height="40"
53+
color="red"
54+
fill="none"
55+
stroke="currentColor"
56+
/>
57+
<Rect
58+
x="139"
59+
y="53"
60+
width="55"
61+
height="40"
62+
color="green"
63+
fill="currentColor"
64+
fillOpacity={0.5}
65+
stroke="currentColor"
66+
/>
67+
{/* Row 3: stroke-opacity=0.5 */}
68+
<Rect
69+
x="5"
70+
y="101"
71+
width="55"
72+
height="40"
73+
color="blue"
74+
fill="currentColor"
75+
/>
76+
<Rect
77+
x="72"
78+
y="101"
79+
width="55"
80+
height="40"
81+
color="red"
82+
fill="none"
83+
stroke="currentColor"
84+
strokeOpacity={0.5}
85+
/>
86+
<Rect
87+
x="139"
88+
y="101"
89+
width="55"
90+
height="40"
91+
color="green"
92+
fill="currentColor"
93+
stroke="currentColor"
94+
strokeOpacity={0.5}
95+
/>
96+
{/* Row 4: fill-opacity=0.5, stroke-opacity=0.5 */}
97+
<Rect
98+
x="5"
99+
y="149"
100+
width="55"
101+
height="40"
102+
color="blue"
103+
fill="currentColor"
104+
fillOpacity={0.5}
105+
/>
106+
<Rect
107+
x="72"
108+
y="149"
109+
width="55"
110+
height="40"
111+
color="red"
112+
fill="none"
113+
stroke="currentColor"
114+
strokeOpacity={0.5}
115+
/>
116+
<Rect
117+
x="139"
118+
y="149"
119+
width="55"
120+
height="40"
121+
color="green"
122+
fill="currentColor"
123+
fillOpacity={0.5}
124+
stroke="currentColor"
125+
strokeOpacity={0.5}
126+
/>
127+
</G>
128+
</Svg>
129+
</View>
130+
);
131+
}

apps/common/test/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import Test2455 from './Test2455';
3636
import Test2471 from './Test2471';
3737
import Test2520 from './Test2520';
3838
import Test2670 from './Test2670';
39+
import Test2923 from './Test2923';
3940

4041
export default function App() {
4142
return <ColorTest />;

e2e/cases/4.svg

Lines changed: 22 additions & 0 deletions
Loading

e2e/references/android/4.png

2.73 KB
Loading

e2e/references/ios/4.png

2.65 KB
Loading

0 commit comments

Comments
 (0)