Draw DOM elements as PDF files using JSPDF

Recently, I received a demand to draw the template of the page into A PDF file and send it to the background. I began to learn from online articles that html2Canvas+ JSPDF has been basically implemented. But the generated PDF is too vague, so we need to do it again. After a few days, it was finally settled. Now to sum up.

The basic idea is:

  • JSPDF converts page elements into binary streams
  • Use new Blob to convert binary streams into files
  • Use new FormData to pass the file to the background

Draw DOM elements as binary streams

In the case of plug-in drawing, this step is as simple as calling the HTML () function. But it was also the hardest part, because the plugin didn’t support Chinese at first, and then provided a way to load the font library. Time is limited, find font files are very large, after using the official way to provide, bigger. The project in my work is based on vue2. X, and when the file is introduced, it will directly indicate memory overflow. Later, the interface was used to transfer the converted files to solve the problem. Therefore, for the sake of project performance, formal development is to prepare for the following.

An interface to transfer base64 format fonts

After cloning the local file on Github, click fontConverter > fontConverter.html to convert the prepared Chinese character library into.js file. Personal use of fzytk.ttf (note only support. TTF format), after testing the source boldface can also (but after the conversion of 40M, interface pressure is very large).

After taking out base64 after ‘var font =’ in the converted JS file, put it in a file for back-end development. The back end reads the file through the interface to the front end.

Save the font locally

Because the font is large, transmission is not easy. You are advised to save the font locally in the browser. Localstorage is only 5M in size, so it is recommended to use the new ES6 indexDB for storage. Easy to store and read.

Install JSPDF

npm install jspdf –save or yarn add jspdf

Ready to go, code time.

  import JsPDF from 'jspdf';// Import plug-ins
  import axios from 'axios'
  import { creatUpdateStore, addDataStore, getStoreData } from './indexDB'// Introduce the encapsulated indexDB functions creatUpdateStore: create, addDataStore: add, getStoreData: read
  let font = null;// Global font variable, individual is generated multiple files, after using global variables, can avoid multiple reads
  creatUpdateStore('fonts');// Create an indexDB library named font
  const cretePDF = () = > { 
  return new Promise(resolve= > {
    const PDF = new JsPDF(' '.'pt'.'a4')// Create a PDF object that can be resized to its own size
    if (font === null) { // Check whether the font has a value
      getStoreData('fonts'.1).then(res= > {// font does not have a value. Methods used to obtain indexDB vary and can be adjusted accordingly.
        if(res? .FZYTK) {// There are local fonts
          font = res.FZYTK
          PDF.addFileToVFS('FZYTK.ttf', font)
          PDF.addFont('FZYTK.ttf'.'FZYTK'.'normal')
          PDF.setFont('FZYTK')
          resolve(PDF)
        } else { // There is no local font
          axios.get('XXX').then(response= > {
            font = response.data.data.replaceAll(''.' ')// Back end read file may be generated, directly replaced.
            addDataStore('fonts', { FZYTK: font })
            PDF.addFileToVFS('FZYTK.ttf', font)
            PDF.addFont('FZYTK.ttf'.'FZYTK'.'normal')
            PDF.setFont('FZYTK')
            resolve(PDF)
          })
        }
      })
    } else {//font has a value, set the font directly
      PDF.addFileToVFS('FZYTK.ttf', font)
      PDF.addFont('FZYTK.ttf'.'FZYTK'.'normal')
      PDF.setFont('FZYTK')
      resolve(PDF)
    }
  })
}
    // vue global function.
    Vue.prototype.getNodePdf = function (nodeId) {
      loading = Loading.service({ //loading, if necessary
        lock: true.text: 'Generating PDF files... '.background: 'rgba (0500200, 5)
      })
      return new Promise((resolve, reject) = > {// Generate a promise. After the wrapper file is generated, proceed to the next step
        try {
          const node = document.querySelector(` #${nodeId}`) // Get the DOM to draw
          const clonedNode = node.cloneNode(true) // Clone the DOM to make it easy to change the style of the DOM
          cretePDF().then(PDF= > {// After the PDF object is generated,
            clonedNode.style.width = `${PDF.getPageWidth() - 20}px`,// Get the PDF size and re-size it to the DOM size so that the DOM can be drawn on top of the PDF file.
            PDF.html(clonedNode, {/ / draw the PDF
              callback: function (doc) {
                const output = doc.output('arraybuffer')// Return the result as a stream. Use pdF.save ('pdfName') and the browser will download the generated PDF directly
                resolve(output) // Return the result.
              },
              // start drawing from coordinates 10 and 10
              x: 10.y: 10})})}catch (e) {
          reject(e)
        } finally {
          loading.close()/ / close the loading}})}Copy the code

The PDF file is generated.

Use Blob to convert streams to file files

The following code

    / * * *@description: Converts Bob to file *@param {string} The DOM id *@param {name} File name */
    getPdfFn(id, name) {
      if (id && name) {
        return new Promise(resolve= > {
          this.getNodePdf(id, name).then(file= > {
            const pdfFile = new Blob([file], { type: 'application/pdf' })
            const files = new window.File([pdfFile], `${name}.pdf`, { type: 'application/pdf' })
            resolve(files)
          })
        })
      }
    }
Copy the code

Through the interface to the background

   const _that = this
   const formData = new FormData()
    files.forEach((file, index) = > { //files is the generated PDF collection
      formData.append(`files[${index}] `, file)
    })
    this.$axios.post('XXX', formData)
      .then(function (res) {
        if (res.data.code === 1000) {
          _that.$message.success('Submitted successfully')
          _that.isEdit = false
        } else {
          _that.$message.error(res.data.msg)
        }
      })
Copy the code

Elemental-ui for personal loading and message and can be adjusted using other frameworks.