jsPDF生成PDF介绍

Yukino 454 2023-04-04

一、介绍

jsPDF是一个浏览器端生成PDF文件的库,内容方面可以添加文本、图片、形状等,并可以对其设置样式,例如:颜色、大小、位置等,还可以将DOM直接转换为PDF内容,并实现文本自动分页,结合定位功能还可以实现页眉页脚。

二、基础使用

1. 创建实例

import jsPDF from "jspdf"

// 创建一个jsPDF的实例
const pdf = new jsPDF()

// 创建一个指定纸张、方向、单位的实例
const pdf = new jsPDF({
  orientation: "p",
  unit: "px",
  format: "a4",
  hotfixes: ["px_scaling"],
})

上面的代码中创建了两个实例,其中第一个为默认的,将使用“a4”的纸张、“mm”为单位、“纵向”打印的实例,第二个为通过options指定部分参数的实例(我在开发过程中只用到了这些),下面对这些参数的功能进行简单说明:

名称 类型 默认值 说明
orientation string p 第一页纸张(Page)的方向,portrait(纵向)或者landscape(横向),简写为pl
unit string mm 指定生成PDF中坐标系的单位,可为pt , mm, cm, in, px, pc, em or ex,后面的部分功能的设置(如:定位)就会默认以此作为数值的单位,其中指定为px的时候需要将hotfixes中设定px_scaling
format string a4 第一页纸张大小,可为以下值:a0 - a10b0 - b10c0 - c10dllettergovernment-letterlegaljunior-legalledgertabloid credit-card
hotfixes string[] [] 一个启用补丁的数组(我的理解是类似插件),例如像上面提到的px_scaling就是通过设定这个值为[“px_scaling”],来使px的转换正确

注:上述说的纸张对应的jsPDF的Page,相当于PDF文件中的每一页;

2. 添加文本

pdf.setFontSize(10)
pdf.setTextColor("#bbb")
pdf.text("这是一段文本", 0, 0, {
    align: "left",
    baseline: "alphabetic",
    maxWidth: 20
})

jsPDF的text函数(text(text: string, x: number, y: number, options?: Object))可以在指定位置插入文本,其中第一个参数text为要插入的文本内容,第二和第三个参数是文本插入的起始坐标(将使用创建jsPDF实例时指定的单位),第四个为额外的参数可以指定文本的属性,例如:baseline、align、maxWidth;

在使用text函数添加文本之前,可以使用setFontSize和setFontColor来指定字体大小和颜色,同时也可以使用setFont来设置字体,但需要先导入额外的字体文件;

3. 导入字体

导入字体可以解决两个问题,一个是设置字体样式,一个是导出之后的乱码问题,前者就不用说了,主要是后者,官方文档中有提到,jsPDF内置的标准字体中只有ASCII编码的字符,如果需要使用UTF-8字符集的字符,则需要额外添加字体文件,目前只支持ttf格式的文件:

The 14 standard fonts in PDF are limited to the ASCII-codepage. If you want to use UTF-8 you have to integrate a custom font, which provides the needed glyphs. jsPDF supports .ttf-files. So if you want to have for example Chinese text in your pdf, your font has to have the necessary Chinese glyphs. So, check if your font supports the wanted glyphs or else it will show garbled characters instead of the right text.
To add the font to jsPDF use our fontconverter in /fontconverter/fontconverter.html. The fontconverter will create a js-file with the content of the provided ttf-file as base64 encoded string and additional code for jsPDF. You just have to add this generated js-File to your project. You are then ready to go to use setFont-method in your code and write your UTF-8 encoded text.
Alternatively you can just load the content of the *.ttf file as a binary string using fetch or XMLHttpRequest and add the font to the PDF file.

import SourceHanSansCNBold from "@/assets/fonts/SourceHanSansCNBold.ttf"
import SourceHanSansCNNormal from "@/assets/fonts/SourceHanSansCNNormal.ttf"
import jsPDF from "jspdf"

const FONT_FAMILY = "SourceHanSansCN"
const fontList = [
    {
        url: SourceHanSansCNBold,
        name: "SourceHanSansCNBold",
        type: "bold",
        fontFamily: FONT_FAMILY,
    },
    {
        url: SourceHanSansCNNormal,
        name: "SourceHanSansCNNormal",
        type: "normal",
        fontFamily: FONT_FAMILY,
    },
]

/**
 * 获取字体文件
 * @param fontURL 字体文件URL
 * @returns 字体文件Base64编码字符串
 */
function getFont(fontURL: string): Promise<string> {
    return new Promise(async (resolve) => {
        const res = await fetch(fontURL)
        const blob = await res.blob()
        const reader = new FileReader()
        reader.onload = function () {
            let content = reader.result?.toString() || ""
            content = content.slice(content.indexOf("base64,") + 7)
            resolve(content)
        }
        reader.readAsDataURL(blob)
    })
}

/**
 * 添加字体
 * @param pdf jsPDF实例对象
 */
async function addFont(pdf: jsPDF) {
    const res = await Promise.all(
        fontList.map((font) =>
            getFont(font.url),
        ),
    )
    res.map((fontContent, index) => {
        const { name, fontFamily, type } = fontList[index]
        pdf.addFileToVFS(name, fontContent)
        pdf.addFont(name, fontFamily, type)
    })
}

const pdf = new jsPDF()
addFont(pdf)
pdf.setFont(FONT_FAMILY, "bold")

上述代码中主要是用到三个函数addFileToVFS、addFont和setFont,其中

  • addFileToVFS是将字体文件加入到vFS(virtual FileSystem),第一个参数为文件名,主要用于后面addFont读取文件的标识,并不需要与真实文件名一致,第二个参数则是文件的内容,上面是读取成base64的编码格式;
  • addFont是读取文件添加字体,第一个参数就是文件名(addFileToVFS时设置的文件名,第二个是要字体名称,第三个是字体样式,均是用于后面setFont的标识,如上面代码,同一个字体名称,可以添加不同的字体文件作为不同的样式;
  • setFont则是设置字体,第一个参数是要使用的字体名称,第二个是要使用的字体样式;

4. 添加图片

import LOGO from "@/assets/logos/logo"

pdf.addImage(LOGO, "PNG", 0, 0, 160, 70)

添加图片比较简单,第一个参数是图片的URI,第二个是参数是图片的格式,例如:JPEG、PNG、WEBP等,第三和第四分别是插入的起始坐标x、y,第五和第六分别是要设定的width和height;

5. 添加形状(线条)

jsPDF提供了一系列形状可以直接绘制,如:line、rect、circle等,这里以line为例

pdf.setDrawColor("#000") 
pdf.setLineWidth(1)
pdf.line(20, 25, 60, 25)

在添加形状之前,可以通过setDrawColor来设置线条的颜色,setLineWidth来设置线条的宽度,setFillColor来设置形状的填充颜色(线条是没有的),最后通过line函数(rect和circle则是同名的函数,具体参数请参考文档)添加线条,line的前两个参数和后两个参数分别对应了线条的起止坐标(x,y);

6. 添加Page实现分页

当一页填充满后,我们可以通过addPage的方法添加新的一页,同时也会自动切换到这一页,addPage有两个参数,分别是format和orientation,与构造函数中的一致;

pdf.addPage('a4', 'p')

同时我们也可以通过setPage函数在不同页之间切换(以0为起始);

pdf.setPage(0)

7. 添加DOM并实现自动分页和页眉页脚

通过上面的内容可以发现,手动去添加文本、图片、形状都需要指定起始坐标和长宽,这就比较麻烦,我们有时也无法知道具体的位置,这时候如果能直接将DOM导出/添加到PDF中就比较方便了,jsPDF也提供了html函数用于添加DOM,基础使用方式如下:

const element = documnet.getElementById("target")
pdf.html(element, {
    callback: () => {
        pdf.save()
    },
    autoPaging: "text",
    margin: 60,
    windowWidth: element.offsetWidth,
    width: element.offsetWidth,
})

与前面的方法不一样的是,添加DOM是一个异步的方法,需要通过callback触发完成,第一个参数为目标DOM,第二个参数则是options,设置一些可选项,下面对上面用到的参数进行简单的解释:

名称 类型 描述
autoPaging boolean| ‘slice’ | ‘text’ 自动分页的模式,其中text会根据文本高度和剩余高度自动分页,以防止文本被切割到两页;true或者slice,则会切割文本、形状;false则不会自动分页
margin number | number[] 外边距,使用数组可以对每个方向进行设定,顺序与CSS一致[top, right, bottom, left]
windowWidth number 元素的宽度,单位px
width number 元素在PDF文档中的宽度,单位为创建实例时设定的宽度,在添加元素时会根据windowWidth和width对元素进行缩放,使其满足目标宽度

上面我们就完成了将DOM添加到PDF文档中,并实现了自动分页、添加外边距,这时我们就可以在外边距的区域添加页眉页脚了:

const element = documnet.getElementById("target")
const exportToPdf = (element: HTMLDivElement | null | undefined): Promise<jsPDF> => {
    return new Promise(async (resolve, reject) => {
        if (!element) {
            reject("element is null")
            return
        }
        const pdf = new jsPDF({
            orientation: "p",
            unit: "px",
            format: "a4",
            hotfixes: ["px_scaling"],
        })
        await addFont(pdf)
        pdf.html(element, {
            callback: () => {
                resolve(pdf)
            },
            autoPaging: "text",
            margin: pageMargin,
            windowWidth: element.offsetWidth,
            width: element.offsetWidth,
        })
    })
}

const pdf = await exportToPdf(element)
const pageNums = pdf.getNumberOfPages()
for (let page = 0; page < pageNums; page++) {
    pdf.setPage(page)
    // 按需求添加页眉页脚
}

简单归纳下,就是通过getNumberOfPages获取PDF文档页数,并通过setPage的方法在页面间切换,在外边距区域,添加页眉页脚元素;

8. 导出

jsPDF提供了save和output两个函数用于导出文件,其中

  • save有一个参数filename,将自动导出成PDF文件,并下载;
  • output有两个参数,第一个参数是导出类型,例如"blob"、"datauri"等,第二个则是特殊导出类型的额外参数,一般用不到;
pdf.save("test.pdf")

const blob = pdf.output("blob")