Skip to content

Commit 91fb531

Browse files
authored
feat(🏞️): Add support for image within svg images (#3416)
1 parent 9bda940 commit 91fb531

7 files changed

Lines changed: 206 additions & 29 deletions

File tree

apps/docs/docs/image-svg.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,54 @@ const SVGWithCustomFonts = () => {
122122
};
123123
```
124124

125+
:::info
126+
127+
On Web, both the `fontMgr` parameter (second parameter) and image resources (third parameter) are ignored as SVG rendering relies on the browser's native SVG renderer rather than Skia's SVG module.
128+
129+
:::
130+
131+
### Images
132+
133+
Both `Skia.SVG.MakeFromData` and `Skia.SVG.MakeFromString` accept an optional third parameter to provide image resources for `<image>` elements in SVGs.
134+
This works similarly to [image loading in Skottie](/docs/skottie#with-assets).
135+
You can reference images either through base64 data URIs or by providing a resource map:
136+
137+
```tsx twoslash
138+
import React from "react";
139+
import { Canvas, ImageSVG, Skia, useData } from "@shopify/react-native-skia";
140+
141+
const SVGWithImages = () => {
142+
// Load an image asset
143+
const logo = useData(require("path/to/image.png"));
144+
145+
if (!logo) {
146+
return null;
147+
}
148+
149+
// Option 1: Using base64 data URI (embedded)
150+
const svgWithEmbeddedImage = Skia.SVG.MakeFromString(
151+
`<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
152+
<image xlink:href="data:image/png;base64,iVBORw0KG..." height="200" width="200" />
153+
</svg>`
154+
);
155+
156+
// Option 2: Using external reference with resource map
157+
const svgWithExternalImage = Skia.SVG.MakeFromString(
158+
`<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
159+
<image xlink:href="logo.png" height="200" width="200" />
160+
</svg>`,
161+
null,
162+
{ "logo.png": logo }
163+
);
164+
165+
return (
166+
<Canvas style={{ flex: 1 }}>
167+
<ImageSVG svg={svgWithExternalImage} x={0} y={0} width={200} height={200} />
168+
</Canvas>
169+
);
170+
};
171+
```
172+
125173
When rendering your SVG with Skia, all fonts available in your app are also available to your SVG.
126174
However, the way you can set the `font-family` attribute is as flexible as on the web.
127175
```jsx

apps/example/src/Examples/API/SVG.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Canvas,
66
ImageSVG,
77
Skia,
8+
useData,
89
useFonts,
910
useSVG,
1011
} from "@shopify/react-native-skia";
@@ -17,6 +18,7 @@ const fonts: Record<string, DataModule[]> = {
1718
};
1819

1920
export const SVG = () => {
21+
const logo = useData(require("../../Tests/assets/mdn_logo_only_color.png"));
2022
const { width, height } = useWindowDimensions();
2123
const svg = useSVG(require("./tiger.svg"));
2224
const fontMgr = useFonts(fonts);
@@ -26,12 +28,24 @@ export const SVG = () => {
2628
</svg>`,
2729
fontMgr
2830
);
31+
const svg3 = Skia.SVG.MakeFromString(
32+
`<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
33+
<image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANwAAADcCAYAAAAbWs+BAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gIUARQAHY8+4wAAApBJREFUeNrt3cFqAjEUhlEjvv8rXzciiiBGk/He5JxdN2U649dY+KmnEwAAAAAv2uMXEeGOwERntwAEB4IDBAeCAwQHggPBAYIDwQGCA8GB4ADBgeAAwYHgAMGB4EBwgOCgpkuKq2it/r8Li2hbvGKqP6s/PycnHHv9YvSWEgQHCA4EBwgOBAeCAwQHggMEByXM+QRUE6D3suwuPafDn5MTDg50KXnVPSdxa54y/oYDwQGCA8EBggPBAYIDwYHggBE+X5rY3Y3Tey97Nn2eU+rnlGfaZa6Ft5SA4EBwgOBAcCA4QHAgOEBwIDjgZu60y1xrDPtIJxwgOBAcIDgQHAgOEBwIDhAcCA4EBwgOBAcIDgQHCA4EB4IDBAeCAwQHggPBAYIDwQGCA8GB4ADBgeAAwYHgAMGB4GADcz9y2McIgxMOBAeCAwQHggMEB4IDwQGCA8EBggPBATdP6+KIGPRdW7i1LCFi6ALfCQfeUoLgAMGB4ADBgeBAcIDgQHCA4CCdOVvK7quwveQgg7eRTjjwlhIQHAgOBAcIDgQHCA4EB4IDBAfl5dhSdl+17SX3F22rdLlOOBAcCA4QHAgOEBwIDgQHCA4EBwgO0qm5pez6Ce0uSym2jXTCgeAAwYHgQHCA4EBwgOBAcCA4QHBQ3vpbyu47Yns51OLbSCccCA4QHAgOBAcIDgQHCA4EB4ID5jDt+vkObjgFM9dywoHgAMGB4EBwgOBAcIDgQHAgOEBwsA5bysPveMLtpW2kEw4EBwgOBAcIDgQHggMEB4IDBAeCg33ZUqZ/Ql9sL20jnXCA4EBwIDhAcCA4QHAgOBAcIDgQHNOZai3DlhKccCA4QHAgOEBwIDgQHCA4AAAAAGA1VyxaWIohrgXFAAAAAElFTkSuQmCC" height="200" width="200" />
34+
</svg>`
35+
);
36+
const svg4 = Skia.SVG.MakeFromString(
37+
`<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
38+
<image xlink:href="test.png" height="200" width="200" />
39+
</svg>`,
40+
null,
41+
{ "test.png": logo }
42+
);
2943
return (
3044
<View style={{ flex: 1 }}>
3145
<Canvas style={{ flex: 1 }}>
32-
<ImageSVG svg={svg} x={0} y={0} width={width / 2} height={height / 2} />
46+
<ImageSVG svg={svg} x={0} y={0} width={width / 2} height={height / 3} />
3347
</Canvas>
34-
<Canvas style={{ flex: 1 }}>
48+
<Canvas style={{ height: 40 }}>
3549
<ImageSVG
3650
svg={svg2}
3751
x={0}
@@ -40,6 +54,12 @@ export const SVG = () => {
4054
height={height / 2}
4155
/>
4256
</Canvas>
57+
<Canvas style={{ flex: 1 }}>
58+
<ImageSVG svg={svg3} x={0} y={0} width={200} height={200} />
59+
</Canvas>
60+
<Canvas style={{ flex: 1 }}>
61+
<ImageSVG svg={svg4} x={0} y={0} width={200} height={200} />
62+
</Canvas>
4363
</View>
4464
);
4565
};
9.33 KB
Loading

packages/skia/apple/SkiaCVPixelBufferUtils.mm

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,12 @@
3030
#include <TargetConditionals.h>
3131
#if TARGET_RT_BIG_ENDIAN
3232
#define FourCC2Str(fourcc) \
33-
(const char[]) { \
34-
*((char *)&fourcc), *(((char *)&fourcc) + 1), *(((char *)&fourcc) + 2), \
35-
*(((char *)&fourcc) + 3), 0 \
36-
}
33+
(const char[]){*((char *)&fourcc), *(((char *)&fourcc) + 1), \
34+
*(((char *)&fourcc) + 2), *(((char *)&fourcc) + 3), 0}
3735
#else
3836
#define FourCC2Str(fourcc) \
39-
(const char[]) { \
40-
*(((char *)&fourcc) + 3), *(((char *)&fourcc) + 2), \
41-
*(((char *)&fourcc) + 1), *(((char *)&fourcc) + 0), 0 \
42-
}
37+
(const char[]){*(((char *)&fourcc) + 3), *(((char *)&fourcc) + 2), \
38+
*(((char *)&fourcc) + 1), *(((char *)&fourcc) + 0), 0}
4339
#endif
4440

4541
// pragma MARK: TextureHolder

packages/skia/cpp/api/JsiSkSVG.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ namespace jsi = facebook::jsi;
2020

2121
class JsiSkSVG : public JsiSkWrappingSkPtrHostObject<SkSVGDOM> {
2222
public:
23-
JsiSkSVG(std::shared_ptr<RNSkPlatformContext> context, sk_sp<SkSVGDOM> svgdom)
23+
JsiSkSVG(std::shared_ptr<RNSkPlatformContext> context, sk_sp<SkSVGDOM> svgdom,
24+
sk_sp<skresources::ResourceProvider> resourceProvider = nullptr)
2425
: JsiSkWrappingSkPtrHostObject<SkSVGDOM>(std::move(context),
25-
std::move(svgdom)) {}
26+
std::move(svgdom)) {
27+
28+
}
29+
30+
~JsiSkSVG() = default;
2631

2732
EXPORT_JSI_API_TYPENAME(JsiSkSVG, SVG)
2833

packages/skia/cpp/api/JsiSkSVGFactory.h

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <memory>
4+
#include <unordered_map>
45
#include <utility>
56

67
#include <jsi/jsi.h>
@@ -14,49 +15,148 @@
1415
#pragma clang diagnostic ignored "-Wdocumentation"
1516

1617
#include "include/core/SkStream.h"
18+
#include "modules/skresources/include/SkResources.h"
1719

1820
#pragma clang diagnostic pop
1921

2022
namespace RNSkia {
2123

2224
namespace jsi = facebook::jsi;
2325

26+
class SVGAssetProvider : public skresources::ResourceProvider {
27+
public:
28+
using AssetMap = std::unordered_map<std::string, sk_sp<SkData>>;
29+
30+
static sk_sp<SVGAssetProvider>
31+
Make(AssetMap assets, skresources::ImageDecodeStrategy strategy =
32+
skresources::ImageDecodeStrategy::kPreDecode) {
33+
return sk_sp<SVGAssetProvider>(
34+
new SVGAssetProvider(std::move(assets), strategy));
35+
}
36+
37+
// Override loadImageAsset() to handle image loading
38+
sk_sp<skresources::ImageAsset>
39+
loadImageAsset(const char[] /*path*/, const char name[],
40+
const char[] /*id*/) const override {
41+
// Identify resources by name only
42+
auto it = fAssets.find(name);
43+
if (it != fAssets.end()) {
44+
// Create ImageAsset from SkData
45+
return skresources::MultiFrameImageAsset::Make(it->second, fStrategy);
46+
}
47+
return nullptr;
48+
}
49+
50+
private:
51+
explicit SVGAssetProvider(AssetMap assets,
52+
skresources::ImageDecodeStrategy strategy)
53+
: fAssets(std::move(assets)), fStrategy(strategy) {
54+
}
55+
const AssetMap fAssets;
56+
const skresources::ImageDecodeStrategy fStrategy;
57+
};
58+
2459
class JsiSkSVGFactory : public JsiSkHostObject {
2560
public:
26-
JSI_HOST_FUNCTION(MakeFromData) {
27-
auto data = JsiSkData::fromValue(runtime, arguments[0]);
28-
auto stream = SkMemoryStream::Make(data);
61+
// Helper function to parse asset map from JS object
62+
static SVGAssetProvider::AssetMap parseAssetMap(jsi::Runtime &runtime,
63+
const jsi::Value &jsValue) {
64+
SVGAssetProvider::AssetMap assets;
2965

30-
auto fontMgr = count > 1 && arguments[1].isObject()
31-
? JsiSkFontMgr::fromValue(runtime, arguments[1])
32-
: nullptr;
66+
if (!jsValue.isObject()) {
67+
return assets;
68+
}
3369

70+
auto jsAssetMap = jsValue.asObject(runtime);
71+
72+
// Convert JS object to C++ AssetMap
73+
auto propertyNames = jsAssetMap.getPropertyNames(runtime);
74+
size_t propertyCount = propertyNames.size(runtime);
75+
76+
for (size_t i = 0; i < propertyCount; i++) {
77+
auto propertyName =
78+
propertyNames.getValueAtIndex(runtime, i).asString(runtime);
79+
auto key = propertyName.utf8(runtime);
80+
auto jsValue = jsAssetMap.getProperty(runtime, propertyName);
81+
82+
// Skip null or undefined values
83+
if (jsValue.isNull() || jsValue.isUndefined()) {
84+
continue;
85+
}
86+
87+
if (jsValue.isObject()) {
88+
auto jsObject = jsValue.asObject(runtime);
89+
if (jsObject.isHostObject(runtime)) {
90+
auto hostObject = jsObject.getHostObject(runtime);
91+
auto skData = std::dynamic_pointer_cast<JsiSkData>(hostObject);
92+
if (skData) {
93+
assets[key] = skData->getObject();
94+
}
95+
}
96+
}
97+
}
98+
99+
return assets;
100+
}
101+
102+
private:
103+
jsi::Value makeSVGFromStream(jsi::Runtime &runtime,
104+
std::unique_ptr<SkMemoryStream> stream,
105+
sk_sp<SkFontMgr> fontMgr,
106+
SVGAssetProvider::AssetMap assets) {
34107
auto builder = SkSVGDOM::Builder();
108+
35109
if (fontMgr) {
36110
builder.setFontManager(fontMgr);
37111
}
112+
113+
auto baseProvider = SVGAssetProvider::Make(
114+
std::move(assets), skresources::ImageDecodeStrategy::kPreDecode);
115+
auto provider = skresources::DataURIResourceProviderProxy::Make(
116+
std::move(baseProvider), skresources::ImageDecodeStrategy::kPreDecode, fontMgr);
117+
118+
// TODO: this sk_sp subclassing issue needs to be fixed.
119+
provider->ref();
120+
builder.setResourceProvider(provider);
121+
38122
auto svg_dom = builder.make(*stream);
39-
auto svg = std::make_shared<JsiSkSVG>(getContext(), std::move(svg_dom));
123+
auto svg =
124+
std::make_shared<JsiSkSVG>(getContext(), std::move(svg_dom));
40125
return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(runtime, svg,
41126
getContext());
42127
}
43128

129+
public:
130+
JSI_HOST_FUNCTION(MakeFromData) {
131+
auto data = JsiSkData::fromValue(runtime, arguments[0]);
132+
auto stream = SkMemoryStream::Make(data);
133+
134+
// Parse fontMgr (second parameter)
135+
auto fontMgr = count > 1 && arguments[1].isObject()
136+
? JsiSkFontMgr::fromValue(runtime, arguments[1])
137+
: nullptr;
138+
139+
// Parse assets map (third parameter)
140+
auto assets = count > 2 ? parseAssetMap(runtime, arguments[2])
141+
: SVGAssetProvider::AssetMap();
142+
143+
return makeSVGFromStream(runtime, std::move(stream), fontMgr, std::move(assets));
144+
}
145+
44146
JSI_HOST_FUNCTION(MakeFromString) {
45147
auto svgText = arguments[0].asString(runtime).utf8(runtime);
46148
auto stream = SkMemoryStream::MakeDirect(svgText.c_str(), svgText.size());
47149

150+
// Parse fontMgr (second parameter)
48151
auto fontMgr = count > 1 && arguments[1].isObject()
49152
? JsiSkFontMgr::fromValue(runtime, arguments[1])
50153
: nullptr;
51154

52-
auto builder = SkSVGDOM::Builder();
53-
if (fontMgr) {
54-
builder.setFontManager(fontMgr);
55-
}
56-
auto svg_dom = builder.make(*stream);
57-
auto svg = std::make_shared<JsiSkSVG>(getContext(), std::move(svg_dom));
58-
return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(runtime, svg,
59-
getContext());
155+
// Parse assets map (third parameter)
156+
auto assets = count > 2 ? parseAssetMap(runtime, arguments[2])
157+
: SVGAssetProvider::AssetMap();
158+
159+
return makeSVGFromStream(runtime, std::move(stream), fontMgr, std::move(assets));
60160
}
61161

62162
size_t getMemoryPressure() const override { return 512; }

packages/skia/src/skia/types/SVG/SVGFactory.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import type { SkFontMgr } from "../Font/FontMgr";
44
import type { SkSVG } from "./SVG";
55

66
export interface SVGFactory {
7-
MakeFromData(data: SkData, fontMgr?: SkFontMgr | null): SkSVG | null;
8-
MakeFromString(str: string, fontMgr?: SkFontMgr | null): SkSVG | null;
7+
MakeFromData(
8+
data: SkData,
9+
fontMgr?: SkFontMgr | null,
10+
assets?: Record<string, SkData | null>
11+
): SkSVG | null;
12+
MakeFromString(
13+
str: string,
14+
fontMgr?: SkFontMgr | null,
15+
assets?: Record<string, SkData | null>
16+
): SkSVG | null;
917
}

0 commit comments

Comments
 (0)