Skip to content

Commit 0daf82c

Browse files
url() function support in background image parser
lint fix add local image example
1 parent c6b137c commit 0daf82c

23 files changed

Lines changed: 1056 additions & 96 deletions

‎packages/react-native/Libraries/StyleSheet/StyleSheetTypes.js‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,16 @@ type RadialGradientValue = {
769769
}>,
770770
};
771771

772-
export type BackgroundImageValue = LinearGradientValue | RadialGradientValue;
772+
type URLBackgroundImageValue = {
773+
type: 'url',
774+
uri: string | number,
775+
};
776+
777+
export type GradientBackgroundImageValue =
778+
LinearGradientValue | RadialGradientValue;
779+
780+
export type BackgroundImageValue =
781+
GradientBackgroundImageValue | URLBackgroundImageValue;
773782

774783
export type BackgroundSizeValue = {
775784
x: string | number,

‎packages/react-native/Libraries/StyleSheet/__tests__/processBackgroundImage-itest.js‎

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,10 @@ describe('processBackgroundImage', () => {
363363
{color: 'blue', positions: ['100%']},
364364
],
365365
},
366+
{
367+
type: 'url',
368+
uri: 'https://example.com',
369+
},
366370
];
367371
const result = processBackgroundImage(input);
368372
expect(result).toEqual([
@@ -374,6 +378,10 @@ describe('processBackgroundImage', () => {
374378
{color: processColor('blue'), position: '100%'},
375379
],
376380
},
381+
{
382+
type: 'url',
383+
uri: 'https://example.com',
384+
},
377385
]);
378386
});
379387

@@ -657,7 +665,11 @@ describe('processBackgroundImage', () => {
657665
],
658666
},
659667
]);
660-
expect(result[0].colorStops[0].color).toEqual({
668+
const layer = result[0];
669+
if (layer.type !== 'linear-gradient') {
670+
throw new Error(`Expected a linear-gradient, got ${layer.type}`);
671+
}
672+
expect(layer.colorStops[0].color).toEqual({
661673
semantic: ['systemRedColor'],
662674
});
663675
});
@@ -674,7 +686,11 @@ describe('processBackgroundImage', () => {
674686
],
675687
},
676688
]);
677-
expect(result[0].colorStops[0].color).toEqual({
689+
const layer = result[0];
690+
if (layer.type !== 'linear-gradient') {
691+
throw new Error(`Expected a linear-gradient, got ${layer.type}`);
692+
}
693+
expect(layer.colorStops[0].color).toEqual({
678694
dynamic: {light: 0xff000000, dark: 0xffffffff},
679695
});
680696
});
@@ -696,7 +712,11 @@ describe('processBackgroundImage', () => {
696712
],
697713
},
698714
]);
699-
expect(result[0].colorStops[0].color).toEqual({
715+
const layer = result[0];
716+
if (layer.type !== 'linear-gradient') {
717+
throw new Error(`Expected a linear-gradient, got ${layer.type}`);
718+
}
719+
expect(layer.colorStops[0].color).toEqual({
700720
resource_paths: ['?attr/colorPrimary'],
701721
});
702722
});
@@ -1188,4 +1208,104 @@ describe('processBackgroundImage', () => {
11881208
expect(result1).toEqual([]);
11891209
expect(result2).toEqual([]);
11901210
});
1211+
1212+
it('should parse url unquoted', () => {
1213+
const result = processBackgroundImage('url(https://example.com/image.png)');
1214+
expect(result).toEqual([
1215+
{type: 'url', uri: 'https://example.com/image.png'},
1216+
]);
1217+
});
1218+
1219+
it('should parse url double quoted', () => {
1220+
const result = processBackgroundImage(
1221+
'url("https://example.com/image.png")',
1222+
);
1223+
expect(result).toEqual([
1224+
{type: 'url', uri: 'https://example.com/image.png'},
1225+
]);
1226+
});
1227+
1228+
it('should parse url single quoted', () => {
1229+
const result = processBackgroundImage(
1230+
"url('https://example.com/image.png')",
1231+
);
1232+
expect(result).toEqual([
1233+
{type: 'url', uri: 'https://example.com/image.png'},
1234+
]);
1235+
});
1236+
1237+
it('should parse url case insensitive', () => {
1238+
const result = processBackgroundImage('UrL(https://example.com/image.png)');
1239+
expect(result).toEqual([
1240+
{type: 'url', uri: 'https://example.com/image.png'},
1241+
]);
1242+
});
1243+
1244+
it('should parse url with query params', () => {
1245+
const result = processBackgroundImage(
1246+
'url(https://example.com/image.png?size=Large&format=webp)',
1247+
);
1248+
expect(result).toEqual([
1249+
{
1250+
type: 'url',
1251+
uri: 'https://example.com/image.png?size=Large&format=webp',
1252+
},
1253+
]);
1254+
});
1255+
1256+
it('should parse url with whitespace', () => {
1257+
const result = processBackgroundImage(
1258+
'url( https://example.com/image.png )',
1259+
);
1260+
expect(result).toEqual([
1261+
{type: 'url', uri: 'https://example.com/image.png'},
1262+
]);
1263+
});
1264+
1265+
it('should parse multiple urls', () => {
1266+
const result = processBackgroundImage(
1267+
'url(https://example.com/bg1.png), url(https://example.com/bg2.png)',
1268+
);
1269+
expect(result).toEqual([
1270+
{type: 'url', uri: 'https://example.com/bg1.png'},
1271+
{type: 'url', uri: 'https://example.com/bg2.png'},
1272+
]);
1273+
});
1274+
1275+
it('should parse url mixed with gradients', () => {
1276+
const result = processBackgroundImage(
1277+
'radial-gradient(circle at top left, red, blue), url(https://example.com/image.png), linear-gradient(to bottom, green, yellow)',
1278+
);
1279+
expect(result).toEqual([
1280+
{
1281+
type: 'radial-gradient',
1282+
shape: 'circle',
1283+
size: 'farthest-corner',
1284+
position: {top: '0%', left: '0%'},
1285+
colorStops: [
1286+
{color: processColor('red'), position: null},
1287+
{color: processColor('blue'), position: null},
1288+
],
1289+
},
1290+
{type: 'url', uri: 'https://example.com/image.png'},
1291+
{
1292+
type: 'linear-gradient',
1293+
direction: {type: 'angle', value: 180},
1294+
colorStops: [
1295+
{color: processColor('green'), position: null},
1296+
{color: processColor('yellow'), position: null},
1297+
],
1298+
},
1299+
]);
1300+
});
1301+
1302+
it('should return empty for url empty', () => {
1303+
const result = processBackgroundImage('url()');
1304+
expect(result).toEqual([]);
1305+
});
1306+
1307+
it('should return empty for url empty quoted', () => {
1308+
const result = processBackgroundImage('url("")');
1309+
expect(result).toEqual([]);
1310+
});
11911311
});

‎packages/react-native/Libraries/StyleSheet/processBackgroundImage.js‎

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@
1313
import type {ProcessedColorValue} from './processColor';
1414
import type {
1515
BackgroundImageValue,
16+
GradientBackgroundImageValue,
1617
RadialGradientPosition,
1718
RadialGradientShape,
1819
RadialGradientSize,
1920
} from './StyleSheetTypes';
2021

22+
const resolveAssetSource = require('../Image/resolveAssetSource').default;
2123
const processColor = require('./processColor').default;
2224

2325
// Pre-compiled regex patterns for performance - avoids regex compilation on each call
2426
const NEWLINE_REGEX = /\n/g;
2527
const GRADIENT_REGEX = /^(linear|radial)-gradient\(((?:\([^)]*\)|[^()])*)\)/;
28+
const URL_REGEX = /^url\((.*)\)$/i;
2629
const COMMA_SPLIT_REGEX = /,(?![^(]*\))/;
2730
const WHITESPACE_SPLIT_REGEX = /\s+/;
2831
const COLOR_STOP_PARTS_REGEX = /\S+\([^)]*\)|\S+/g;
@@ -70,13 +73,24 @@ type RadialGradientBackgroundImage = {
7073
}>,
7174
};
7275

76+
type URLBackgroundImage = {
77+
type: 'url',
78+
uri: string,
79+
// Natural size in layout units, known only for bundled assets. Native falls
80+
// back to the decoded image's own size when these are absent.
81+
intrinsicWidth?: number,
82+
intrinsicHeight?: number,
83+
};
84+
7385
// null color indicate that the transition hint syntax is used. e.g. red, 20%, blue
7486
type ColorStopColor = ProcessedColorValue | null;
7587
// percentage or pixel value
7688
type ColorStopPosition = number | string | null;
7789

7890
type ParsedBackgroundImageValue =
79-
LinearGradientBackgroundImage | RadialGradientBackgroundImage;
91+
| LinearGradientBackgroundImage
92+
| RadialGradientBackgroundImage
93+
| URLBackgroundImage;
8094

8195
export default function processBackgroundImage(
8296
backgroundImage: ?(ReadonlyArray<BackgroundImageValue> | string),
@@ -92,6 +106,41 @@ export default function processBackgroundImage(
92106
);
93107
} else if (Array.isArray(backgroundImage)) {
94108
for (const bgImage of backgroundImage) {
109+
if (bgImage.type === 'url') {
110+
let uri: ?string = null;
111+
let intrinsicWidth: ?number = null;
112+
let intrinsicHeight: ?number = null;
113+
if (typeof bgImage.uri === 'string') {
114+
uri = bgImage.uri;
115+
} else if (typeof bgImage.uri === 'number') {
116+
const source = resolveAssetSource(bgImage.uri);
117+
if (source != null && source.uri != null) {
118+
uri = source.uri;
119+
// The asset registry records these already divided by the asset's
120+
// scale, so they are layout units and need no density maths native
121+
// side. Matches how `Image` derives its default dimensions.
122+
if (typeof source.width === 'number') {
123+
intrinsicWidth = source.width;
124+
}
125+
if (typeof source.height === 'number') {
126+
intrinsicHeight = source.height;
127+
}
128+
}
129+
}
130+
if (uri != null) {
131+
// The registry reports both dimensions or neither.
132+
result = result.concat(
133+
intrinsicWidth != null && intrinsicHeight != null
134+
? {type: 'url', uri, intrinsicWidth, intrinsicHeight}
135+
: {type: 'url', uri},
136+
);
137+
continue;
138+
} else {
139+
// If the URI is invalid, return an empty array. Same as web.
140+
return [];
141+
}
142+
}
143+
95144
const processedColorStops = processColorStops(bgImage);
96145
if (processedColorStops == null) {
97146
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web.
@@ -191,7 +240,9 @@ export default function processBackgroundImage(
191240
return result;
192241
}
193242

194-
function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{
243+
function processColorStops(
244+
bgImage: GradientBackgroundImageValue,
245+
): ReadonlyArray<{
195246
color: ColorStopColor,
196247
position: ColorStopPosition,
197248
}> | null {
@@ -258,10 +309,28 @@ function processColorStops(bgImage: BackgroundImageValue): ReadonlyArray<{
258309
function parseBackgroundImageCSSString(
259310
cssString: string,
260311
): ReadonlyArray<ParsedBackgroundImageValue> {
261-
const gradients = [];
312+
const backgroundImages: Array<ParsedBackgroundImageValue> = [];
262313
const bgImageStrings = splitGradients(cssString);
263314

264315
for (const bgImageString of bgImageStrings) {
316+
const urlMatch = URL_REGEX.exec(bgImageString);
317+
if (urlMatch) {
318+
let uri = urlMatch[1].trim();
319+
const first = uri[0];
320+
if ((first === '"' || first === "'") && uri.endsWith(first)) {
321+
uri = uri.slice(1, -1);
322+
}
323+
if (uri.length > 0) {
324+
// A url() in a css string is not an asset, so its natural size is only
325+
// known once the image is decoded.
326+
backgroundImages.push({
327+
type: 'url',
328+
uri,
329+
});
330+
}
331+
continue;
332+
}
333+
265334
const bgImage = bgImageString.toLowerCase();
266335
const match = GRADIENT_REGEX.exec(bgImage);
267336
if (match) {
@@ -272,11 +341,11 @@ function parseBackgroundImageCSSString(
272341
: parseLinearGradientCSSString(gradientContent);
273342

274343
if (gradient != null) {
275-
gradients.push(gradient);
344+
backgroundImages.push(gradient);
276345
}
277346
}
278347
}
279-
return gradients;
348+
return backgroundImages;
280349
}
281350

282351
function parseRadialGradientCSSString(

‎packages/react-native/ReactCommon/react/renderer/components/view/BackgroundImagePropsConversions.cpp‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,24 @@ void parseProcessedBackgroundImage(
208208
}
209209

210210
backgroundImage.emplace_back(std::move(radialGradient));
211+
} else if (type == "url") {
212+
auto uriIt = rawBackgroundImageMap.find("uri");
213+
if (uriIt != rawBackgroundImageMap.end() &&
214+
uriIt->second.hasType<std::string>()) {
215+
URLBackgroundImage urlBackgroundImage;
216+
urlBackgroundImage.uri = (std::string)(uriIt->second);
217+
auto widthIt = rawBackgroundImageMap.find("intrinsicWidth");
218+
if (widthIt != rawBackgroundImageMap.end() &&
219+
widthIt->second.hasType<Float>()) {
220+
urlBackgroundImage.intrinsicWidth = (Float)(widthIt->second);
221+
}
222+
auto heightIt = rawBackgroundImageMap.find("intrinsicHeight");
223+
if (heightIt != rawBackgroundImageMap.end() &&
224+
heightIt->second.hasType<Float>()) {
225+
urlBackgroundImage.intrinsicHeight = (Float)(heightIt->second);
226+
}
227+
backgroundImage.emplace_back(std::move(urlBackgroundImage));
228+
}
211229
}
212230
}
213231

@@ -409,6 +427,24 @@ void parseUnprocessedBackgroundImageList(
409427
}
410428

411429
backgroundImage.emplace_back(std::move(radialGradient));
430+
} else if (type == "url") {
431+
auto uriIt = rawBackgroundImageMap.find("uri");
432+
if (uriIt != rawBackgroundImageMap.end() &&
433+
uriIt->second.hasType<std::string>()) {
434+
URLBackgroundImage urlBackgroundImage;
435+
urlBackgroundImage.uri = (std::string)(uriIt->second);
436+
auto widthIt = rawBackgroundImageMap.find("intrinsicWidth");
437+
if (widthIt != rawBackgroundImageMap.end() &&
438+
widthIt->second.hasType<Float>()) {
439+
urlBackgroundImage.intrinsicWidth = (Float)(widthIt->second);
440+
}
441+
auto heightIt = rawBackgroundImageMap.find("intrinsicHeight");
442+
if (heightIt != rawBackgroundImageMap.end() &&
443+
heightIt->second.hasType<Float>()) {
444+
urlBackgroundImage.intrinsicHeight = (Float)(heightIt->second);
445+
}
446+
backgroundImage.emplace_back(std::move(urlBackgroundImage));
447+
}
412448
}
413449
}
414450

@@ -472,6 +508,13 @@ void fromCSSColorStop(
472508

473509
std::optional<BackgroundImage> fromCSSBackgroundImage(
474510
const CSSBackgroundImage& cssBackgroundImage) {
511+
if (std::holds_alternative<CSSURLFunction>(cssBackgroundImage)) {
512+
const auto& urlFunc = std::get<CSSURLFunction>(cssBackgroundImage);
513+
URLBackgroundImage urlBackgroundImage;
514+
urlBackgroundImage.uri = urlFunc.url;
515+
return BackgroundImage{urlBackgroundImage};
516+
}
517+
475518
if (std::holds_alternative<CSSLinearGradientFunction>(cssBackgroundImage)) {
476519
const auto& gradient =
477520
std::get<CSSLinearGradientFunction>(cssBackgroundImage);

0 commit comments

Comments
 (0)