Generate and download a custom PDF with pdf-lib

Since there does not seem to be an article about how to create a custom PDF from data inside a database collection without using the PDF Generator API, I thought I would post my solution. This solution is free and fairly easy to copy and customize.

I’m using the npm package pdf-lib to create the PDF-Document as well as the WIX Velo MediaManager API to upload and then download the PDF.

In my example, I’m creating the PDF on a button click. After that, the PDF will be stored as a base64 string, which is passed to the savePDF() function:

import { savePDF } from "backend/handlePDF";
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
import wixLocation from 'wix-location';

export async function button1_click(event) {
    // Set filename
    const fileName = "myExamplePDF";

    // Create a new PDFDocument
    const pdfDoc = await PDFDocument.create();

    // Embed the Times Roman font
    const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman);

    // Add a blank page to the document
    const page = pdfDoc.addPage();

    // Get the width and height of the page
    const { width, height } = page.getSize();

    // Draw a string of text toward the top of the page
    const fontSize = 30;
    page.drawText('Creating PDFs in JavaScript is awesome!', {
        x: 50,
        y: height - 4 * fontSize,
        size: fontSize,
        font: timesRomanFont,
        color: rgb(0, 0.53, 0.71),
    });

    // HERE YOU CAN CUSTOMIZE THE PDF AS YOU WISH (see pdf-lib documentation)

    // Serialize the PDFDocument to Base64
    const pdfDataUri = await pdfDoc.saveAsBase64({ dataUri: true });
    const dataUriPrefix = 'data:application/pdf;base64,';
    const base64 = pdfDataUri.slice(dataUriPrefix.length);

    savePDF(base64, fileName)
        .then(downloadUrl => {
            console.log(downloadUrl);
            wixLocation.to(downloadUrl); //only works when published
        })
        .catch((error) => {
            console.error("Error while saving the PDF: ", error);
        });
}

The savePDF() function receives the base64 string as well as the fileName and calls the uploadPDF() function, which transforms the base64 string into a buffer. The buffer can then be used by the upload() function to upload the file to the Media Manager.

Once the upload is finished, the uploadPDF() function returns the file information that contain the fileUrl. This URL is passed to the getDownloadUrl() function, which returns a URL that can be used with the to() function. This opens the download bar in your browser.

import { mediaManager } from 'wix-media-backend';

export function savePDF(base64, fileName) {
    return new Promise((resolve, reject) => {
        
        uploadPDF(base64, fileName)
            .then((file) => {

                getDownloadUrl(file.fileUrl)
                    .then((downloadUrl) => {
                        resolve(downloadUrl);
                    })
                    .catch((error) => {
                        reject(error);
                    });
            })
            .catch((error) => {
                reject(error);
            });
    });
}

export function uploadPDF(base64, fileName) {
    // create Buffer from Base64
    const buffer = Buffer.from(base64, 'base64');

    return mediaManager.upload(
        "/UserUploads",
        buffer,
        fileName + ".pdf", {
            "mediaOptions": {
                "mimeType": "application/pdf",
                "mediaType": "document"
            },
            "metadataOptions": {
                "isPrivate": false,
                "isVisitorUpload": false,
                "context": {
                    "someKey1": "someValue1",
                    "someKey2": "someValue2"
                }
            }
        }
    );
}

export async function getDownloadUrl(fileUrl) {
    const myFileDownloadUrl = await mediaManager.getDownloadUrl(fileUrl, 10);
    return myFileDownloadUrl;
}

Of course, you can customize the functions according to your needs. For example, you could add the moveFilesToTrash() function after the to() function to delete the file from the MediaManager after the download. Additionally, as mentioned at the beginning, you can use the wix-data API to add data from a collection to your PDF.

I hope you found this explanation helpful and can apply it to your own website.

9 Likes

This looks very interesting thanks ill try it out

HP96, this is awesome and solves a huge problem of mine. However, could you possibly provide an example of how the moveFilesToTrash() function would work after the download, I can’t seem to make that happen.

Hi Brad,

Sorry for the delay. Here is the example:

First, I changed the savePDF() function from the original post. It now calls the trashFiles() function, which moves the file to the trash after it is downloaded. Note that the fileUrl is inside an array, unlike when calling the getDownloadUrl() function.

import { mediaManager } from ‘wix-media-backend’;

export function savePDF(base64, fileName) {
return new Promise((resolve, reject) => {

    uploadPDF(base64, fileName) 
        .then((file) => { 

            getDownloadUrl(file.fileUrl) 
                .then((downloadUrl) => { 
                    resolve(downloadUrl); 
                    const fileArray = [file.fileUrl]; 
                    setTimeout(() => { 
                          trashFiles(fileArray); 
                    }, 1000); 
                }) 
                .catch((error) => { 
                    reject(error); 
                }); 
        }) 
        .catch((error) => { 
            reject(error); 
        }); 
}); 

}

This is the trashFiles() function, which contains the .moveFilesToTrash command provided by the Wix API. You can add it after the getDownloadUrl() function.

export function trashFiles(fileUrls) {
return mediaManager.moveFilesToTrash(fileUrls)
.then(() => {
console.log(‘Success! Files have been trashed.’);
})
.catch((error) => {
console.error(error);
})
}

I hope that helps you :slight_smile:

Thank you very much for posting this solution. I have used pdf-lib in Node and did not know how to use it in wix.

1 Like

Thank you guys this solution is helping me but im having the error 413.
Let me explain better, i created an website that the owner wants to allow the clients to get access of the reports of their provided service and in the end it must allow to download this report selected when cliecked on a button, i managed to do all of it except the pdf download of the file and with your help i managed that but since there are many images on my database on wix i can only get a few images and when i try to get more images on the pdf it gives me the error 413 how can i solve this situation. Just to mention im a beginner.

this is my code:

export async function btnDownloadPdf_click(event) {
            
  $w('#btnBaixarPdf').collapse();
  $w('#imgLoading').expand();
       
    const datas = $w('#rptListDownload').data;

   // Set filename
    const fileName = `Report_${datas[0].monthReport}`;

    // Create a new PDFDocument
    const pdfDoc = await PDFDocument.create();

    // Embed the Times Roman font
    const timesRomanFont = await pdfDoc.embedFont(StandardFonts.Helvetica);

    // Add a blank page to the document
    const page = pdfDoc.addPage([595.276, 841.890]);

    // Get the width and height of the page
    const { width, height } = page.getSize();

    const cmToPoints = (cm) => cm * 16.35; // Convert cm to points

    const imageBytes1 = await fetch('https://res.cloudinary.com/drhmgeae5/image/upload/v1692208742/logotipos_am8vme.png').then((response) => response.arrayBuffer());
    const imageLogo1 = await pdfDoc.embedPng(imageBytes1);

    const imageBytes2 = await fetch('https://res.cloudinary.com/drhmgeae5/image/upload/v1692208742/logotipovemprobus_1_acrtte.png').then((response) => response.arrayBuffer());
    const imageLogo2 = await pdfDoc.embedPng(imageBytes2);

    const imageWidth = 60; // Adjust the image width as needed
    const xPosition = (page.getWidth() - 2 * imageWidth) / 2;

    // Calculate the y-coordinate for both images to position them 5 cm from the top
    const yPosition = page.getHeight() - cmToPoints(5);

    // Draw the first image
    page.drawImage(imageLogo1, {
        x: xPosition,
        y: yPosition,
        width: imageWidth,
        height: 60,
    });

    // Draw the second image next to the first image
    page.drawImage(imageLogo2, {
        x: xPosition + imageWidth,
        y: yPosition,
        width: imageWidth,
        height: 40,
    });

    const counter = datas.length; // Count the number of arrays
       
    // Query the Wix database collection to retrieve the user's information
        const userId = datas[0].user; // Replace with the actual user identifier

        const userResult = await wixData.query('Users')
            .eq('_id', userId) // Assuming '_id' is the unique identifier field
            .find();

        if (userResult.items.length > 0) {
            // Get the user's name from the retrieved data
            const userName = userResult.items[0].company; // Replace 'nome' with the actual property name for the user's name

            // Example: Add text with the user's name
            page.drawText(`Client : ${userName}`, {
                x: 30,
                y: 725,
                size: 11,
                font: timesRomanFont,
                color: rgb(0, 0.53, 0.71),
            });
        } else {
            console.error('User not found in the database');
        }

        // Query the Wix database collection to retrieve the user's information
        const TipoId = datas[0].user; // Replace with the actual user identifier

        const results = await wixData.query('Contrato')
            .eq('Users', TypeId) // Assuming '_id' is the unique identifier field
            .find();

        if (results.items.length > 0) {
            // Get the user's name from the retrieved data
            const typePublicity = results.items[0].typePublicity; // Replace 'nome' with the actual property name for the user's name

            // Example: Add text with the user's name
            page.drawText(`Publicity type : ${typePublicity}`, {
                x: 250,
                y: 725,
                size: 11,
                font: timesRomanFont,
                color: rgb(0, 0.53, 0.71),
            });
        } else {
            console.error('User not found in the database');
        }

     // Example: Add text
    page.drawText(`Month : ${datas[0].monthReport}`, {
        x: 30,
        y: 705,
        size: 11,
        font: timesRomanFont,
        color: rgb(0, 0.53, 0.71),
    });

    // Example: Add text
    page.drawText(`Year : ${datas[0].yearReport}`, {
        x: 150,
        y: 705,
        size: 11,
        font: timesRomanFont,
        color: rgb(0, 0.53, 0.71),
    });

    // Example: Add text
    page.drawText(`# of cars : ${counter}`, {
        x: 220,
        y: 705,
        size: 11,
        font: timesRomanFont,
        color: rgb(0, 0.53, 0.71),
    });

    // Example: Add text
    page.drawText(`Time of report : ${formatDate(datas[0].monthOfReport)} - ${formatDate(datas[0].dateOfWork)}`, {
        x: 320,
        y: 705,
        size: 11,
        font: timesRomanFont,
        color: rgb(0, 0.53, 0.71),
    });

          // Initialize the starting y-coordinate
          let y = 675;

        const contentHeight = 20; // Adjust this value based on your content

    // Define a function to add a new page to the PDF
    const addNewPage = () => {
        const pageHeight = 841.890; // Adjust to your page height
    const remainingSpace = y - contentHeight; // Calculate the remaining space on the current page

    if (remainingSpace < 30) {
        currentPage = pdfDoc.addPage([595.276, pageHeight]);
        y = pageHeight - 30; // Reset y-coordinate for the new page
    }

    // Add any page-specific content or headers here
    return currentPage;
    };

    // Iterate through the repeater data and add it to the PDF
    let currentPage = addNewPage(); // Initial page
    let xy = 20; // Initial y-coordinate

    // Iterate through the repeater data and add it to the PDF
    for (let index = 0; index < datas.length; index++) {
        const item = datas[index];

        // Fetch and embed images for the current item
        const fullAttachmentReport = getFullImageURL(item.attachTheReport);
        const fullAttachmentOfBack= getFullImageURL(item.attachTheBack);
        const fullAttachmentOfLeft = getFullImageURL(item.attachTheLeft);
        const fullAttachmentOfRight = getFullImageURL(item.attachTheRight);

        const imageCalendar = await fetch(fullAttachmentReport).then((response) => response.arrayBuffer());
        const image1 = await pdfDoc.embedJpg(imageCalendar);

        const imageBack = await fetch(fullAttachmentOfBack).then((response) => response.arrayBuffer());
        const image2 = await pdfDoc.embedJpg(imageBack);

        const imageLeft = await fetch(fullAttachmentOfLeft).then((response) => response.arrayBuffer());
        const image3 = await pdfDoc.embedJpg(imageLeft);

        const imageRight = await fetch(fullAttachmentOfRight).then((response) => response.arrayBuffer());
        const image4 = await pdfDoc.embedJpg(imageRight);
    
// Count the number of indexed arrays
    const count = index + 1;

    // Add text
    page.drawText(`${count}`, {
          x: 30,
          y,
          size: 10,
          font: timesRomanFont,
          color: rgb(0, 0.53, 0.71),
    });

    // Add text
    page.drawText(`Plate : ${item.plate}`, {
          x: 50,
          y,
          size: 10,
          font: timesRomanFont,
          color: rgb(0, 0.53, 0.71),
    });

    // Add text
    page.drawText(`days driven : ${item.daysDriven}`, {
          x: 190,
          y,
          size: 10,
          font: timesRomanFont,
          color: rgb(0, 0.53, 0.71),
    });

    // Add text
    page.drawText(`Days Stoped : ${item.daysStoped}`, {
          x: 300,
          y,
          size: 10,
          font: timesRomanFont,
          color: rgb(0, 0.53, 0.71),
    });

      // Add text
    page.drawText(`City : ${item.city}`, {
          x: 420,
          y,
          size: 10,
          font: timesRomanFont,
          color: rgb(0, 0.53, 0.71),
    });

      // Increment the y-coordinate for the next set of data
      y -= 15; // Move down by 20 units for the next item

      // Add text
    page.drawText(`Description : ${item.description}`, {
          x: 50,
          y,
          size: 10,
          font: timesRomanFont,
          color: rgb(0, 0.53, 0.71),
    });

      
        // Increment the y-coordinate for the next set of data
        y -= 85; // Move down by 85 units for the next item    

     
    // Calculate the total width of the two images and the space between them
    const totalWidth = 2 * 180; // Assuming each image has a width of 80 units
    const spaceBetweenImages = 150; // Adjust this value as needed

    // Calculate the starting x-coordinate for the first image
    const startX = (page.getWidth() - totalWidth - spaceBetweenImages) / 2;

    // Calculate the total width of the two images and the space between them
    const totalWidth2 = 2 * 180; // Assuming each image has a width of 80 units
    const spaceBetweenImages2 = 150; // Adjust this value as needed

    // Calculate the starting x-coordinate for the first image
    const startX2 = (page.getWidth() - totalWidth2 - spaceBetweenImages2) / 2;

// Add the first embedded image (image1) to the PDF page
page.drawImage(image1, {
    x: startX,
    y, // You should set the 'y' coordinate to position the image
    width: 135, // Set the width of the first image
    height: 75, // Set the height of the first image
});

// Add the second embedded image (image2) to the PDF page, positioned next to the first image
page.drawImage(image2, {
    x: startX + 50 + spaceBetweenImages, // Position it to the right of the first image
    y, // Set the 'y' coordinate to match the position of the first image
    width: 135, // Set the width of the second image
    height: 75, // Set the height of the second image
});


// Add the first embedded image (image1) to the PDF page
page.drawImage(image3, {
    x: startX + 230 + spaceBetweenImages,
    y, // You should set the 'y' coordinate to position the image
    width: 135, // Set the width of the first image
    height: 75, // Set the height of the first image
});

       // Increment the y-coordinate for the next set of data
      y -= 85; // Move down by 20 units for the next item

// Add the second embedded image (image2) to the PDF page, positioned next to the first image
page.drawImage(image4, {
    x: startX2, // Position it to the right of the first image
    y, // Set the 'y' coordinate to match the position of the first image
    width: 135, // Set the width of the second image
    height: 75, // Set the height of the second image
});

    
       // Increment the y-coordinate for the next set of data
      y -= 30; // Move down by 20 units for the next item

      // Increment the y-coordinate for the next set of data
    xy -= contentHeight; // Move down by contentHeight units for the next item

  }
  
    // Serialize the PDFDocument to Base64
    const pdfDataUri = await pdfDoc.saveAsBase64({ dataUri: true });
    const dataUriPrefix = 'data:application/pdf;base64,';
    const base64 = pdfDataUri.slice(dataUriPrefix.length);

    console.log("pdfDataUri:", pdfDataUri);
    console.log("base64:", base64);

    savePDF(base64, fileName)
        .then(downloadUrl => {
            console.log("download Url", downloadUrl);
            wixLocation.to(downloadUrl); //only works when published

            $w('#imgLoading').collapse();
            $w('#btnDownloadPdf').expand();

        })
        .catch((error) => {
            console.error("1 Erro ocorreu ao gravar o PDF: ", error);

            $w('#imgLoading').collapse();
            $w('#btnDownloadPdf').expand();
        });
  
  
}
2 Likes

Hi,

I apologize for the delayed response.

I encountered a similar issue when attempting to download a PDF file larger than 2MB. Perhaps you could consider compressing the images before uploading them to the CMS collection so that they are small enough for the PDF download.

I also attempted to compress the images just before embedding them in the PDF, but I couldn’t find a solution that worked.

I hope this information proves helpful to you.

Best regards.

1 Like

can the wix media manager be changed with an ftp server to do the same thing ? if possible can you give an example of it.

thanks in advance

I don’t know if that is possible. If the FTP server can handle the base64 string or the buffer, then I guess it might be possible.

Thanks a lot @hp1 !! This article helped me get started with generating downloadable pdfs!

Where do I copy all of the code to make this work? I’m assuming some in a backend file since it’s being referenced? Sorry new here.

Please open your own POST and describe in detail what you are searching for, explaining your issue and i will take a look onto it.

Respecto a tu problema, estuve haciendo algunas pruebas, hay puntos a considerar en el tamaño de imagenes, para un pdf es de 72 pixeles por pulgada, por lo que si insertas una imagen asegurate de que cumpla esta resolucion para que se acomode a la hoja, el error 413 sucede cuando la imagen a insertar es demasiado grande, el tamaño carta tiene aproximadamente 616 por 791 puntos de pixel, tu imagen deberia estar en ese rango de pixeles, sin embargo en mis pruebas la biblioteca npm pdf lib soporta hasta imagenes de 270 pixeles por pulgada, si quieres un rango mas preciso me soporto una imagen de 2966 x 2307 pixeles por encima de eso la biblioteca presenta un error 413, el error 413 puede presentarse por 2 razones, la longitud de los pixeles es demasiado extensa, o el tamaño de archivo es demasiado grande, como recomendacion para evitar el error del peso puedes probar a usar otra extension de archivo como *.jpg ya que es mucho mas liviana que *.png