# Creating a React component to render normal SVG in react-pdf.

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.

```elixir
<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`](https://react-pdf.org/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

```elixir
<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.
    

```javascript
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.

```javascript
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`](https://github.com/syntax-tree/hast)`JavaScript object.`
    
* We will install the library
    

```bash
  npm install svg-parser --save
```

* We will import the `svg-parser` into the component file and it will convert the string to an object.
    

```javascript
   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.

```elixir
  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👇

```elixir
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.

```elixir
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:

```html
<circle style="fill:green; margin: 30px;">
```

Our target is to convert the style into an object as follows

```elixir
<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.

```elixir
const convertStylesStringToObject = (stringStyles) => {
    let styles =
      typeof stringStyles === "string" && stringStyles !== undefined
        ? stringStyles
          .replaceAll("&quot;", "'")
          .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.

```elixir
   if (name === "svg") {
      return <Svg {...props}>{children}</Svg>;
   }
```

Also, we will import all the SVG tags in the component.

```elixir
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.

```elixir
  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.

```javascript
 <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.

```javascript
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("&quot;", "'")
          .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 😊.

### References

* [React PDF](https://react-pdf.org/svg)
    
* [Github.com (Thank you @dennemark for creating the issue and sharing the idea)](https://github.com/diegomura/react-pdf/issues/1234)
