需求背景
公司产品的项目上要求给导出的 pdf 文件添加平铺式水印,当前项目使用的 pdf 导出插件是 pdfmake ,但是该插件并没有实现平铺式水印,官方自带的水印效果无法满足项目要求,需要自行编写水印生成代码
源码分析
通过研究 pdfmake 源码中的 createpdf,可以得知内部实现 pdf 生成是通过 _createdoc 方法创建 pdfkit 实例,并使用该插件的 API 完成相关的文档操作,进一步研究得知 _createdoc 方法还提供了一个回调函数,通过改回调函数可以对文档进行二次交互,进而可以考虑在回调函数中操作文档生成平铺式水印的思路,最后通过对 download 源码的理解实现 save 方法将添加水印后的文档导出为 pdf 文件
源码参考
- pdfmake/src/browser-extensions/pdfMake.js#createPdf at 0.2 · bpampuch/pdfmake (github.com)
- pdfmake/src/browser-extensions/pdfMake.js#_createdoc at 0.2 · bpampuch/pdfmake (github.com)
- pdfmake/src/browser-extensions/pdfMake.js#download at 0.2 · bpampuch/pdfmake (github.com)
- pdfmake-document
- Text in PDFKit
实现代码
完整的实现代码如下所示,该实现依赖于 pdfmake": "^0.2.13",请自行安装相关依赖,其中用于文件保存的 FileSaver.js 可从 github 上下载:github.com/eligrey/Fil...
ts
import pdfMake, { createPdf } from 'pdfmake' // 基于 v0.2
import pdfFonts from 'pdfmake/build/vfs_fonts'
import saveAs from './FileSaver'
pdfMake.vfs = pdfFonts.pdfMake.vfs
export class PDFFile {
private readonly pdfDocument: any = null
private readonly _docDefinition: any = {}
/**
* Creates an instance of PDFFile.
* @param {*} docDefinition reference to https://pdfmake.github.io/docs/0.1/document-definition-object/
* @memberof PDFFile
*/
constructor(docDefinition: any) {
this._docDefinition = docDefinition
// 导入自定义中文字体包,如果不导入的话使用中文字体会有问题,中文字体包请自行下载
const root = '/'
const fonts = {
commonFont: {
normal: root + 'fonts/SourceHanSansSC-Normal-2.otf',
bold: root + 'fonts/SourceHanSansSC-Medium-2.otf',
italics: root + 'fonts/SourceHanSansSC-Normal-2.otf',
bolditalics: root + 'fonts/SourceHanSansSC-Medium-2.otf',
},
}
this.pdfDocument = createPdf(
{
...docDefinition,
watermark: undefined, // 水印另外实现,且只支持文本水印
},
null,
fonts
)
}
/**
* 保存文档
*
* @param {boolean} download 是否自动下载文档,如果为否则返回一个 blob 对象
* @param {string} [fileName] 文档名称名称
* @return {*}
* @memberof PDFFile
*/
public save(download: boolean, fileName?: string) {
return new Promise((resolve, reject) => {
// 使用 pdfmake 内部的 _createDoc 以获取 pdfkit 实例用于生成水印
// doc options reference to https://pdfkit.org/docs/getting_started.html#switching_to_previous_pages
// To use it, just pass bufferPages: true as an option to the PDFDocument constructor.
// Then, you can call doc.switchToPage(pageNumber) to switch to a previous page (page numbers start at 0).
this.pdfDocument._createDoc({ bufferPages: true }, (doc: any) => {
// console.log(this.pdfDocument, doc)
const rejectTimeout = setTimeout(() => reject('Timeout!'), 5000) // 等待超时未能正常导出判定为失败
const range = doc.bufferedPageRange() // { start: number, count: number }
// loop throgh all pages
const { text } = this._docDefinition.watermark
if (text) {
// 给所有页面添加平铺式水印
for (let i = 0; i < range.start + range.count; i++) {
const watermark = Object.assign({
color: 'black',
opacity: 0.2,
angle: -50,
fontSize: 14
}, this._docDefinition.watermark)
doc.switchToPage(i)
doc.fill(watermark.color)
doc.opacity(watermark.opacity)
doc.rotate(watermark.angle, { origin: [doc.page.width / 2, doc.page.height / 2] })
doc.fontSize(watermark.fontSize)
// 获取当前页宽高
const { width, height } = doc.page
const margin = [200, 100] // 水平/垂直间距
for (let ix = 0; ix <= width + margin[0] + 70; ix += margin[0]) {
// 水印横向间隔
let lineNum = 0
for (let iy = 0; iy <= height; iy += margin[1]) {
// 水印纵向间隔
lineNum++
const pos = [lineNum & 1 ? ix - 70 : ix, iy]
// console.log(pos)
doc.text(text, pos[0], pos[1], { lineBreak: false })
}
}
}
}
this.pdfDocument._flushDoc(doc, (buffer: any) => {
// get a blob you can do whatever you like with
const blob = this.pdfDocument._bufferToBlob(buffer)
clearTimeout(rejectTimeout)
if (download) {
saveAs(blob, fileName + '.pdf')
resolve(true)
} else {
resolve(blob)
}
})
})
})
}
}
使用示例
ts
const watermark = {
text: 'your watermark text',
}
const pdfFile = new PDFFile({
// 这里除了增加 watermark 属性外,其他属性和官方文档完全一致
watermark,
content: pdfContent,
defaultStyle: {
font: 'commonFont'
}
})
pdfFile.save(true, '导出示例')
实现效果
