diegomura / react-pdf

📄 Create PDF files using React
https://react-pdf.org
MIT License
14.72k stars 1.17k forks source link

React-pdf/renderer SVG text not rendering correctly #2382

Open Neuroforge opened 1 year ago

Neuroforge commented 1 year ago

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 .

Sadly https://react-pdf.org/repl does not understand object destructuring so i was not able to replicate.

Screenshot 2023-08-21 at 4 01 11 am

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

Screenshot 2023-08-21 at 2 55 33 am Screenshot 2023-08-21 at 2 55 22 am

Desktop (please complete the following information):

I've tried mutating all of the properties from kebab-case to camelCase, but no luck with that.

Neuroforge commented 11 months ago

No joy?

jessypouliot98 commented 10 months ago

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