├── .babelrc
├── .gitignore
├── README.md
├── index.d.ts
├── index.js
├── package.json
├── screenshoots
└── sample.png
├── test
└── utils.js
└── utils.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-native-svg-uri
2 | Render SVG images in React Native from an URL or a static file
3 |
4 | This was tested with RN 0.33 and react-native-svg 4.3.1 (depends on this library)
5 | [react-native-svg](https://github.com/react-native-community/react-native-svg)
6 |
7 |
8 | Not all the svgs can be rendered, if you find problems fill an issue or a PR in
9 | order to contemplate all the cases
10 |
11 | Install library from `npm`
12 |
13 | ```bash
14 | npm install react-native-svg-uri --save
15 | ```
16 |
17 | Link library react-native-svg
18 |
19 | ```bash
20 | react-native link react-native-svg # not react-native-svg-uri !!!
21 | ```
22 |
23 | ## Props
24 |
25 | | Prop | Type | Default | Note |
26 | |---|---|---|---|
27 | | `source` | `ImageSource` | | Same kind of `source` prop that `` component has
28 | | `svgXmlData` | `String` | | You can pass the SVG as String directly
29 | | `fill` | `Color` | | Overrides all fill attributes of the svg file
30 | | `fillAll` | `Boolean` | Adds the fill color to the entire svg object
31 |
32 | ## Known Bugs
33 |
34 | - [ANDROID] There is a problem with static SVG file on Android,
35 | Works OK in debug mode but fails to load the file in release mode.
36 | At the moment the only workaround is to pass the svg content in the svgXmlData prop.
37 |
38 | ## Usage
39 |
40 | Here's a simple example:
41 |
42 | ```javascript
43 | import SvgUri from 'react-native-svg-uri';
44 |
45 | const TestSvgUri = () => (
46 |
47 |
52 |
53 | );
54 | ```
55 |
56 | or a static file
57 |
58 | ```javascript
59 |
60 | ```
61 |
62 | This will render:
63 |
64 | 
65 |
66 | ## Testing
67 | 1. Make sure you have installed dependencies with `npm i`
68 | 2. Run tests with `npm test`
69 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for react-native-svg-uri 1.1.2
2 | // Project: https://github.com/matc4/react-native-svg-uri
3 | // Definitions by: Kyle Roach
4 | // TypeScript Version: 2.2.2
5 |
6 | import React, { Component } from 'react'
7 | import { ImageURISource } from 'react-native'
8 |
9 | interface SvgUriProps {
10 | /**
11 | * The width of the rendered svg
12 | */
13 | width?: number | string
14 |
15 | /**
16 | * The height of the rendered svg
17 | */
18 | height?: number | string
19 |
20 | /**
21 | * Source path for the .svg file
22 | * Expects a require('path') to the file or object with uri.
23 | * e.g. source={require('my-path')}
24 | * e.g. source={{ur: 'my-path'}}
25 | */
26 | source?: ImageURISource
27 |
28 | /**
29 | * Direct svg code to render. Similar to inline svg
30 | */
31 | svgXmlData?: string
32 |
33 | /**
34 | * Fill color for the svg object
35 | */
36 | fill?: string
37 |
38 | /**
39 | * Invoked when load completes successfully.
40 | */
41 | onLoad?: Function
42 |
43 | /**
44 | * Fill the entire svg element with same color
45 | */
46 | fillAll?: boolean
47 | }
48 |
49 | export default class SvgUri extends Component { }
50 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import {View} from 'react-native';
3 | import PropTypes from 'prop-types'
4 | import xmldom from 'xmldom';
5 | import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
6 |
7 | import Svg,{
8 | Circle,
9 | Ellipse,
10 | G ,
11 | LinearGradient,
12 | RadialGradient,
13 | Line,
14 | Path,
15 | Polygon,
16 | Polyline,
17 | Rect,
18 | Text,
19 | TSpan,
20 | Defs,
21 | Stop
22 | } from 'react-native-svg';
23 |
24 | import * as utils from './utils';
25 |
26 | const ACCEPTED_SVG_ELEMENTS = [
27 | 'svg',
28 | 'g',
29 | 'circle',
30 | 'path',
31 | 'rect',
32 | 'defs',
33 | 'line',
34 | 'linearGradient',
35 | 'radialGradient',
36 | 'stop',
37 | 'ellipse',
38 | 'polygon',
39 | 'polyline',
40 | 'text',
41 | 'tspan'
42 | ];
43 |
44 | // Attributes from SVG elements that are mapped directly.
45 | const SVG_ATTS = ['viewBox', 'width', 'height'];
46 | const G_ATTS = ['id'];
47 |
48 | const CIRCLE_ATTS = ['cx', 'cy', 'r'];
49 | const PATH_ATTS = ['d'];
50 | const RECT_ATTS = ['width', 'height'];
51 | const LINE_ATTS = ['x1', 'y1', 'x2', 'y2'];
52 | const LINEARG_ATTS = LINE_ATTS.concat(['id', 'gradientUnits']);
53 | const RADIALG_ATTS = CIRCLE_ATTS.concat(['id', 'gradientUnits']);
54 | const STOP_ATTS = ['offset'];
55 | const ELLIPSE_ATTS = ['cx', 'cy', 'rx', 'ry'];
56 |
57 | const TEXT_ATTS = ['fontFamily', 'fontSize', 'fontWeight', 'textAnchor']
58 |
59 | const POLYGON_ATTS = ['points'];
60 | const POLYLINE_ATTS = ['points'];
61 |
62 | const COMMON_ATTS = ['fill', 'fillOpacity', 'stroke', 'strokeWidth', 'strokeOpacity', 'opacity',
63 | 'strokeLinecap', 'strokeLinejoin',
64 | 'strokeDasharray', 'strokeDashoffset', 'x', 'y', 'rotate', 'scale', 'origin', 'originX', 'originY', 'transform', 'clipPath'];
65 |
66 | let ind = 0;
67 |
68 | function fixYPosition (y, node) {
69 | if (node.attributes) {
70 | const fontSizeAttr = Object.keys(node.attributes).find(a => node.attributes[a].name === 'font-size');
71 | if (fontSizeAttr) {
72 | return '' + (parseFloat(y) - parseFloat(node.attributes[fontSizeAttr].value));
73 | }
74 | }
75 | if (!node.parentNode) {
76 | return y;
77 | }
78 | return fixYPosition(y, node.parentNode)
79 | }
80 |
81 | class SvgUri extends Component{
82 |
83 | constructor(props){
84 | super(props);
85 |
86 | this.state = {fill: props.fill, svgXmlData: props.svgXmlData};
87 |
88 | this.createSVGElement = this.createSVGElement.bind(this);
89 | this.obtainComponentAtts = this.obtainComponentAtts.bind(this);
90 | this.inspectNode = this.inspectNode.bind(this);
91 | this.fetchSVGData = this.fetchSVGData.bind(this);
92 |
93 | this.isComponentMounted = false;
94 |
95 | // Gets the image data from an URL or a static file
96 | if (props.source) {
97 | const source = resolveAssetSource(props.source) || {};
98 | this.fetchSVGData(source.uri);
99 | }
100 | }
101 |
102 | componentWillMount() {
103 | this.isComponentMounted = true;
104 | }
105 |
106 | componentWillReceiveProps (nextProps){
107 | if (nextProps.source) {
108 | const source = resolveAssetSource(nextProps.source) || {};
109 | const oldSource = resolveAssetSource(this.props.source) || {};
110 | if(source.uri !== oldSource.uri){
111 | this.fetchSVGData(source.uri);
112 | }
113 | }
114 |
115 | if (nextProps.svgXmlData !== this.props.svgXmlData) {
116 | this.setState({ svgXmlData: nextProps.svgXmlData });
117 | }
118 |
119 | if (nextProps.fill !== this.props.fill) {
120 | this.setState({ fill: nextProps.fill });
121 | }
122 | }
123 |
124 | componentWillUnmount() {
125 | this.isComponentMounted = false
126 | }
127 |
128 | async fetchSVGData(uri) {
129 | let responseXML = null, error = null;
130 | try {
131 | const response = await fetch(uri);
132 | responseXML = await response.text();
133 | } catch(e) {
134 | error = e;
135 | console.error("ERROR SVG", e);
136 | } finally {
137 | if (this.isComponentMounted) {
138 | this.setState({ svgXmlData: responseXML }, () => {
139 | const { onLoad } = this.props;
140 | if (onLoad && !error) {
141 | onLoad();
142 | }
143 | });
144 | }
145 | }
146 |
147 | return responseXML;
148 | }
149 |
150 | // Remove empty strings from children array
151 | trimElementChilden(children) {
152 | for (child of children) {
153 | if (typeof child === 'string') {
154 | if (child.trim().length === 0)
155 | children.splice(children.indexOf(child), 1);
156 | }
157 | }
158 | }
159 |
160 | createSVGElement(node, childs){
161 | this.trimElementChilden(childs);
162 | let componentAtts = {};
163 | const i = ind++;
164 | switch (node.nodeName) {
165 | case 'svg':
166 | componentAtts = this.obtainComponentAtts(node, SVG_ATTS);
167 | if (this.props.width) {
168 | componentAtts.width = this.props.width;
169 | }
170 | if (this.props.height) {
171 | componentAtts.height = this.props.height;
172 | }
173 |
174 | return ;
175 | case 'g':
176 | componentAtts = this.obtainComponentAtts(node, G_ATTS);
177 | return {childs};
178 | case 'path':
179 | componentAtts = this.obtainComponentAtts(node, PATH_ATTS);
180 | return {childs};
181 | case 'circle':
182 | componentAtts = this.obtainComponentAtts(node, CIRCLE_ATTS);
183 | return {childs};
184 | case 'rect':
185 | componentAtts = this.obtainComponentAtts(node, RECT_ATTS);
186 | return {childs};
187 | case 'line':
188 | componentAtts = this.obtainComponentAtts(node, LINE_ATTS);
189 | return {childs};
190 | case 'defs':
191 | return {childs};
192 | case 'linearGradient':
193 | componentAtts = this.obtainComponentAtts(node, LINEARG_ATTS);
194 | return {childs};
195 | case 'radialGradient':
196 | componentAtts = this.obtainComponentAtts(node, RADIALG_ATTS);
197 | return {childs};
198 | case 'stop':
199 | componentAtts = this.obtainComponentAtts(node, STOP_ATTS);
200 | return {childs};
201 | case 'ellipse':
202 | componentAtts = this.obtainComponentAtts(node, ELLIPSE_ATTS);
203 | return {childs};
204 | case 'polygon':
205 | componentAtts = this.obtainComponentAtts(node, POLYGON_ATTS);
206 | return {childs};
207 | case 'polyline':
208 | componentAtts = this.obtainComponentAtts(node, POLYLINE_ATTS);
209 | return {childs};
210 | case 'text':
211 | componentAtts = this.obtainComponentAtts(node, TEXT_ATTS);
212 | return {childs};
213 | case 'tspan':
214 | componentAtts = this.obtainComponentAtts(node, TEXT_ATTS);
215 | if (componentAtts.y) {
216 | componentAtts.y = fixYPosition(componentAtts.y, node)
217 | }
218 | return {childs};
219 | default:
220 | return null;
221 | }
222 | }
223 |
224 | obtainComponentAtts({attributes}, enabledAttributes) {
225 | const styleAtts = {};
226 |
227 | if (this.state.fill && this.props.fillAll) {
228 | styleAtts.fill = this.state.fill;
229 | }
230 |
231 | Array.from(attributes).forEach(({nodeName, nodeValue}) => {
232 | Object.assign(styleAtts, utils.transformStyle({
233 | nodeName,
234 | nodeValue,
235 | fillProp: this.state.fill
236 | }));
237 | });
238 |
239 | const componentAtts = Array.from(attributes)
240 | .map(utils.camelCaseNodeName)
241 | .map(utils.removePixelsFromNodeValue)
242 | .filter(utils.getEnabledAttributes(enabledAttributes.concat(COMMON_ATTS)))
243 | .reduce((acc, {nodeName, nodeValue}) => {
244 | acc[nodeName] = (this.state.fill && nodeName === 'fill' && nodeValue !== 'none') ? this.state.fill : nodeValue
245 | return acc
246 | }, {});
247 | Object.assign(componentAtts, styleAtts);
248 |
249 | return componentAtts;
250 | }
251 |
252 | inspectNode(node){
253 | // Only process accepted elements
254 | if (!ACCEPTED_SVG_ELEMENTS.includes(node.nodeName)) {
255 | return ();
256 | }
257 |
258 | // Process the xml node
259 | const arrayElements = [];
260 |
261 | // if have children process them.
262 | // Recursive function.
263 | if (node.childNodes && node.childNodes.length > 0){
264 | for (let i = 0; i < node.childNodes.length; i++){
265 | const isTextValue = node.childNodes[i].nodeValue
266 | if (isTextValue) {
267 | arrayElements.push(node.childNodes[i].nodeValue)
268 | } else {
269 | const nodo = this.inspectNode(node.childNodes[i]);
270 | if (nodo != null) {
271 | arrayElements.push(nodo);
272 | }
273 | }
274 | }
275 | }
276 |
277 | return this.createSVGElement(node, arrayElements);
278 | }
279 |
280 | render () {
281 | try {
282 | if (this.state.svgXmlData == null) {
283 | return null;
284 | }
285 |
286 | const inputSVG = this.state.svgXmlData.substring(
287 | this.state.svgXmlData.indexOf("