diegomura / react-pdf

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

Multiple `toBlob` executed at the same time throws TypeError: Cannot read property 'hasGlyphForCodePoint' of null #310

Closed klis87 closed 3 years ago

klis87 commented 6 years ago

OS: Linux manjaro 4.14.67-1-MANJARO

React-pdf version: 1.0.0-alpha.17

Description: I created my custom createPDF function which returns blob instance promise, it works perfectly, but the problem is that if I run multiple calls to this async function, then I sometimes get this error: react-pdf.browser.es.js:3367 Uncaught (in promise) TypeError: Cannot read property 'hasGlyphForCodePoint' of null at eval (react-pdf.browser.es.js:3367) at Array.reduce () at buildSubsetForFont (react-pdf.browser.es.js:3366) at eval (react-pdf.browser.es.js:3376) at Array.map () at ignoreChars (react-pdf.browser.es.js:3375) at getFragments (react-pdf.browser.es.js:3466) at eval (react-pdf.browser.es.js:3447) at Array.forEach () at getFragments (react-pdf.browser.es.js:3422)

How to replicate issue including code snippet (if applies): This is how this function looks like:

const MyDocument = ({ children }) => (
  <Document>
    <Page
      size="A4"
      wrap
    >
      <Text
        render={({ pageNumber, totalPages }) => `${pageNumber} z ${totalPages}`}
        fixed
        style={{ position: 'absolute', bottom: 10, right: 10 }}
      />
      {children}
    </Page>
  </Document>
);

export const createPDF = pdfContent => {
  const instance = pdf();
  instance.updateContainer(<MyDocument>{pdfContent}</MyDocument>);
  return instance.toBlob();
};

And this is where I call it:

const {
  createPDF,
} = await import(/* webpackChunkName: "pdf" */ 'pdf');

const blobs = await Promise.all(
  this.props.attachments.map(a =>
    createPDF(a.pdfContent),
  ),
);

Do you have any idea what could cause this error? Is it forbidden to render multiple pdfs at the same time? Does PDF rendering has any side effects or use some global state?

klis87 commented 6 years ago

It seems to me that this problem only happens if I render multiple pdfs at the same time during the 1st trial. If for instance I call createPDF as single promise, then

const blobs = await Promise.all(
  this.props.attachments.map(a =>
    createPDF(a.pdfContent),
  ),
);

will be always successful.

Also, I am not sure whether this is related, but I register a custom font on app load:

Font.register(`${window.location.origin}${montserratFont}`, {
  family: 'Montserrat',
});
diegomura commented 6 years ago

Odd, Can you provide a way for me to replicate this? Seems like a race condition, and would be very time consuming for me to even try to replicate this

klis87 commented 6 years ago

@diegomura sure, no problem, I will try to manage this today

klis87 commented 6 years ago

@diegomura see the problem at https://codesandbox.io/s/jjjpp84v7y

I confirm that this only happens with custom font, so probably this is some kind of registration font race condition

diegomura commented 6 years ago

Hey @klis87 Thanks for the sandbox!

I saw that the example was using 1.0.0-alpha.15 version of react-pdf. The pdf method API changed a bit on last version, making it much simpler to use (before actually wasn't intended to be part of the public API. It was exposed because legacy reasons). Adding docs is on my todo list 😄

I updated your code sample with the new version and adjust it to the new api, and several blob generations works as expected. You can find it here: https://codesandbox.io/s/nk76lx7p0m

Please try it and report back so we can close this issue

klis87 commented 6 years ago

@diegomura yeah, I tried it also with alpha.17, sry for consusion, I could prepare the example in this version in the 1st place. But Problem still occurs, tr to reload your example and click Get multiple blob, I just got error Unhandled Rejection (TypeError) Cannot read property 'hasGlyphForCodePoint' of null too.

mmichelli commented 5 years ago

You can not use a PDFDownloadLink and PDFViewer with a custom font.

klis87 commented 5 years ago

Just for the info, this issue is still present in 1.1 version. For now I solve it by const = [firstDocument, ...restDocuments] = documents, and awaiting for the firstDocument render, and then wrapping restDocuments in Promise.all of pdf generators.

theobat commented 5 years ago

I got the same problem, will try to reproduce

klis87 commented 5 years ago

@theobat use my sandbox as a base, because it really has it reproduced already - https://codesandbox.io/s/jjjpp84v7y but for older version, you could upgrade too see whether newest version is still affected by this issue.

For now I am still living with workaround I mentioned above

alundiak commented 5 years ago

Not sure how it's related or is it helpful, but I also have similar behavior (an error as @klis87 mentioned).

TypeError: Cannot read property 'hasGlyphForCodePoint' of null
    at react-pdf.browser.es.js:3777
    at Array.reduce (<anonymous>)
    at buildSubsetForFont (react-pdf.browser.es.js:3776)
    at react-pdf.browser.es.js:3787
    at Array.map (<anonymous>)
    at ignoreChars (react-pdf.browser.es.js:3786)
    at getFragments (react-pdf.browser.es.js:3900)
    at getAttributedString (react-pdf.browser.es.js:3906)
    at Text.layoutText (react-pdf.browser.es.js:3985)
    at Text.measureText (react-pdf.browser.es.js:4012)

I have 3 instances of Components using main REPL code for Quixote https://react-pdf.org/repl

My code is simple aggregation and split.

When only Download, or only Viewer or only Blob rendered, all is OK. not errors. When Viewer renedered together with EITEHR Download or Blob, then I have that TypeError in console.

Most probably, @mmichelli is right, that we can't use Viewer and other instances together.

I also tried both approaches of registering font: old:

Font.register(
    'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf',
    { family: 'Oswald' },
);

and new:

Font.register({
    family: 'Oswald',
    src: 'https://fonts.gstatic.com/s/oswald/v13/Y_TKV6o8WovbUd3m_X9aAA.ttf'
});

And sure thing, when I comment code related to Font.register I have no TypeError when I render all 3 components (Viewer, Download, Blob).

Note: I created separated issue for the outdated REPL code: #586

@diegomura could you plz confirm, that it was designed so, and if so, plz reflect it in README or doc page and close this issue? Is it an issue at all, or just wrong usage of @react-pdf/renderer API?

PS. I use MacOS v10.14.4, Node v11.14.0, React v16.8.6, Webpack v4.30, Babel v7.4.3, @react-pdf/renderer v1.5.4.

Plonq commented 5 years ago

I'm getting this error with two separate PDFDownloadLink components, so it doesn't seem to be restricted to PDFViewer. Both by PDFDownloadLink components are rendering different documents with different data. The only shared data is the fonts. EDIT: actually, they also share some styled components using @react-pdf/styled-components, however when not using custom fonts there are no errors.

I'm using v1.5.6

hutch120 commented 5 years ago

So I got this working with a setTimeout hack. (After trying a bunch of other things like moving the Font registration code around)

Firstly, the layout I have is a Sidebar component with some print options like page size, orientation, etc; a Topbar component with some action buttons including the download link; and lastly the main PDFViewer component.

Hack was to delay load the Topbar component containing the download link, after loading the PDFViewer. No doubt some sort of callback from React render of the PDFViewer component would be much more robust.

Code extract here cut down for clarity.

import React from 'react'
...

const styles = theme => ({ root: {} })

class Topbar extends React.Component {
  state = { load: false }

  componentDidMount () {
    window.setTimeout(this.setStartLoading.bind(this), 2000)
  }

  setStartLoading () { this.setState({ load: true }) }

  render () {
    const { load } = this.state
    if (!load) { return null }
    return (<DownloadLink doc={<Provider store={window.store}><MyPDFDocument/></Provider> } /> 
  }
}

Topbar.propTypes = { ... }
function mapStateToProps (state, ownProps) { return { ... } }
const mapDispatchToProps = dispatch => { return { ... } }

export default withRoot(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Topbar)))
Plonq commented 5 years ago

@hutch120 I managed a slightly better hack by effectively re-implementing PDFDownloadLink and nesting BlobProvider. This way the second document only renders once the first is finished. You could theoretically nest forever, and I'm sure a recursive solution is possible as well, which might take an array of documents to render.

_renderDownloadLinks(
    packingSlipsDocument: Object,
    packingSlipsFilename: string,
    purchaseOrderReportDocument: ?Object,
    purchaseOrderReportFilename: ?string,
) {
    const downloadOnIE = (blob, fileName) => () => {
        if (window.navigator.msSaveBlob) {
            window.navigator.msSaveBlob(blob, fileName);
        }
    };

    return (
        <BlobProvider document={packingSlipsDocument}>
            {({ blob, url, loading, error }) => {
                if (error) {
                    return `Error creating PDF: ${error}`;
                }

                return loading ? (
                    'Generating packing slips...'
                ) : (
                    <React.Fragment>
                        <a
                            className="d-block"
                            download={packingSlipsFilename}
                            href={url}
                            onClick={downloadOnIE(blob)}
                        >
                            Download packing slips
                        </a>
                        {purchaseOrderReportDocument && (
                            <BlobProvider document={purchaseOrderReportDocument}>
                                {({ blob: blob2, url: url2, loading: loading2, error: error2 }) => {
                                    if (error2) {
                                        return `Error creating PDF: ${error2}`;
                                    }

                                    return loading2 ? (
                                        'Generating purchase order report...'
                                    ) : (
                                        <a
                                            className="d-block"
                                            download={purchaseOrderReportFilename}
                                            href={url2}
                                            onClick={downloadOnIE(blob2)}
                                        >
                                            Download purchase order report
                                        </a>
                                    );
                                }}
                            </BlobProvider>
                        )}
                    </React.Fragment>
                );
            }}
        </BlobProvider>
    );
}
hutch120 commented 5 years ago

Thanks @Plonq ... your suggestion inspired me to do some more on this, I ended up taking a bit of a different approach which should be much more robust.

Instead of doing multiple renders I started by creating a Redux Action to dispatch the blob returned from PDFDownloadLink and then injected that into an iframe component for the preview. Not only is this more robust, but also no doubt much faster.

There was one kind of weird workaround I needed to do which is add key={Math.random()} to the PDFDownloadLink component to get it to re-render correctly when settings changed (page size, etc), see below.

For reference, the code below is a cut down version of what I ended up with.

import React from 'react'
import { setPrintBlobURL } from './actions'
...

const styles = theme => ({ ... })

class DownloadLink extends React.Component {
  loading () {
    const { classes } = this.props
    return (
      <IconButton className={classes.buttonDownload} aria-label="Downloading">
        <IconDownloading />
      </IconButton>
    )
  }

  loaded (blob, url, loading, error) {
    const { classes, fontSize, setPrintBlobURL } = this.props
    setPrintBlobURL(url) // Dispatch the Blob URL for the preview component.
    return (
      <IconButton className={classes.buttonDownload} aria-label="Download">
        <IconDownload />
      </IconButton>
    )
  }

  render () {
    const { classes, doc } = this.props

    // Note: key={Math.random()} forces the pdf document to refresh. Couldn't figure out a better way to rerender correctly on settings change.
    return (
      <div className={classes.downloadLink}>
        <PDFDownloadLink key={Math.random()} document={doc} fileName={this.getFilename()}>
          {({ blob, url, loading, error }) => (loading ? this.loading() : this.loaded(blob, url, loading, error))}
        </PDFDownloadLink>
      </div>
    )
  }
}

DownloadLink.propTypes = { ... }
function mapStateToProps (state, ownProps) { return { ... } }

const mapDispatchToProps = dispatch => {
  return {
    setPrintBlobURL: printBlobURL => {
      dispatch(setPrintBlobURL(printBlobURL))
    }
  }
}

export default withRoot(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(DownloadLink)))

And my Preview component looks like this:

import React from 'react'
...

const styles = theme => ({ ... })

class Preview extends React.Component {
  render () {
    const { classes, printBlobURL } = this.props
    if (!printBlobURL || printBlobURL === '') {
      return null
    }
    return (
      <iframe
        src={printBlobURL}
        className={classes.pdf}
        title={'reactpdfprintpreview'}
        id={'reactpdfprintpreview'}
        type="application/pdf"
        width="100%"
        height="100%"
        frameBorder="0"
        /* allowFullScreen="allowfullscreen"
        mozallowfullscreen="mozallowfullscreen"
        msallowfullscreen="msallowfullscreen"
        oallowfullscreen="oallowfullscreen"
        webkitallowfullscreen="webkitallowfullscreen" */
      />
    )
  }
}

Preview.propTypes = {
  classes: PropTypes.object.isRequired,
  printBlobURL: PropTypes.string.isRequired
}

function mapStateToProps (state, ownProps) {
  return { printBlobURL: state.printBlobURL }
}

const mapDispatchToProps = dispatch => { return {} }

export default withRoot(withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(Preview)))
hutch120 commented 5 years ago

So, I spent a lot of time today getting this module working with Redux and I wanted to make a few notes of my journey.. not sure this is the best place... but maybe this could be some update to the doco? Also... this started out as a solution for the multiple distinct PDFs this thread discusses, but turned into something else, so sorry for the dump, but hope it helps someone.

Pretty much boils down to this innocent looking code:

<PDFDownloadLink 
  key={Math.random()} 
  document={   
    <Provider store={window.store}>
      <DocDefault />
    </Provider>} 
  ...

Notice DocDefault component is passed without any props because I assumed I could reference Redux state in the DocDefault component ... but the problem is, when the referenced Redux state changes it tries to re-render the DocDefault component, which fails horribly (often causes a browser debugger crash), which I now clearly see is because it is inside the PDFDownloadLink component!

Once I finally figured out the issue was the re-render triggered by Redux, I was able to fix it.

So, essentially when integrating with a Redux project I would do one of these three things: 1) include the whole PDFDownloadLink component in the Document definition, or 2) pass the Redux variables as props, or maybe 3) reference the Redux store directly with getState(), anyway, I ended up doing 2) and removing all Redux mapStateToProps variables from DocDefault:

<PDFDownloadLink
  key={Math.random()}
  document={
    <Provider store={window.store}>
      <DocDefault printSize={printSize} printOrientation={printOrientation} data={data} />
    </Provider>  }
   ...
shakepompom commented 5 years ago

I have the same problem. I have ParentComponent, that map ChildrenComponents, in which I have \, that generates PDF for each child. The solution I found is next:

import React, { useEffect, useState } from 'react';
import { pdf } from '@react-pdf/renderer';
import PdfDocument from './PdfDocument'; // our PDF Document which would be downloaded

function Parent() {
  // data, which we get eg from server
  const childrenArray = [
    {
      id: 1,
      name: 'name-1',
      description: 'description-1',
    },
    {
      id: 2,
      name: 'name-2',
      description: 'description-2',
    },
    {
      id: 3,
      name: 'name-3',
      description: 'description-3',
    },
  ];

  const [children, setChildrenWithBlob] = useState(childrenArray);

  // here is all magic
  // !important: generate docs one by one
  async function createPDF() {
    const childrenWithBlob = [];
    let i = 0;

    while (i < childrenArray.length) {
      // eslint-disable-next-line no-await-in-loop
      await pdf(<PdfDocument data={childrenArray[i]} />)
        .toBlob()
        // eslint-disable-next-line no-loop-func
        .then(blobProp => {
          childrenWithBlob.push({
            ...childrenArray[i],
            url: URL.createObjectURL(blobProp),
            blob: blobProp,
          });
        });

      i += 1;
    }

    setChildrenWithBlob(childrenWithBlob);
  }

  // we generate pdf after componentDidMount
  useEffect(() => {
    createPDF();
  }, []);

  // get this code from @react-pdf/renderer
  const downloadOnIE = blobProp => () => {
    if (window.navigator.msSaveBlob) {
      window.navigator.msSaveBlob(blobProp, 'fileName.pdf');
    }
  };

  // need this instead of using <PDFDownloadLink>
  // 'cause have 2 triggers in each child component to download the same PDFDoc.
  // <PDFDownloadLink> didn't work for me
  const createTriggerToDownload = (child, trigger) => {
    const { blob, url } = child;

    return (
      <a href={url} download="fileName.pdf" onClick={() => downloadOnIE(blob)}>
        {trigger}
      </a>
    );
  };

  return (
    <>
      {children.map(child => (
        <div key={child.id}>
          {createTriggerToDownload(
            child,
            <button type="button">Download PDF</button>
          )}
          <div>{child.name}</div>
          <div>{child.description}</div>
          {createTriggerToDownload(
            child,
            <button type="button">Download</button>
          )}
        </div>
      ))}
    </>
  );
}

Hope this will help :)

UPD: @diegomura thank u very much for this project ❤️

jp06 commented 5 years ago

I had this error for a while. In my case, I have a <BlobProvider/> returning two anchor tags: one opening in new tab and another for download. I was racking my head because I almost just reused the code I have that is not spewing out errors. I tried reproducing in CodeSandbox but it is successfully rendered without errors.

I think it is that you have to avoid rerendering when the PDF is being generated. I solved mine by putting a boolean loadFinished state to finalize rendering and set it as condition to render the <BlobProvider/>, similar to what others above came up with too.

tyankatsu0105 commented 5 years ago

I have a question.
I have the same issue when using a custom font(GenShinGothic).
I think this answer is probably correct.
But it seems like this page can use PDFDownloadLink and PDFViewer in the same page.
Why is it possible?

Xcellion commented 4 years ago

Anyone stumbling on this who is using PDFDownloadLink, I ended up using this solution to generate the PDFs on user click and it ended up working!

corneliugaina commented 4 years ago

Try to execute your function / loop with async-await promise.

Keep in mind that the bug will still present if you loop with forEach (parallel execution). Use a simple for ... of to read/execute the files in sequence, and so avoiding any congestion while generating pdf files.

klis87 commented 4 years ago

The key here is await first createPDF promise to be resolved, then u can execute next calls in parallel in any number desired, for instance like:

const createPDF = pdfContent => {
  const instance = pdf();
  instance.updateContainer(<MyDocument>{pdfContent}</MyDocument>);
  return instance.toBlob();
};

const = [firstContent, ...remainingContents] = pdfContentList;
const firstBlob = await createPDF(firstContent);
const remainingBlobs = await Promise.all(
  remainingContents.map(pdfContent =>
    createPDF(pdfContent),
  ),
);
const allBlobs = [firstBlob, ...remainingBlobs]

This is workaround of course, but for me it mitigated this issue.

bocha13 commented 4 years ago

I've been having this error when creating multiple pdf at once. And like @corneliushka said, my problem was that I was looping through the array using .forEach. Changed it to for .. of and that solved my issue.

ampsteric commented 4 years ago

how to use custom google font in this? That is why I am getting the error.

andrewgrinko commented 4 years ago

Another solution that works on Node: use base64 data uri as source in Font.register instead of url/path.

ghost commented 4 years ago

This is not a solution. It's a workaround i've used that in my hook. Since it did not fit my architecture to render the pdf's one by one i made a solution that will just retry if document creation fails. Works surprisingly well actually. Hope it can help someone out:

import React, { useEffect, useState } from 'react';
import { pdf } from '@react-pdf/renderer';
import TemplateLoader from 'templates/TemplateLoader';

interface OnPDF {
  loading: boolean;
  documentURL?: string;
}

function usePDF(resume: CV.Base): OnPDF {
  const [loading, setLoading] = useState(true);
  const [documentURL, setDocumentURL] = useState<string | undefined>();

  useEffect(() => {
    const generateBlob = async () => {
      let blob;
      let retryAttempts = 0;

      setLoading(true);

      do {
        // To circumvent race condition in @react-pdf when rendering
        // multiple PDF's with custom fonts simultaneously.
        try {
          blob = await pdf(<TemplateLoader resume={resume} />).toBlob();
        } catch (error) {
          // Render failed, let's try again but first some rest.
          await new Promise((resolve) => setTimeout(resolve, 500));
          retryAttempts++;
        }
      } while (!blob || retryAttempts > 3);

      setDocumentURL(window.URL.createObjectURL(blob));
      setLoading(false);
    };

    if (resume) {
      generateBlob();
    } else {
      setLoading(false);
      setDocumentURL(undefined);
    }
  }, [resume]);

  return { loading, documentURL };
}

export default usePDF;
pixeloft commented 3 years ago

@erikandersson has the only solution that worked for me. I tried literally EVERY single one of these, but this one is the only one that works!

baxxos commented 3 years ago

same as @pixeloft - the only bulletproof solution was to introduce a retry policy.

I've tried all other solutions mentioned here (i.e. awaiting the first document blob and only then generating the rest and also the Promise.all with await in for .. of loop), but they only mitigated the issue at best.

Nases commented 3 years ago

don't do: { condition && <SomePDFComponent /> } do: { condition ? <SomePDFComponent /> : <></> }

Nases commented 3 years ago

This is not a solution. It's a workaround i've used that in my hook. Since it did not fit my architecture to render the pdf's one by one i made a solution that will just retry if document creation fails. Works surprisingly well actually. Hope it can help someone out:

import React, { useEffect, useState } from 'react';
import { pdf } from '@react-pdf/renderer';
import TemplateLoader from 'templates/TemplateLoader';

interface OnPDF {
  loading: boolean;
  documentURL?: string;
}

function usePDF(resume: CV.Base): OnPDF {
  const [loading, setLoading] = useState(true);
  const [documentURL, setDocumentURL] = useState<string | undefined>();

  useEffect(() => {
    const generateBlob = async () => {
      let blob;
      let retryAttempts = 0;

      setLoading(true);

      do {
        // To circumvent race condition in @react-pdf when rendering
        // multiple PDF's with custom fonts simultaneously.
        try {
          blob = await pdf(<TemplateLoader resume={resume} />).toBlob();
        } catch (error) {
          // Render failed, let's try again but first some rest.
          await new Promise((resolve) => setTimeout(resolve, 500));
          retryAttempts++;
        }
      } while (!blob || retryAttempts > 3);

      setDocumentURL(window.URL.createObjectURL(blob));
      setLoading(false);
    };

    if (resume) {
      generateBlob();
    } else {
      setLoading(false);
      setDocumentURL(undefined);
    }
  }, [resume]);

  return { loading, documentURL };
}

export default usePDF;

This works great! I think you meant,

while (!blob && retryAttempts < 3)

instead of

while (!blob || retryAttempts > 3)

Because the loop will continue forever if blob fails every time. also,

retryAttempts < 3
joshbalfour commented 3 years ago

This isn't ideal but to workaround this I'm using https://www.npmjs.com/package/easy-promise-queue:

import React from 'react'
import ReactPDF from '@react-pdf/renderer'
import PromiseQueue from 'easy-promise-queue'

import DocumentToRender, { DocumentProps } from './Document'

// can only handle one at a time https://github.com/diegomura/react-pdf/issues/310 
const renderQueue = new PromiseQueue({ concurrency: 1 })

export const getDocument = async (data: DocumentProps) => {
    const promise = new Promise((resolve, reject) => {
        renderQueue.add(() => ReactPDF.renderToStream(
            <Document
                {...(data as DocumentProps)}
            />
        ).then(resolve).catch(reject))
    })

    return promise
}