react-pdf
react-pdf copied to clipboard
React-pdf/renderer SVG text not rendering correctly
Describe the bug SVG text ignoring props and not rendering correctly..
To Reproduce Steps to reproduce the behavior including code snippet (if applies):
I have gone into some detail here. https://stackoverflow.com/questions/76940497/react-pdf-renderer-svg-text-not-rendering-correctly
The original SVG can be obtained here, when parsed as a string it is the input to the function below - https://pastebin.com/UAbRimdn
The parsed json object can be found here - https://pastebin.com/6pZ8qsxF
The original SVG is the output of a vega chart spec and renders correctly (in a browser/gimp etc). As far as i can tell the json object is a true representation of the svg and contains all of the properties present in the original svg. I will look into that some more.
The additional library converts from a svg string (vega output) to a parseable json representation of the svg image. https://www.npmjs.com/package/svg-parser
This is my main parsing method that takes the svg, as a string, and converts it to React-pdf/renderer <Svg/>.
Sadly https://react-pdf.org/repl does not understand object destructuring so i was not able to replicate.
However, here is the code i have written.
import React from "react";
import { Path, Svg, Text, G } from "@react-pdf/renderer";
import { parse } from "svg-parser";
const convertJsonToPdfComponent = (jsonElement: any) => {
if (jsonElement.type === "element") {
const { tagName, properties, children } = jsonElement;
console.log("Converting element", tagName, properties, children);
// Handle different types of elements
switch (tagName) {
case "svg":
console.log("SVG properties", properties);
return (
<Svg {...properties}>
{children?.map((child: any) => convertJsonToPdfComponent(child))}
</Svg>
);
case "g":
console.log("G properties", properties);
return (
<G {...properties}>
{children?.map((child: any) => convertJsonToPdfComponent(child))}
</G>
);
case "path":
console.log("Path properties", properties);
return (
<Path {...properties}>
{children?.map((child: any) => convertJsonToPdfComponent(child))}
</Path>
);
case "text":
properties["font-family"] = "Montserrat";
console.log("Text properties", properties);
return (
<Text {...properties}>
{children?.map((child: any) => convertJsonToPdfComponent(child))}
</Text>
);
default:
return null;
}
} else if (jsonElement.type === "text") {
return jsonElement.value;
}
return null;
};
export const recursivelyConvertJsonToPdfComponents = (svgString: string) => {
console.log("Parsing SVG");
const svgObject = parse(svgString);
return svgObject.children.map((child: any) =>
convertJsonToPdfComponent(child)
);
};
You can make use of react-pdf REPL to share the snippet
Expected behavior The chart text should appear in the correctly location and be the correct size.
Screenshots
Desktop (please complete the following information):
- OS: MacOS
- Browser Chrome
- React-pdf version "@react-pdf/renderer": "^3.1.12",
I've tried mutating all of the properties from kebab-case to camelCase, but no luck with that.
No joy?
Hey I had a similar case to this where I tried extracting icons from react-icons
to something that would work in react-pdf and this is what worked for me. Not perfect, but this might help you out
import { renderToString } from "react-dom/server";
import NodeHtmlParser from "node-html-parser";
import HTMLElement from "node-html-parser/dist/nodes/html";
const SvgMapper = (
{ style, svg, currentColor, height, width }: {
style?: Pdf.SVGProps["style"];
svg: React.ReactElement;
currentColor: string;
height: number;
width: number;
}
) => {
const html = renderToString(svg);
const node = NodeHtmlParser.parse(html);
type NhpNode = (typeof node.childNodes)[number];
const mapSvgShape = (node: NhpNode, index = 0) => {
if (node.nodeType === NodeHtmlParser.NodeType.COMMENT_NODE) {
return null;
}
if (node.nodeType === NodeHtmlParser.NodeType.TEXT_NODE) {
return node.rawText;
}
const el = node as HTMLElement;
const shapes = el.childNodes.map(mapSvgShape);
switch (el.rawTagName) {
case null: {
return (
<React.Fragment key={index}>
{shapes}
</React.Fragment>
);
}
case "svg": {
return (
<Pdf.Svg
key={index}
style={style}
viewBox={el.attributes.viewBox}
// No support for `1em`
// height={el.attributes.height}
// width={el.attributes.width}
height={height}
width={width}
>
<Pdf.G
stroke={el.attributes.stroke === "currentColor" ? currentColor : el.attributes.stroke}
fill={el.attributes.fill === "currentColor" ? currentColor : el.attributes.fill}
strokeWidth={el.attributes["stroke-width"]}
>
{shapes}
</Pdf.G>
</Pdf.Svg>
);
}
case "path": {
return (
<Pdf.Path key={index} d={el.attributes.d} />
);
}
case "title": return null;
default: {
console.log(el);
return null;
}
}
};
return mapSvgShape(node) as React.ReactElement;
};
// Usage
<SvgMapper
currentColor={"black"}
svg={<SomeIcon />}
height={14}
width={14}
/>
PS: Notice how the svg case has a nested Pdf.G because Pdf.Svg doesn't have stroke/fill props!
You may need to implement other svg shapes like line, rect, etc., etc. I didn't need them