├── .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 | ![Component example](./screenshoots/sample.png) 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 {childs}; 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("") + 6) 289 | ).replace(//g, ''); 290 | 291 | const doc = new xmldom.DOMParser().parseFromString(inputSVG); 292 | 293 | const rootSVG = this.inspectNode(doc.childNodes[0]); 294 | 295 | return( 296 | 297 | {rootSVG} 298 | 299 | ); 300 | } catch(e){ 301 | console.error("ERROR SVG", e); 302 | return null; 303 | } 304 | } 305 | } 306 | 307 | SvgUri.propTypes = { 308 | style: PropTypes.object, 309 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 310 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 311 | svgXmlData: PropTypes.string, 312 | source: PropTypes.any, 313 | fill: PropTypes.string, 314 | onLoad: PropTypes.func, 315 | fillAll: PropTypes.bool 316 | } 317 | 318 | module.exports = SvgUri; 319 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-svg-uri", 3 | "version": "1.2.3", 4 | "description": "Render an SVG Image from an URL", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/mocha/bin/mocha --compilers js:babel-core/register" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/matiascba/react-native-svg-uri.git" 12 | }, 13 | "keywords": [ 14 | "react-native", 15 | "svg", 16 | "url", 17 | "uri", 18 | "http" 19 | ], 20 | "author": "Matias Cortes", 21 | "license": "ISC", 22 | "dependencies": { 23 | "xmldom": "^0.1.22" 24 | }, 25 | "peerDependencies": { 26 | "react-native-svg": "^5.3.0" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/matiascba/react-native-svg-uri/issues" 30 | }, 31 | "homepage": "https://github.com/matiascba/react-native-svg-uri#readme", 32 | "devDependencies": { 33 | "babel-core": "^6.24.1", 34 | "babel-preset-react-native": "^2.1.0", 35 | "chai": "^3.5.0", 36 | "mocha": "^3.2.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /screenshoots/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vault-development/react-native-svg-uri/c6841a6e7d1a6ee5cf5b515d4faea6002e166f3c/screenshoots/sample.png -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | 3 | import {transformStyle, camelCase, removePixelsFromNodeValue, getEnabledAttributes} from '../utils'; 4 | 5 | describe('transformStyle', () => { 6 | it('transforms style attribute', () => { 7 | expect( 8 | transformStyle({nodeName: 'style', nodeValue: 'fill:rgb(0,0,255);stroke:rgb(0,0,0)'}) 9 | ).to.deep.equal({ 10 | fill: 'rgb(0,0,255)', 11 | stroke: 'rgb(0,0,0)', 12 | }); 13 | }); 14 | 15 | it('transforms style attribute with dash-case attribute', () => { 16 | expect( 17 | transformStyle({nodeName: 'style', nodeValue: 'stop-color:#ffffff'}) 18 | ).to.deep.equal({ 19 | stopColor: '#ffffff', 20 | }); 21 | }); 22 | }); 23 | 24 | describe('removePixelsFromNodeValue', () => { 25 | it('removes pixels from x, y, height and width attributes', () => { 26 | expect(removePixelsFromNodeValue({nodeName: 'x', nodeValue: '2px'})).to.deep.equal({nodeName: 'x', nodeValue: '2'}); 27 | expect(removePixelsFromNodeValue({nodeName: 'y', nodeValue: '4px'})).to.deep.equal({nodeName: 'y', nodeValue: '4'}); 28 | expect(removePixelsFromNodeValue({nodeName: 'height', nodeValue: '65px'})).to.deep.equal({nodeName: 'height', nodeValue: '65'}); 29 | expect(removePixelsFromNodeValue({nodeName: 'width', nodeValue: '999px'})).to.deep.equal({nodeName: 'width', nodeValue: '999'}); 30 | }); 31 | }) 32 | 33 | describe('camelCase', () => { 34 | it('transforms two word attribute with dash', () => { 35 | expect(camelCase('stop-color')).to.deep.equal('stopColor'); 36 | }); 37 | 38 | it('does not do anything to string that is already camel cased', () => { 39 | expect(camelCase('stopColor')).to.deep.equal('stopColor'); 40 | }); 41 | }); 42 | 43 | describe('getEnabledAttributes', () => { 44 | it('return true when nodeName is found', () => { 45 | const enabledAttributes = ['x', 'y', 'strokeOpacity']; 46 | const hasEnabledAttribute = getEnabledAttributes(enabledAttributes); 47 | 48 | expect(hasEnabledAttribute({nodeName: 'x'})).to.deep.equal(true); 49 | expect(hasEnabledAttribute({nodeName: 'stroke-opacity'})).to.deep.equal(true); 50 | }); 51 | 52 | it('return false when nodeName is not found', () => { 53 | const enabledAttributes = ['width', 'height']; 54 | const hasEnabledAttribute = getEnabledAttributes(enabledAttributes); 55 | 56 | expect(hasEnabledAttribute({nodeName: 'depth'})).to.deep.equal(false); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | export const camelCase = value => value.replace(/-([a-z])/g, g => g[1].toUpperCase()); 2 | 3 | export const camelCaseNodeName = ({nodeName, nodeValue}) => ({nodeName: camelCase(nodeName), nodeValue}); 4 | 5 | export const removePixelsFromNodeValue = ({nodeName, nodeValue}) => ({nodeName, nodeValue: nodeValue.replace('px', '')}); 6 | 7 | export const transformStyle = ({nodeName, nodeValue, fillProp}) => { 8 | if (nodeName === 'style') { 9 | return nodeValue.split(';') 10 | .reduce((acc, attribute) => { 11 | const [property, value] = attribute.split(':'); 12 | if (property == "") 13 | return acc; 14 | else 15 | return {...acc, [camelCase(property)]: fillProp && property === 'fill' ? fillProp : value}; 16 | }, {}); 17 | } 18 | return null; 19 | }; 20 | 21 | export const getEnabledAttributes = enabledAttributes => ({nodeName}) => enabledAttributes.includes(camelCase(nodeName)); 22 | --------------------------------------------------------------------------------