Skip to content

Instantly share code, notes, and snippets.

@dennemark
Last active August 29, 2023 09:47
Show Gist options
  • Save dennemark/5f0f3d7452d9334f9349172db6c40f74 to your computer and use it in GitHub Desktop.
Save dennemark/5f0f3d7452d9334f9349172db6c40f74 to your computer and use it in GitHub Desktop.
SVG to react-pdf/renderer
import "./styles.css";
import { useMemo, createElement } from "react";
import { parse, TextNode, ElementNode, RootNode } from "svg-parser";
import {
Document,
Page,
PDFDownloadLink,
} from "@react-pdf/renderer";
const svgpic = `<svg xmlns="http://www.w3.org/2000/svg"
width="467" height="462"><rect x="80" y="60" width="250" height="250" rx="20"
style="fill:#ff0000; stroke:#000000;stroke-width:2px;" /><rect x="140" y="120" width="250" height="250" rx="40"
style="fill:#0000ff; stroke:#000000; stroke-width:2px;
fill-opacity:0.7;" /></svg>`;
const svgpic2 = `<svg xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" xmlns="http://www.w3.org/2000/svg" style="font-family:Helvetica;font-size:12px;" width="600" height="600" viewBox="0 0 600 600">
<g data-z-index="0.1" opacity="1" transform="translate(590,509) rotate(90) scale(-1,1) scale(1 1)">
<rect x="344.5" y="291.5" width="50" height="204" fill="#A9DACC" stroke="#ffffff" stroke-width="1" opacity="1" class="highcharts-point highcharts-color-0"></rect>
<rect x="120.5" y="51.5" width="50" height="444" fill="#A9DACC" stroke="#ffffff" stroke-width="1" opacity="1" class="highcharts-point highcharts-color-0"></rect>
</g>
</svg>`
const supportedStyleProps = [
"color",
"dominantBaseline",
"fill",
"fillOpacity",
"fillRule",
"opacity",
"stroke",
"strokeWidth",
"strokeOpacity",
"strokeLinecap",
"strokeDasharray",
"transform",
"textAnchor",
"visibility"
]
function isElementNode(node: TextNode | ElementNode): boolean {
return node.type === 'element'
}
function removeLineBreaks(text?: string | number | boolean) {
if (typeof text === 'string') {
return text.replace(/(\r\n|\n|\r)/gm, "")
}
return text;
}
// https://dev.to/qausim/convert-html-inline-styles-to-a-style-object-for-react-components-2cbi
const formatStringToCamelCase = (str: string) => {
const splitted = str.split("-");
if (splitted.length === 1) return splitted[0];
return (
splitted[0] +
splitted
.slice(1)
.map((word) => word[0].toUpperCase() + word.slice(1))
.join("")
);
};
const getStyleObjectFromString = (str: string | null) => {
const style: any = {};
if (!str) return {};
str.split(";").forEach((el) => {
let [property, value] = el.split(":");
if (!property) return;
if (property === "cursor") return;
const formattedProperty = formatStringToCamelCase(property.trim());
if (supportedStyleProps.includes(formattedProperty)) {
if(formattedProperty === "strokeDasharray"){
value = value.replace(/pt/g, "") //dasharray has now px
}
style[formattedProperty] = value.trim();
}
});
return style;
};
function handleRelativePositioning(node: ElementNode, parentX?: number, parentY?: number) {
return {
x: (Number(node.properties?.x ?? parentX ?? 0)) + Number(node.properties?.dx ?? 0),
y: (Number(node.properties?.y ?? parentY ?? 0)) + Number(node.properties?.dy ?? 0)
};
}
function getParentPosition(pos: number | string | undefined) {
if (!pos) return 0;
if (typeof pos === 'string') return Number(pos);
return pos;
}
function svgToJSXWithRelPositioning(
node: TextNode | ElementNode | string, key?: string, parentX?: number, parentY?: number
): any {
if (typeof node === 'string') {
return removeLineBreaks(node);
}
if (!isElementNode(node)) {
return removeLineBreaks(node.value);
}
const elementName = node.tagName;
if (!elementName) {
console.log('NO TAG NAME: ', node);
return null;
}
let componentProps;
if (node.tagName === 'desc' || node.tagName === 'defs') return null;
if (node.properties !== undefined) {
if (node.tagName === "text" || node.tagName === "tspan" || node.tagName === "rect") {
componentProps = handleRelativePositioning(node, parentX, parentY);
if(node.tagName !== "rect"){
componentProps = {
...componentProps,
textAnchor: node.properties['text-anchor']
}
}else{
componentProps = {
...node.properties,
...componentProps,
}
}
}else{
componentProps = node.properties;
}
console.log(node, componentProps)
if (node.properties.style) {
componentProps = {
...componentProps,
style: getStyleObjectFromString(node.properties.style as string)
}
}
}
let children = [];
if (node.children && node.children.length > 0) {
children = node.children.map(
(childNode: TextNode | ElementNode | string, i: number) =>
svgToJSXWithRelPositioning(
childNode, key+"-"+i, getParentPosition(node.properties.x), getParentPosition(node.properties.y)
)
)
}else{
children = [""]
}
componentProps = {...componentProps, key: key ?? "root"};
return createElement(elementName.toUpperCase(), componentProps, children);
}
const SvgComponent = ({svgXml}:{svgXml: string}) => {
const svgElement = useMemo(() => {
if (!svgXml || svgXml === "") return <></>;
const svg = svgXml.replace(/px/g, "pt"); //replace all px with pt
const parsed: RootNode = parse(svg);
return svgToJSXWithRelPositioning(parsed.children[0]);
}, [svgXml]);
return <>{svgElement}</>;
};
export default function App() {
return (
<PDFDownloadLink
document={
<Document>
<Page size="A4">
<SvgComponent svgXml={svgpic} />
</Page>
</Document>
}
fileName="somename.pdf"
>
{({ blob, url, loading, error }) =>
loading ? "Loading document..." : "Download now!"
}
</PDFDownloadLink>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment