An easy/quick guide on how to generate custom PDF for weweb for free (100% client side)

So I needed to create a button that generates a custom PDF for my weweb app from components already displayed on the screen. I looked into a few existing threads on how to do so, and most seemed like they had to utilize a backend(Xano) and/or third party plugins (paid or free) to achieve this. That was too complicated for my case so I found a way to do it all on the frontend side. Its not the best way but gets the job done, so I felt like I should share it since many people were interested in such functionality.

My approach:
I used html2pdf.js and html2canvas to capture components as images and add to the pdf.

I chatgpted my way through this code so I don’t expect it to be best practice or “proper” way to get this done.

Step 1: Create a custom weweb component.

Step 2: Add desired fonts and styling in the template section of the vue component to style the button appearance. (this was all I needed), colors and such are added later in css.

<template>
  <link href="https://fonts.googleapis.com/css2?family=Suez+One&display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400&display=swap" rel="stylesheet">
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@500&display=swap" rel="stylesheet">

  <button @click="generatePDF" class="pdf-button">
    <img src="./pdff.png"
         alt="PDF Icon" style="width: 18px; height: 18px; margin-right: 5px; margin-bottom: 2px;">
    PDF
  </button>
</template>

For the script part:

<script>
import html2pdf from "html2pdf.js";
import html2canvas from "html2canvas";

export default {
  methods: {
    generatePDF() {

    const element1 = document.querySelector('[uid="10sft964b-262eer9-414ertertgbfa9"]');
    const element2 = document.querySelector('[uid="4e708eregergb3e1d59erggerg0ae"]');

    if (!element1 || !element2 ) {
      console.error("Target element not found");
      return;


//Here is where this gets tricky, you would need to grab the "**Section UID**" 
and not the elements(divs and such). The querySelector() function was able to find/recognize
 sections only and not individual components or containers. So my approach was if I had 4 
different parts in a page where I want to add to the PDF, I would create a new section for each
 part, so I have the freedom to move around these components in the PDF once captured. 


//Another important part, make sure the section created for each component has a specified max
 or fixed width, usually one that matches the component ur tryna capture. If you set it to auto or 
100%, the components captured dimensions would differ based on the user's screen size.


// Function to capture an element and return a promise with the image
    const captureElementAsImage = (el) => {
            return html2canvas(el, { scale: 2 }).then(canvas => {
                const imgData = canvas.toDataURL("image/png");
                const imgElement = document.createElement('img');
                imgElement.src = imgData;
                imgElement.style.width = '100%'; // Scale to fit the PDF
                imgElement.style.height = 'auto'; // Maintain aspect ratio
                return imgElement;
            });
        };

// Create a temporary container for the PDF content
    const container = document.createElement('div');

// Capture each element and append to container
    captureElementAsImage(element1).then(imgElement => { 
        container.appendChild(imgElement);
 
// Here below I added empty divs to space elements from each other because Chatgpt couldnt 
figure how to specifically assign each element to a separate page and provide a 1 inch margin around the page borders so I did this approach to manually achieve that.
 If there is an easy way to apply 1 inch margin to the document and have element automatically get added to next page if it falls on that margin lmk in the comments! 

//Create a new paragraph div with "space" and a 200px bottom margin
        const spaceParagraph1 = document.createElement('div');
        spaceParagraph1.innerText = 'space';
        spaceParagraph1.style.marginBottom = '200px'; // Add bottom margin
        spaceParagraph1.style.color = 'white';
        container.appendChild(spaceParagraph1);

// Capture the second element and append to container
        captureElementAsImage(element2).then(imgElement => {
          container.appendChild(imgElement);

// PDF options
            const options = {
              filename: "Demographics_data.pdf",
              image: { type: "jpeg", quality: 0.98 },
              html2canvas: { scale: 2 },
              jsPDF: { unit: "in", format: "letter", orientation: "portrait", margins: { top: 1, bottom: 1, left: 1, right: 1 } },
            };
// this will add page numbering at the bottom before saving/downloading the pdf    
html2pdf()
          .from(container)
          .set(options)
          .toPdf()
          .get('pdf')
          .then((pdf) => {
            const totalPages = pdf.internal.getNumberOfPages();
            for (let i = 1; i <= totalPages; i++) {
              pdf.setPage(i);
              pdf.setFontSize(10);
              pdf.setTextColor(100);
              pdf.text(`Page ${i} of ${totalPages}`, (pdf.internal.pageSize.getWidth() / 2), (pdf.internal.pageSize.getHeight() - 0.75), { align: "center" });
            }
          })
          .save()
          .then(() => {
              console.log('PDF generated'); // Log when PDF is generated
            }).catch(error => {
              console.error('Error generating PDF:', error);
            });
          });
        });
      });
    }
  },
};
</script>


//some styling for the button

<style scoped>
.pdf-button {
  padding: 10px 12px;
  font-family: 'Inter', sans-serif;
  font-weight: 700;
  font-size: 14px;
  background-color: #d91c1f;
  color: rgb(255, 255, 255);
  border: none;
  border-radius: 6px;
  cursor: pointer;
  display: flex; /* Use flex to align the icon and text */
  align-items: center;
}

.pdf-button:hover {
  background-color: #b51818;
}
.pdf-button span {
  padding-bottom: 10px; /* Adjust this value to lift the text up */
}
</style>

After clicking this button, the pdf will generate and download right away, 100% client side rendering. Specifying the section width will have the PDF look consistent no matter the screen size of the end user.

Hope this helps someone, lmk in comments if theres any questions. Heres an example of an output using this approach: ( I coded extra stuff for title and date generated at the top)

4 Likes

Olá, tudo bem.
Gostei muito da sua solução.
Estou tendo problemas em resolver em uma ferramenta que estou desenvolvendo.

Você teria mais detalhes, para que eu possa implementar a mesma solução.

Obrigado.

What is the issue you’re facing?