react-pdf icon indicating copy to clipboard operation
react-pdf copied to clipboard

Render custom HTML markup inside pdf document

Open abollam opened this issue 5 years ago • 24 comments

Hi all, I am trying to render HTML inside the <View>/<Text> component while rendering the PDF inside the <PDFViewer> using a Template.

I'd like to render my custom HTML code shown below in my pdf

<p><span style="font-size: 24px;">Pre Interview Notes</span></p><p><br></p><p><strong style="font-size: 14px;"><u>This is a test Pre Interview Notes:</u></strong></p><p><br></p><p><em style="font-size: 14px;">The Guest requires a wheel chair for the show</em></p><p><br></p><p><br></p>

Environment

  • Browser [e.g. Chrome ]:
  • React-PDF version [1.6.8]:
  • React version [e.g. 16.8.6]:

abollam avatar May 04 '20 16:05 abollam

I'm also doing exactly this.

The way I'm doing it is to convert the html into json structure, then parse the json structure into @react-pdf elements. So you have a routine for that converts <p> elements into paragraph Views and then places children <span> elements as nested Text children etc.

That could be a problem if you're not sure what the incoming HTML is going to be - you'll have to do a complete parser for every html element.

tomgallagher avatar May 10 '20 09:05 tomgallagher

Hi all, I am trying to render HTML inside the <View>/<Text> component while rendering the PDF inside the <PDFViewer> using a Template.

I'd like to render my custom HTML code shown below in my pdf

<p><span style="font-size: 24px;">Pre Interview Notes</span></p><p><br></p><p><strong style="font-size: 14px;"><u>This is a test Pre Interview Notes:</u></strong></p><p><br></p><p><em style="font-size: 14px;">The Guest requires a wheel chair for the show</em></p><p><br></p><p><br></p>

Environment

  • Browser [e.g. Chrome ]:
  • React-PDF version [1.6.8]:
  • React version [e.g. 16.8.6]:

Same here, Im trying to render my data coming from CKEditor. Any Updates?

alvnrapada avatar Sep 01 '20 09:09 alvnrapada

@tomgallagher don't suppose you have a basic example of how you're doing this? I need to render html from tinymce

stevenbrown-85 avatar Sep 09 '20 22:09 stevenbrown-85

@stevenbrown-85 it's a bit more involved than a basic example but I can give you some pointers.

HTML and the mirror react-pdf code are both tree structures, so you need two different functions to walk/save the tree and then rebuild it. You're looking at recursion to do this.

As you are using tinyMCE, you're in luck as you can limit the html structures that can be in your tree, you don't need to do every single HTML element. One thing to note is that tinyMCE onEditorChange event just provides a list of HTML elements, so you need to provide a 'wrapper' parent element for the below to work. I use a document fragment.

Look at this function: dom-to-json, which is where I started. The tree structure is preserved by using a childNodes array of sub-nodes. So you start with a document fragment with all the tinyMCE elements as childNodes.

Then you will need to write your own json-to-pdf function. Mine has an entry point that looks like this

export const JsonToPdfComponent = (input, bodyTextColorString) => {
    
    /*
        the object was created with following values 
        { 
            nodeType: <integer>, 
            nodeTagName: <string>, 
            nodeName: <string>, 
            nodeValue: <string>, 
            childNodes: <array> 
            attributes: <array of attributes arrays>
        }
    */
    
    //first we have an error handler which will result in blank elements in the pdf rather than crashes
    if (input === undefined) { console.error("JsonToPdfComponent: Undefined JSON Input"); return null; }
    //create the object, either parsing a JSON string or just using an existing javascript object
    let json = typeof input === 'string' ? JSON.parse(input) : input;
    //define the node type
    let nodeType = json.nodeType;
    //define the tag type
    let tagType = json.nodeTagName;
    //then the construction process is different depending on the type of node
    switch (nodeType) {
        //MOST OF THE WORK DONE ON ELEMENT_NODES
        case 1: 
            //then we need to create styled component views for each tag
            switch(tagType) {
                case 'h1':
                   //SEE EXAMPLE BELOW
                    return createH1ViewElement(json, bodyTextColorString);
                case 'h2':
                    return createH2ViewElement(json, bodyTextColorString);
                case 'h3':
                    return createH3ViewElement(json, bodyTextColorString);
                case 'h4':
                    return createH4ViewElement(json, bodyTextColorString);
                case 'h5':
                    return createH5ViewElement(json, bodyTextColorString);
                case 'h6':
                    return createH6ViewElement(json, bodyTextColorString);
                case 'strong':
                    return createStrongTextElement(json, bodyTextColorString);
                case 'em':
                    return createEmphasisTextElement(json, bodyTextColorString);
                case 'p':
                    return createParagraphTextElement(json, bodyTextColorString);
                case 'span':
                    return createSpanTextElement(json, bodyTextColorString);
                //we add a link tag only when the anchor tag has children, i.e. text
                case 'a':
                    return createLinkElement(json, bodyTextColorString);
                case 'img':
                    return createImageElement(json);
                //special processing for ordered and unordered list components
                case 'ol':
                    return createListComponent(json, bodyTextColorString, tagType);
                case 'ul':
                    return createListComponent(json, bodyTextColorString, tagType);
                //special processing for tables
                case 'table':
                    return createTableComponent(json, bodyTextColorString);
                //special processing for sup text
                case 'sup':
                    return createSupComponent(json, bodyTextColorString);
                default:
                    console.log(`No Processing for Tag ${tagType.toUpperCase()}`);
            }
            break;
        //TEXT_NODE - we can just create the simple text item
        case 3: 
            //this will return a null value if the text filtered for formatting characters has a length of zero
            return createTextComponent(json.nodeValue);
        default: 
            console.log("Skipping Node", json);
    }

};

Note that I do some intermediate processing, so there's a crucial bit missing from this code, which is how to process the parent fragment element. You would do this by case 11, where you would, for example, create the react-pdf Page element using a routine like the following code sample and then all the childNodes accordingly.

To rebuild, you use React.createElement for each node, so for a simple h1 example:

import { View, Text } from '@react-pdf/renderer';

const createH1ViewElement = (node, bodyTextColorString) => {
    
    return React.createElement(
        //every html element should have its own view
        View,
        //add the special style props to the view, if you have any, from the JSON attributes array
        {
            //stops headers appearing at the bottom of the page
            minPresenceAhead: 1,
        },
        //then we pass in the children of the json object, as children of the React element
        React.createElement(
            Text,
            //add the special style props to the text element
            {
                //add the custom styles you have set for the pdf component
                style: {...dynamicStyles.styledHeader1, color: bodyTextColorString},
            },
            //then we pass in the children of the json object = recursion or the end of recursion
            node.childNodes.map(child => JsonToPdfComponent(child, bodyTextColorString))
        )
    );
    
};

That should get you started with basic text. Lists and tables are a bit tougher to get right.

Styles need to be converted from CSS to something understood by react-pdf. TinyMCE does most of the styling with span elements.

If you have all the default fonts from tinyMCE, they will need to be loaded from somewhere. Images are also a pain as they have CORS issues.

Hope that helps a bit.

There's a big project potentially for react-pdf that converts any HTML string to pdf.

Tom

tomgallagher avatar Sep 10 '20 06:09 tomgallagher

Thanks @tomgallagher - very helpful, much appreciated. I'll give this a bash over next few days and see how i get on. I have disabled fonts and images for tinymce so they wont be a problem - dealing with tables will be interesting though!

stevenbrown-85 avatar Sep 10 '20 10:09 stevenbrown-85

@stevenbrown-85, no problem and good luck! The lists and tables are a bit annoying but the worst bit was inline <sup> elements. There's no good way to do those in @react-pdf, whatever you might read in related issues.

tomgallagher avatar Sep 10 '20 11:09 tomgallagher

For later readers, I think that parsing HTML string is the hardest part and react-html-parser can help.

It will parse the string to ReactElement[] so that we can map the ReactElement to React-PDF elements.

iamacoderguy avatar Dec 16 '20 11:12 iamacoderguy

For later readers, I think that parsing HTML string is the hardest part and react-html-parser can help.

It will parse the string to ReactElement[] so that we can map the ReactElement to React-PDF elements.

@iamacoderguy Hello, I do not understand how to map ReactElement to ReactPdfElements and I could not find anything on it. Could you please elaborate on that?

DubstepKnight avatar Jan 15 '21 15:01 DubstepKnight

Here's my simple method. Html Render page

const parsedHtml = htmlParser(taskDescription);
return (
<View>
      <Text>Description:</Text>
          {parsedHtml}
  </View>
)
 

html-parser:


import ReactHtmlParser from 'react-html-parser';
import { Text } from '@react-pdf/renderer';
import React from 'react';

export const htmlParser = (taskDescription: string | null) => {
  let returnContentConst;
  if (taskDescription) {
    const parsedHtml = ReactHtmlParser(taskDescription);
    parsedHtml.forEach(element => {
      const type = element.type;

      element.props.children.forEach((content: string) => {
        switch (type) {
          case 'p':
            returnContentConst = (<Text>{content}</Text>)
            break;
          default:
            returnContentConst = (<Text>{content}</Text>)
            break;
        }
      })
    })
    return returnContentConst;
  } else {
    return returnContentConst;
  }
}

ArnasDickus avatar Jan 28 '21 13:01 ArnasDickus

@ArnasDickus Thanks! I appreciate it, Will try it once we start refactoring the code and adding more features.

DubstepKnight avatar Feb 16 '21 09:02 DubstepKnight

Using node-html-parser, I assembled a some helper functions and an <Html> tag here: https://github.com/danomatic/react-pdf-html

Feedback would be welcome! I'm hoping it can become a standardized part of the react-pdf library at some point. Ideally we can make it extensible so that users can implement custom tag renderers.

danomatic avatar Jun 09 '21 00:06 danomatic

Here's my simple method. Html Render page

const parsedHtml = htmlParser(taskDescription);
return (
<View>
      <Text>Description:</Text>
          {parsedHtml}
  </View>
)
 

html-parser:


import ReactHtmlParser from 'react-html-parser';
import { Text } from '@react-pdf/renderer';
import React from 'react';

export const htmlParser = (taskDescription: string | null) => {
  let returnContentConst;
  if (taskDescription) {
    const parsedHtml = ReactHtmlParser(taskDescription);
    parsedHtml.forEach(element => {
      const type = element.type;

      element.props.children.forEach((content: string) => {
        switch (type) {
          case 'p':
            returnContentConst = (<Text>{content}</Text>)
            break;
          default:
            returnContentConst = (<Text>{content}</Text>)
            break;
        }
      })
    })
    return returnContentConst;
  } else {
    return returnContentConst;
  }
}

Hey @ArnasDickus Thanks for the posting, but I am getting an error Error: Invalid element of type div passed to PDF renderer. Do you know why I am getting the error?

hotcakedev628 avatar Jun 15 '21 03:06 hotcakedev628

any updates on this?

brxshncy avatar Jul 15 '21 12:07 brxshncy

@brxshncy

any updates on this?

I've continued working on https://github.com/danomatic/react-pdf-html. Please give it a try and let me know what you think!

danomatic avatar Jul 16 '21 22:07 danomatic

@brxshncy

any updates on this?

I've continued working on https://github.com/danomatic/react-pdf-html. Please give it a try and let me know what you think!

Hello @danomatic , it says not available as an NPM package, how do I install this on an existing project? what specific files to download

brxshncy avatar Jul 19 '21 09:07 brxshncy

@brxshncy I have not bundled and published as an npm package in the event that the author of react-pdf was interested in including it. To use it at this time, just copy the files from src in the repo and make sure you have these dependencies:

  • @react-pdf/renderer
  • camelize
  • css
  • node-html-parser
  • typescript

danomatic avatar Jul 23 '21 21:07 danomatic

@diegomura are you interested in including https://github.com/danomatic/react-pdf-html as part of react-pdf so that it can be maintained together or would you prefer that the Html component be maintained separately?

danomatic avatar Jul 23 '21 21:07 danomatic

Hi @danomatic, while we wait for @diegomura to consider including react-pdf-html as part of react-pdf, would it be possible for you to publish https://github.com/danomatic/react-pdf-html to npm? I have a use-case that would definitely benefit from npm availability. Thanks!

thismax avatar Jul 30 '21 16:07 thismax

@thismax and @brxshncy I just published it to NPM:

https://www.npmjs.com/package/react-pdf-html

danomatic avatar Aug 14 '21 04:08 danomatic

@danomatic brilliant, thank you!

thismax avatar Sep 02 '21 20:09 thismax

@danomatic You're Amazing!

Luisparr14 avatar Mar 06 '23 19:03 Luisparr14

Edit from @danomatic, a version that handles nested elements and elements if yall need it.

import ReactHtmlParser from "react-html-parser";
import { Text } from "@react-pdf/renderer";
import React from "react";

interface ParsedElementProps {
  children: React.ReactNode;
  [key: string]: any;
}

export const htmlParser = (taskDescription: string | null): JSX.Element => {
  const parseElements = (elements: React.ReactNode): React.ReactNode[] => {
    const returnContentConst: React.ReactNode[] = [];

    React.Children.forEach(elements, (element) => {
      if (typeof element === "string") {
        // Handle string content
        returnContentConst.push(<Text key={Math.random()}>{element}</Text>);
      } else if (React.isValidElement(element)) {
        const elementProps = element.props as ParsedElementProps;
        const type = element.type;
        const children = parseElements(elementProps.children);

        switch (type) {
          case "p":
            returnContentConst.push(
              <Text key={Math.random()}>{children}</Text>
            );
            break;
          case "strong":
            returnContentConst.push(
              <Text
                key={Math.random()}
                style={{ fontFamily: "Helvetica-Bold", fontWeight: 600 }}
              >
                {children}
              </Text>
            );
            break;
          // Add more cases as needed for other HTML tags
          default:
            returnContentConst.push(
              <Text key={Math.random()}>{children}</Text>
            );
            break;
        }
      }
    });

    return returnContentConst;
  };

  if (taskDescription) {
    const parsedHtml = ReactHtmlParser(taskDescription);

    const returnContentConst = parseElements(parsedHtml);

    return <>{returnContentConst}</>;
  } else {
    return <></>;
  }
};

Hopes this help!

geralddevelop avatar Jul 13 '24 03:07 geralddevelop

@geralddevelop this is nice! I'm thinking of switching react-pdf-html to use react-html-parser for its parsing. I like it's transform callback option.

danomatic avatar Jul 21 '24 22:07 danomatic

Para los lectores posteriores, creo que analizar cadenas HTML es la parte más difícil y react-html-parser puede ayudar. Analizará la cadena como ReactElement[] para que podamos asignar ReactElement a los elementos React-PDF.

@iamacoderguy Hola, no entiendo cómo mapear ReactElement a ReactPdfElements y no pude encontrar nada al respecto. ¿Podrías explicarme más sobre eso?

you can do it in the following way, The only solution for this was to convert the hmtl to a react component with html-to-react.

`const HtmlToReactParser = HtmlToReact.Parser; const htmlToReactParser = new HtmlToReactParser();

const HtmlText = ({ html }) => { const reactElement = htmlToReactParser.parse(html); const convertElement = (element) => { if (!element) return null;

const { type, props } = element;
const { children } = props;

switch (type) {
  case 'h1':
    return <Text style={styles.heading}>{children.map(convertElement)}</Text>;
  case 'h2':
    return <Text style={styles.subheading}>{children.map(convertElement)}</Text>;
  case 'p':
    return <Text style={styles.paragraph}>{children.map(convertElement)}</Text>;
  case 'b':
    return <Text style={styles.bold}>{children.map(convertElement)}</Text>;
  case 'i':
    return <Text style={styles.italic}>{children.map(convertElement)}</Text>;
  case 'u':
    return <Text style={styles.underline}>{children.map(convertElement)}</Text>;
  default:
    return <Text>{children.map(convertElement)}</Text>;
}

};

return convertElement(reactElement); };`

erickfabiandev avatar Aug 07 '24 23:08 erickfabiandev

Will close as this won't be something we will add. We can't render arbitrary elements in the PDF, we need to stick with known primitives

diegomura avatar Mar 03 '25 19:03 diegomura

i am building a resume builder and when i try to export resume my desing breaks from edges or if content goes on next page i tried jspdf, html2canvas , reactrenderer but nothing works ...

Rishikesh-00 avatar Oct 04 '25 19:10 Rishikesh-00