Recently, while working on a React project, I encountered a bug (or requirement) where SVG images weren't getting exported in PDF. The PDF export feature was an existing feature developed earlier by one of my colleagues in the project. He developed this PDF export feature using react-pdf
. As per the requirement, only jpeg
and png
images were considered for export in PDF, which works fine for react-pdf. Everything was working fine until the requirement changed and users were allowed to upload SVG images in the application. Since the SVG image was allowed to be uploaded, it was expected that the SVG should be exported in the PDF as well. This is where things got out of hand. Normal SVGs don't get rendered in react-pdf. React-pdf has its DSL (syntax) to render SVG images. This blog post will explain how I solved this problem of rendering normal SVGs into react-pdf.
What do we want to achieve? (An example)
Suppose, we have the following SVG string which we want to export on pdf.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none" stroke="#000" stroke-width="4" aria-label="Chicken">
<path d="M48.1 34C22.1 32 1.4 51 2.5 67.2c1.2 16.1 19.8 17 29 17.8H89c15.7-6.6 6.3-18.9.3-20.5A28 28 0 0073 41.7c-.5-7.2 3.4-11.6 6.9-15.3 8.5 2.6 8-8 .8-7.2.6-6.5-12.3-5.9-6.7 2.7l-3.7 5c-6.9 5.4-10.9 5.1-22.2 7zM48.1 34c-38 31.9 29.8 58.4 25 7.7M70.3 26.9l5.4 4.2"/>
</svg>
We can see in the above code snippet there are two main tags. One is the main <svg>
tag and the other is the <path>
tag. Now we are going to refer to react-pdf
documentation under the SVG
section. We can see we have both tags under the SVG section. For the svg
it is a <Svg />
tag and for path
it is the <Path>
tag. So the above SVG string will become as follows
<Svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none" stroke="#000" stroke-width="4" aria-label="Chicken">
<Path d="M48.1 34C22.1 32 1.4 51 2.5 67.2c1.2 16.1 19.8 17 29 17.8H89c15.7-6.6 6.3-18.9.3-20.5A28 28 0 0073 41.7c-.5-7.2 3.4-11.6 6.9-15.3 8.5 2.6 8-8 .8-7.2.6-6.5-12.3-5.9-6.7 2.7l-3.7 5c-6.9 5.4-10.9 5.1-22.2 7zM48.1 34c-38 31.9 29.8 58.4 25 7.7M70.3 26.9l5.4 4.2" />
</Svg>
Converting an image from storage URL to SVG string
The main requirement to use react-pdf SVG is that we need to have normal SVG as a string. But in most cases and even in our case as well, we are getting these images i.e. jpg, png, and svg as URL
from the s3 bucket. We need to fetch these images blob and convert it into the SVG string. So, the steps to achieve this are as follows.
First, fetch the image bytes from the API endpoint.
Then convert it to
text
. You can refer to the following code to achieve it.
const response = await fetch(s3_bucket_url,
{
headers: {"x-access-token": token}
});
const rawSVG = await response.text();
const photoContent = rawSVG;
Creating SVG Renderer component
Now comes the meat of the blog i.e. component that will convert the fetched SVG string to a react-pdf
specific SVG. We will create a component SVGRenderer
. It will take svgString
from the parent component where we are fetching the svgString.
export const SVGRenderer = ({ svgString }) => {
// Logic will be here
}
The working of this component can be divided into 5 steps:
Step 1: Convert the SVG string to a javascript object.
Step 2: Convert each tag into a react-pdf
tag
Step 3: Convert styles into style objects compatible with React-Pdf
Step 4: Create an object of each tag to the corresponding react-pdf SVG object.
Step 5: Calling svgToJsx function.
Step 1 - Convert the SVG string to a javascript object
We want our SVG string to be converted into a Javascript object so that we can pluck different properties of SVG and use it to render
react-pdf
specific SVG.For this, we are going to use the
svg-parser
library. The package says that"(It)Takes a string representing an SVG document or fragment, turn it into
HAST
JavaScript object.
We will install the library
npm install svg-parser --save
- We will import the
svg-parser
into the component file and it will convert the string to an object.
import { parse as svgparse } from "svg-parser";
export const SVGRenderer = ({ svgString }) => {
const parsedSVG = svgparse(svgString);
.....
.....
.....
}
Step 2 - Convert each tag into a react-pdf
tag
Each SVG is made up of multiple nested tags. Like in our example in the introduction, there is an svg
tag underneath there is a path
tag. Each tag has its properties and styling. We have to write a recursive function that will pluck the properties, content, and styles of each tag. Later, we will use those properties to construct the react-pdf
SVG object. Refer to the following pseudo-code you well get an idea.
const tag = tag.type === "element" ? tag.tagName : tag.type;
const props = {
style: convertStylesStringToObject(tag.properties.style), // We are yet to implement this function
key: someUniqueKey,
...tag.properties
}
let children = tag.content
Later, we will use the above constructed props to create an SVG tag. Something like this👇
if (tag === "svg") {
return <Svg {...props}>{children}</Svg>;
}
The above sample code gives us some idea about how we are going to create the function. We will name this function svgToJsx
it will take two arguments. The first argument is the tag
we will name it obj
and the other tag is the index
for the unique key. Refer to the following code.
const svgToJsx = (obj, index) => {
let name = obj.type === "element" ? obj.tagName : obj.type;
let props = { key: index + name };
if (obj.properties !== undefined) {
if (obj.properties.style !== undefined) {
props.style = convertStylesStringToObject(obj.properties.style); // Yet to write
}
props = { ...obj.properties, ...props };
}
let children =
obj.children !== undefined ? (
obj.children.map((c, i) => {
return svgToJsx(c, index + "-" + i);
})
) : (
<></>
);
......
......
......
}
Step 3 - Convert styles into style objects compatible with React-Pdf
You may have noticed the convertStylesStringToObject
function mentioned in the svgToJsx
function which is yet to be implemented. Normally in SVG, we write inline CSS as follows:
<circle style="fill:green; margin: 30px;">
Our target is to convert the style into an object as follows
<Circle style={{ fill: 'green', margin: 30 }} />
For this, we are going to write a reducer function that will convert the CSS string into a JSX compatible style object. It will do two tasks:
a) Remove spaces and semicolons
b) Convert two-word(dasherized) attributes into camel case For example, stroke-width
will be converted to strokeWidth
, or background-color
will be converted to backgroundColor
. Refer to the following code for reference.
const convertStylesStringToObject = (stringStyles) => {
let styles =
typeof stringStyles === "string" && stringStyles !== undefined
? stringStyles
.replaceAll(""", "'")
.split(";")
.reduce((acc, style) => {
const colonPosition = style.indexOf(":");
if (colonPosition === -1) return acc;
const camelCaseProperty = style
.substr(0, colonPosition)
.trim()
.replace(/^-ms-/, "ms-")
.replace(/-./g, (c) => c.substr(1).toUpperCase()),
value = style.substr(colonPosition + 1).trim();
let isSvgStyle = [
"color",
"dominantBaseline",
"fill",
"fillOpacity",
"fillRule",
"opacity",
"stroke",
"strokeWidth",
"strokeOpacity",
"strokeLinecap",
"strokeDasharray",
"transform",
"textAnchor",
"visibility"
].includes(camelCaseProperty);
return isSvgStyle && value ? {
...acc,
[camelCaseProperty]: value
} : acc;
}, {})
: {};
return styles;
};
Step 4 - Pass created SVG attributes, props, and styles to react-pdf
SVG tag
We've already seen an example in the introduction where we converted the svg
and path
to Svg
and Path
. So, in the final part of the svgToJsx
function, we will convert each tag into a react-pdf
specific tag as per the name
and will apply styles and content.
if (name === "svg") {
return <Svg {...props}>{children}</Svg>;
}
Also, we will import all the SVG tags in the component.
import {
Svg,
Line,
Polyline,
Polygon,
Path,
Rect,
Circle,
Ellipse,
Text,
Tspan,
G,
Stop,
Defs,
ClipPath,
LinearGradient,
RadialGradient
} from "@react-pdf/renderer";
Step 5: Calling svgToJsx function
In the last step SvgRenderer
component will return the svgToJsx
function and it will pass parsedSVG.children[0]
and an initial index value of 0
as parameters. Refer to the code snippet below.
return <>
{svgToJsx(parsedSVG.children[0], 0)}
</>
Passing the SVG string to the component
Now, we understand the working of the SVGRenderer
component. The only step that is remaining is to pass svg string we fetched from the API to this component. Refer to the following code.
<SVGRenderer svgString={photoContent} />
This completes the entire code explanation of converting SVG to react-pdf
specific SVG. Below is the complete code of the SVGRender
component.
import React from 'react';
import {
Svg,
Line,
Polyline,
Polygon,
Path,
Rect,
Circle,
Ellipse,
Text,
Tspan,
G,
Stop,
Defs,
ClipPath,
LinearGradient,
RadialGradient
} from "@react-pdf/renderer";
import { parse as svgparse } from "svg-parser";
export const SVGRenderer = ({ svgString }) => {
const svgWithoutPixel = svgString.replaceAll("px", "pt");
const parsedSVG = svgparse(svgWithoutPixel);
const convertStylesStringToObject = (stringStyles) => {
let styles =
typeof stringStyles === "string" && stringStyles !== undefined
? stringStyles
.replaceAll(""", "'")
.split(";")
.reduce((acc, style) => {
const colonPosition = style.indexOf(":");
if (colonPosition === -1) return acc;
const camelCaseProperty = style
.substr(0, colonPosition)
.trim()
.replace(/^-ms-/, "ms-")
.replace(/-./g, (c) => c.substr(1).toUpperCase()),
value = style.substr(colonPosition + 1).trim();
let isSvgStyle = [
"color",
"dominantBaseline",
"fill",
"fillOpacity",
"fillRule",
"opacity",
"stroke",
"strokeWidth",
"strokeOpacity",
"strokeLinecap",
"strokeDasharray",
"transform",
"textAnchor",
"visibility"
].includes(camelCaseProperty);
return isSvgStyle && value ? {
...acc,
[camelCaseProperty]: value
} : acc;
}, {})
: {};
return styles;
};
/**
* Rendering logic to convert normal SVG (svgString) to react-pdf compatible SVG
* **/
const svgToJsx = (obj, index) => {
let name = obj.type === "element" ? obj.tagName : obj.type;
let props = { key: index + name };
if (obj.properties !== undefined) {
if (obj.properties.style !== undefined) {
props.style = convertStylesStringToObject(obj.properties.style);
}
props = { ...obj.properties, ...props };
}
let children =
obj.children !== undefined ? (
obj.children.map((c, i) => {
return svgToJsx(c, index + "-" + i);
})
) : (
<></>
);
if (obj.type === "text") {
return obj.value;
}
if (name === "tspan") {
let y = props.y ?? 0 + props.dy ?? 0;
let x = props.x ?? 0 + props.dx ?? 0;
console.log("tspan", children, y);
return children.length > 0 ? (
<Tspan x={x} y={y} key={props.key}>
{children}
</Tspan>
) : (
<></>
);
}
if (name === "text") {
return (
<Text
x={props.x ?? 0 + props.dx ?? 0}
y={props.y ?? 0 + props.dy ?? 0}
key={props.key}
>
{children}
</Text>
);
}
if (name === "svg") {
return <Svg {...props}>{children}</Svg>;
}
if (name === "path") {
return <Path {...props}>{children}</Path>;
}
if (name === "line") {
return <Line {...props}>{children}</Line>;
}
if (name === "polyline") {
return <Polyline {...props}>{children}</Polyline>;
}
if (name === "polygon") {
return <Polygon {...props}>{children}</Polygon>;
}
if (name === "rect") {
return <Rect {...props}>{children}</Rect>;
}
if (name === "circle") {
return <Circle {...props}>{children}</Circle>;
}
if (name === "ellipse") {
return <Ellipse {...props}>{children}</Ellipse>;
}
if (name === "g") {
return <G {...props}>{children}</G>;
}
if (name === "stop") {
return <Stop {...props}>{children}</Stop>;
}
if (name === "defs") {
return (
<>
{/*<Defs {...props}>
{obj.children !== undefined
? obj.children.map((c, i) => {
return <></>;// svgToJsx(c, index+"-"+i);
})
: undefined}
</Defs>*/}
</>
);
}
if (name === "clipPath") {
return <ClipPath {...props}>{children}</ClipPath>;
}
if (name === "linearGradient") {
return <LinearGradient {...props}>{children}</LinearGradient>;
}
if (name === "radialGradient") {
return <RadialGradient {...props}>{children}</RadialGradient>;
}
};
return <>
{svgToJsx(parsedSVG.children[0], 0)}
</>
}
I hope you like this blog. If you have any questions please comment below. Thanks for reading 😊.