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

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

Recently, while working on a React project, I encountered a bug (or requirement) that SVG images aren't getting exported in PDF. The PDF export feature was an existing feature earlier developed by one of my colleagues in the project. He developed this pdf export feature using react-pdf. As per requirement, only jpeg and png images were considered to export in pdf which works fine for react-pdf. Everything was working fine until the requirement changed and the user was 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 solve this problem of rendering normal SVG 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("&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.

   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("&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

Did you find this article valuable?

Support AbulAsar S. by becoming a sponsor. Any amount is appreciated!