0

I create a PDF in ReactJS using react-pdf/renderer and download it using file-saver.

Here is my code that creates the PDF and downloads it:

const LazyDownloadPDFButton = (number, date, totalHours, formattedHours) => (
        <Button
            className={classes.download}
            onClick={
                async () => {
                    const doc = <InvoicePDF number={number} date={date} totalHours={totalHours} formattedHours={formattedHours} />
                    const asPdf = pdf()
                    asPdf.updateContainer(doc)
                    const blob = await asPdf.toBlob()
                    saveAs(blob, `PDF${number}.pdf`)
                }}>
            Download
        </Button>
    ) 

where InvoicePDF is a separate component that renders the PDF pages with the necessary arguments, as in react-pdf/renderer documentation page.

Before download the actual PDF I have to merge it with another existing PDF that will be choose from computer drive. To do that I have the next code snippet:

fileRef = useRef()

<Button onClick={() => fileRef.current.click()}>
    Upload file

    <input
        ref={fileRef}
        type='file'
        style={{ display: 'none' }}
    />
</Button>

Which returns me the details of the file.

I tried to updateContainer with this selected file, but there are errors.

How this new file should be merged with the InvoicePDF that is created?


In the meantime, I tried to create my last blob from arrayBuffers like this:

This is the function that concatenates the created PDF with the selected PDF and it returns the correct sum.

function concatArrayBuffers(buffer1, buffer2) {
        if (!buffer1) {
          return buffer2;
        } else if (!buffer2) {
          return buffer1;
        }
      
        var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
        tmp.set(new Uint8Array(buffer1), 0);
        tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
        return tmp.buffer;
      };

And my method now have a finalBlob that is created with arrayBuffers but the problem is that the resulted PDF will always contain just the content of the second arrayBuffer (which is either the selected pdf or the created pdf)

const LazyDownloadPDFButton = (number, date, totalHours, formattedHours) => (
        <Button
            className={classes.download}
            onClick={
                async () => {
                    const doc = <InvoicePDF number={number} date={date} totalHours={totalHours} formattedHours={formattedHours} />
                    const asPdf = pdf()

                    asPdf.updateContainer(doc)

                    const initialBlob = await new Blob([fileRef.current.files[0]], { type: 'application/pdf' }).arrayBuffer()
                    const blob = await (await asPdf.toBlob()).arrayBuffer()
        
                    const finalArrayBuffer = concatArrayBuffers(initialBlob, blob)

                    const finalBlob = new Blob([finalArrayBuffer], { type: 'application/pdf' })

                    saveAs(finalBlob, `PDF${number}.pdf`)
                }}
        >
            Download
        </Button>
    )

2 Answers 2

1

Just A Simple Solution Made By me...

https://github.com/ManasMadan/pdf-actions https://www.npmjs.com/package/pdf-actions

    import { createPDF,pdfArrayToBlob, mergePDF } from "pdf-actions";
    
    // Async Function To Merge PDF Files Uploaded Using The Input Tag in HTML
    const mergePDFHandler = async (files) => {
      // Converting File Object Array To PDF Document Array
      files.forEach((file)=>await createPDF.PDFDocumentFromFile(file))
      // Merging The PDF Files to A PDFDocument
      const mergedPDFDocument = await mergePDF(files)
      // Converting The Merged Document to Unit8Array
      const mergedPdfFile = await mergedPDFDocument.save();
      // Saving The File To Disk
      const pdfBlob = pdfArrayToBlob(mergedPdfFile);
    };
Sign up to request clarification or add additional context in comments.

2 Comments

could you use this to fetch various PDF documents from server and merge them into a single one and store in client?
Yes but some obvious pre and post processing would be required
0

Solution

After some research and a lot of failed tries I came to an answer on how to merge the PDFs in the correct order, and bonus add an image (in my case a signature) on every page of the final PDF.

This is the final code:

function base64toBlob(base64Data, contentType) {
    contentType = contentType || '';
    var sliceSize = 1024;
    var byteCharacters = atob(base64Data);
    var bytesLength = byteCharacters.length;
    var slicesCount = Math.ceil(bytesLength / sliceSize);
    var byteArrays = new Array(slicesCount);

    for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
        var begin = sliceIndex * sliceSize;
        var end = Math.min(begin + sliceSize, bytesLength);

        var bytes = new Array(end - begin);
        for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
            bytes[i] = byteCharacters[offset].charCodeAt(0);
        }
        byteArrays[sliceIndex] = new Uint8Array(bytes);
    }

    return new Blob(byteArrays, { type: contentType });
}


async function mergeBetweenPDF(pdfFileList, number) {
    const doc = await PDFDocument.create()

    const getUserSignature = () => {
        switch (selectedUser.id) {
            case 1:
                return FirstImage

            case 2:
                return SecondImage

            default:
                return null
        }

    }

    const pngURL = getUserSignature()
    const pngImageBytes = pngURL ? await fetch(pngURL).then((res) => res.arrayBuffer()) : null

    const pngImage = pngURL ? await doc.embedPng(pngImageBytes) : null
    const pngDims = pngURL ? pngImage.scale(0.5) : null

    const initialPDF = await PDFDocument.load(pdfFileList[0])
    const appendixPDF = await PDFDocument.load(pdfFileList[1])

    const initialPDFPages = await doc.copyPages(initialPDF, initialPDF.getPageIndices())

    for (const page of initialPDFPages) {
        if (pngURL) {
            page.drawImage(pngImage, {
                x: page.getWidth() / 2 - pngDims.width / 2 + 75,
                y: page.getHeight() / 2 - pngDims.height,
                width: pngDims.width,
                height: pngDims.height,

            });
        }
        doc.addPage(page)
    }

    const appendixPDFPages = await doc.copyPages(appendixPDF, appendixPDF.getPageIndices())
    for (const page of appendixPDFPages) {
        if (pngURL) {
            page.drawImage(pngImage, {
                x: page.getWidth() / 2 - pngDims.width / 2 + 75,
                y: page.getHeight() / 2 - pngDims.height,
                width: pngDims.width,
                height: pngDims.height,

            });
        }

        doc.addPage(page)
    }

    const base64 = await doc.saveAsBase64()

    const bufferArray = base64toBlob(base64, 'application/pdf')

    const blob = new Blob([bufferArray], { type: 'application/pdf' })

    saveAs(blob, `Appendix${number}.pdf`)
}

const LazyDownloadPDFButton = (number, date, totalHours, formattedHours) => (
    <Button
        className={classes.download}
        onClick={
            async () => {
                const doc = <InvoicePDF number={number} date={date} totalHours={totalHours} formattedHours={formattedHours} />
                const asPdf = pdf()

                asPdf.updateContainer(doc)

                let initialBlob = await new Blob([fileRef.current.files[0]], { type: 'application/pdf' }).arrayBuffer()
                let appendixBlob = await (await asPdf.toBlob()).arrayBuffer()

                mergeBetweenPDF([initialBlob, appendixBlob], number)
            }}
    >
        Download
    </Button>
)

So the LazyDownloadPDFButton is my button that request the respective parameters to create the final PDF. InvoicePDF is my created PDF with the parameters, and initialBlob is the PDF that I upload on my page, which requires to be the first in merged PDF, and appendixBlob is the created PDF that will be attached to initialBlob.

In mergeBetweenPDF I am using pdf-lib library to create the final document, where I create the image, take the 2 initial PDFs that are send, looping them, add the image on every page, and then add every page to the final doc which will be downloaded.

Hope one day this will help someone.

2 Comments

Does this convert both of the pdfs to images in the final pdf? Also, do you have any idea how this would work if you already had the needed pdf's in your local file system rather than having to upload them?
Hello @MarkoMarchisio, in the end will create one PDF file with the pages needed, not an image or add 2 images as page for PDF. Also, the logic is regarding 2 PDFs, I guess it doesn't matter if both PDF are selected, created previously, selected or given a path to them :-?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.