Last active
August 29, 2023 09:47
-
-
Save dennemark/5f0f3d7452d9334f9349172db6c40f74 to your computer and use it in GitHub Desktop.
SVG to react-pdf/renderer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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