react-native-svg-uri
Version:
Render an SVG Image from an URL
309 lines (268 loc) • 9.03 kB
JavaScript
import React, {Component} from "react";
import {View} from 'react-native';
import PropTypes from 'prop-types'
import xmldom from 'xmldom';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
import Svg,{
Circle,
Ellipse,
G ,
LinearGradient,
RadialGradient,
Line,
Path,
Polygon,
Polyline,
Rect,
Text,
TSpan,
Defs,
Stop
} from 'react-native-svg';
import * as utils from './utils';
const ACCEPTED_SVG_ELEMENTS = [
'svg',
'g',
'circle',
'path',
'rect',
'defs',
'line',
'linearGradient',
'radialGradient',
'stop',
'ellipse',
'polygon',
'polyline',
'text',
'tspan'
];
// Attributes from SVG elements that are mapped directly.
const SVG_ATTS = ['viewBox', 'width', 'height'];
const G_ATTS = ['id'];
const CIRCLE_ATTS = ['cx', 'cy', 'r'];
const PATH_ATTS = ['d'];
const RECT_ATTS = ['width', 'height'];
const LINE_ATTS = ['x1', 'y1', 'x2', 'y2'];
const LINEARG_ATTS = LINE_ATTS.concat(['id', 'gradientUnits']);
const RADIALG_ATTS = CIRCLE_ATTS.concat(['id', 'gradientUnits']);
const STOP_ATTS = ['offset'];
const ELLIPSE_ATTS = ['cx', 'cy', 'rx', 'ry'];
const TEXT_ATTS = ['fontFamily', 'fontSize', 'fontWeight']
const POLYGON_ATTS = ['points'];
const POLYLINE_ATTS = ['points'];
const COMMON_ATTS = ['fill', 'fillOpacity', 'stroke', 'strokeWidth', 'strokeOpacity', 'opacity',
'strokeLinecap', 'strokeLinejoin',
'strokeDasharray', 'strokeDashoffset', 'x', 'y', 'rotate', 'scale', 'origin', 'originX', 'originY'];
let ind = 0;
function fixYPosition (y, node) {
if (node.attributes) {
const fontSizeAttr = Object.keys(node.attributes).find(a => node.attributes[a].name === 'font-size');
if (fontSizeAttr) {
return '' + (parseFloat(y) - parseFloat(node.attributes[fontSizeAttr].value));
}
}
if (!node.parentNode) {
return y;
}
return fixYPosition(y, node.parentNode)
}
class SvgUri extends Component{
constructor(props){
super(props);
this.state = {fill: props.fill, svgXmlData: props.svgXmlData};
this.createSVGElement = this.createSVGElement.bind(this);
this.obtainComponentAtts = this.obtainComponentAtts.bind(this);
this.inspectNode = this.inspectNode.bind(this);
this.fetchSVGData = this.fetchSVGData.bind(this);
this.isComponentMounted = false;
// Gets the image data from an URL or a static file
if (props.source) {
const source = resolveAssetSource(props.source) || {};
this.fetchSVGData(source.uri);
}
}
componentWillMount() {
this.isComponentMounted = true;
}
componentWillReceiveProps (nextProps){
if (nextProps.source) {
const source = resolveAssetSource(nextProps.source) || {};
const oldSource = resolveAssetSource(this.props.source) || {};
if(source.uri !== oldSource.uri){
this.fetchSVGData(source.uri);
}
}
if (nextProps.svgXmlData !== this.props.svgXmlData) {
this.setState({ svgXmlData: nextProps.svgXmlData });
}
if (nextProps.fill !== this.props.fill) {
this.setState({ fill: nextProps.fill });
}
}
componentWillUnmount() {
this.isComponentMounted = false
}
async fetchSVGData(uri){
let responseXML = null;
try {
const response = await fetch(uri);
responseXML = await response.text();
} catch(e) {
console.error("ERROR SVG", e);
} finally {
if (this.isComponentMounted) {
this.setState({svgXmlData:responseXML});
}
}
return responseXML;
}
// Remove empty strings from children array
trimElementChilden(children) {
for (child of children) {
if (typeof child === 'string') {
if (child.trim.length === 0)
children.splice(children.indexOf(child), 1);
}
}
}
createSVGElement(node, childs){
this.trimElementChilden(childs);
let componentAtts = {};
const i = ind++;
switch (node.nodeName) {
case 'svg':
componentAtts = this.obtainComponentAtts(node, SVG_ATTS);
if (this.props.width) {
componentAtts.width = this.props.width;
}
if (this.props.height) {
componentAtts.height = this.props.height;
}
return <Svg key={i} {...componentAtts}>{childs}</Svg>;
case 'g':
componentAtts = this.obtainComponentAtts(node, G_ATTS);
return <G key={i} {...componentAtts}>{childs}</G>;
case 'path':
componentAtts = this.obtainComponentAtts(node, PATH_ATTS);
return <Path key={i} {...componentAtts}>{childs}</Path>;
case 'circle':
componentAtts = this.obtainComponentAtts(node, CIRCLE_ATTS);
return <Circle key={i} {...componentAtts}>{childs}</Circle>;
case 'rect':
componentAtts = this.obtainComponentAtts(node, RECT_ATTS);
return <Rect key={i} {...componentAtts}>{childs}</Rect>;
case 'line':
componentAtts = this.obtainComponentAtts(node, LINE_ATTS);
return <Line key={i} {...componentAtts}>{childs}</Line>;
case 'defs':
return <Defs key={i}>{childs}</Defs>;
case 'linearGradient':
componentAtts = this.obtainComponentAtts(node, LINEARG_ATTS);
return <LinearGradient key={i} {...componentAtts}>{childs}</LinearGradient>;
case 'radialGradient':
componentAtts = this.obtainComponentAtts(node, RADIALG_ATTS);
return <RadialGradient key={i} {...componentAtts}>{childs}</RadialGradient>;
case 'stop':
componentAtts = this.obtainComponentAtts(node, STOP_ATTS);
return <Stop key={i} {...componentAtts}>{childs}</Stop>;
case 'ellipse':
componentAtts = this.obtainComponentAtts(node, ELLIPSE_ATTS);
return <Ellipse key={i} {...componentAtts}>{childs}</Ellipse>;
case 'polygon':
componentAtts = this.obtainComponentAtts(node, POLYGON_ATTS);
return <Polygon key={i} {...componentAtts}>{childs}</Polygon>;
case 'polyline':
componentAtts = this.obtainComponentAtts(node, POLYLINE_ATTS);
return <Polyline key={i} {...componentAtts}>{childs}</Polyline>;
case 'text':
componentAtts = this.obtainComponentAtts(node, TEXT_ATTS);
if (componentAtts.y) {
componentAtts.y = fixYPosition(componentAtts.y, node)
}
return <Text key={i} {...componentAtts}>{childs}</Text>;
case 'tspan':
componentAtts = this.obtainComponentAtts(node, TEXT_ATTS);
if (componentAtts.y) {
componentAtts.y = fixYPosition(componentAtts.y, node)
}
return <TSpan key={i} {...componentAtts}>{childs}</TSpan>;
default:
return null;
}
}
obtainComponentAtts({attributes}, enabledAttributes) {
const styleAtts = {};
Array.from(attributes).forEach(({nodeName, nodeValue}) => {
Object.assign(styleAtts, utils.transformStyle({
nodeName,
nodeValue,
fillProp: this.state.fill
}));
});
const componentAtts = Array.from(attributes)
.map(utils.camelCaseNodeName)
.map(utils.removePixelsFromNodeValue)
.filter(utils.getEnabledAttributes(enabledAttributes.concat(COMMON_ATTS)))
.reduce((acc, {nodeName, nodeValue}) => {
acc[nodeName] = (this.state.fill && nodeName === 'fill' && nodeValue !== 'none') ? this.state.fill : nodeValue
return acc
}, {});
Object.assign(componentAtts, styleAtts);
return componentAtts;
}
inspectNode(node){
// Only process accepted elements
if (!ACCEPTED_SVG_ELEMENTS.includes(node.nodeName)) {
return null;
}
// Process the xml node
const arrayElements = [];
// if have children process them.
// Recursive function.
if (node.childNodes && node.childNodes.length > 0){
for (let i = 0; i < node.childNodes.length; i++){
const isTextValue = node.childNodes[i].nodeValue
if (isTextValue) {
arrayElements.push(node.childNodes[i].nodeValue)
} else {
const nodo = this.inspectNode(node.childNodes[i]);
if (nodo != null) {
arrayElements.push(nodo);
}
}
}
}
return this.createSVGElement(node, arrayElements);
}
render () {
try {
if (this.state.svgXmlData == null) {
return null;
}
const inputSVG = this.state.svgXmlData.substring(
this.state.svgXmlData.indexOf("<svg "),
(this.state.svgXmlData.indexOf("</svg>") + 6)
);
const doc = new xmldom.DOMParser().parseFromString(inputSVG);
const rootSVG = this.inspectNode(doc.childNodes[0]);
return(
<View style={this.props.style}>
{rootSVG}
</View>
);
} catch(e){
console.error("ERROR SVG", e);
return null;
}
}
}
SvgUri.propTypes = {
style: PropTypes.object,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
svgXmlData: PropTypes.string,
source: PropTypes.any,
fill: PropTypes.string,
}
module.exports = SvgUri;