一、预览文件
1、浏览器新标签页中预览
通过 a 标签预览,只展示文件名称,点击链接以后,在新标签页中预览。
<a href="文件地址" target="_blank">网站备案信息真实性承诺书</a>
2、浏览器当前标签页中预览
通过 iframe 标签或 embed 标签进行预览,直接在当前标签页预览。
<iframe src="文件地址" frameborder="0" width="100%" height="100%" />
<embed src="文件地址" width="100%" height="100%" />
二、隐藏工具栏
只需要在标签的 src 路径后面拼接 #toolbar=0 即可隐藏工具栏
<a href="文件地址#toolbar=0" target="_blank">网站备案信息真实性承诺书</a>
<iframe src="文件地址#toolbar=0" frameborder="0" width="100%" height="100%" />
<embed src="文件地址#toolbar=0" width="100%" height="100%" />
三、使用pdf-lib添加水印并预览
1、安装 pdf-lib
npm install pdf-lib -S
2、引入
import { degrees, PDFDocument, StandardFonts, rgb } from 'pdf-lib'
3、添加水印
3.1、文本水印
将文本添加进去作为水印,但是只支持英文,不支持中文。如果需要使用中文,必须安装自定义字体库,我自己也尝试了一下,自定义字体引入后老是报错没有该字体,不太推荐使用。
// 安装自定义字体库依赖
npm install "@pdf-lib/fontkit" -S
// 引入
import fontkit from "@pdf-lib/fontkit"
// 添加文本水印
async modifyPdf(title, url) {
const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica) // 只支持英文
// 自定义字体包路径,放在 public 目录
// const fonturl = './font/font.ttf'
// const fontBytes = await fetch(fonturl).then(res => res.arrayBuffer())
// pdfDoc.registerFontkit(fontkit)
// const customFont = await pdfDoc.embedFont(fontBytes)
const pages = pdfDoc.getPages()
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
// width
const { height } = page.getSize()
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
page.drawText('SYCEE', {
x: j * 100,
y: height / 5 + i * 100,
size: 30,
font: helveticaFont,
// font: customFont, // 自定义字体
color: rgb(0.95, 0.1, 0.1),
opacity: 0.2,
rotate: degrees(-35)
})
}
}
}
// 序列化为字节
const pdfBytes = await pdfDoc.save()
// 预览
this.preView(title, pdfBytes)
},
3.2、图片水印
将图片的 ArrayBuffer 传递进去,将图片作为水印,支持 png 和 jpg 格式的图片,可参考官网,图片水印位置可以自己调整一下。
// 添加图片水印
async embedImages(title, url) {
const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const pngUrl = 'https://pdf-lib.js.org/img/logo3.png'
const pngImageBytes = await fetch(pngUrl).then(res => res.arrayBuffer())
const pngImage = await pdfDoc.embedPng(pngImageBytes)
const pngDims = pngImage.scale(0.5)
const pages = pdfDoc.getPages()
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
const { width, height } = page.getSize()
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
page.drawImage(pngImage, {
x: width / 2 - pngDims.width / 2 + j * 100,
y: height / 2 - pngDims.height + i * 100,
width: pngDims.width,
height: pngDims.height,
rotate: degrees(-35)
})
}
}
}
// 序列化为字节
const pdfBytes = await pdfDoc.save()
// 预览
this.preView(title, pdfBytes)
},
3.3、canvas 水印(推荐)
canvas 水印其实也是传入文本,然后通过 canvas 画出来。原理是通过 toDataURL 转为 图片的 base64 路径,然后通过 XHR 去加载该图片拿到图片的 Blob,再调用 Blob 的arrayBuffer 方法拿到 buffer 传递进去作为水印,此方法更灵活。但是通过 XHR 加载图片的方式打包发布以后会报错(查不出原因),所以此部分的代码我采用了添加图片水印的方式进行了优化,代码中会有说明。
// 添加 canvas 水印
async canvasWatermark(title, url) {
const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(existingPdfBytes)
// 旋转角度大小
const rotateAngle = Math.PI / 6
// labels是要显示的水印文字,垂直排列
const labels = []
labels.push('巨蟹座不吃鱼SYCEE')
const pages = pdfDoc.getPages()
const size = pages[0].getSize()
const pageWidth = size.width
const pageHeight = size.height
// 生成 canvas 文字图片
const canvas = document.createElement('canvas')
let canvasWidth = (canvas.width = pageWidth)
let canvasHeight = (canvas.height = pageHeight)
const context = canvas.getContext('2d')
context.font = '32px Arial'
context.shadowColor = 'rgba(187, 187, 187, .8)'
// 在绕画布逆方向旋转30度
context.rotate(-rotateAngle)
// 获取文本的最大长度
const textWidth = Math.max(...labels.map(item => context.measureText(item).width))
const lineHeight = 20
const fontHeight = 40
let positionY = 0
let i = 0
while (positionY <= pageHeight) {
positionY = positionY + lineHeight * 5
i++
}
canvasWidth += Math.sin(rotateAngle) * (positionY + i * fontHeight) // 给canvas加上画布向左偏移的最大距离
canvasHeight = 2 * canvasHeight
for (positionY = 0, i = 0; positionY <= canvasHeight; positionY = positionY + lineHeight * 5) {
// 进行画布偏移是为了让画布旋转之后水印能够左对齐;
context.translate(-(Math.sin(rotateAngle) * (positionY + i * fontHeight)) - (i % 2) * 50, 0)
for (let positionX = 0; positionX < canvasWidth; positionX += 2 * textWidth - 100) {
let spacing = 0
labels.forEach(item => {
context.fillText(item, positionX, positionY + spacing)
context.fillStyle = 'rgba(187, 187, 187, .8)' // 字体颜色
spacing = spacing + lineHeight
})
}
context.translate(Math.sin(rotateAngle) * (positionY + i * fontHeight), 0)
context.restore()
i++
}
// ---------- 打包发布后 res.target 报错 undefined---------------
// // 图片的base64编码路径
// const dataUrl = canvas.toDataURL('image/png')
// // 使用Xhr请求获取图片Blob
// const xhr = new XMLHttpRequest()
// xhr.open('get', dataUrl, true)
// xhr.responseType = 'blob'
// xhr.onload = res => {
// const imgBlob = res.target.response
// // 获取Blob图片Buffer
// imgBlob.arrayBuffer().then(async buffer => {
// const pngImage = await pdfDoc.embedPng(buffer)
// for (let i = 0; i < pages.length; i++) {
// pages[i].drawImage(pngImage)
// }
// // 序列化为字节
// const pdfBytes = await pdfDoc.save()
// // 预览
// this.preView(title, pdfBytes)
// })
// }
// xhr.send()
// ---------- 打包发布后 res.target 报错 undefined---------------
// ----------- 优化 --------------------
// 图片的base64编码路径
const png = canvas.toDataURL('img/png')
// 获取图片
const imagePDF = await pdfDoc.embedPng(png)
for (let i = 0; i < pages.length; i++) {
pages[i].drawImage(imagePDF)
}
// 序列化为字节
const pdfBytes = await pdfDoc.save()
// 预览
this.preView(title, pdfBytes)
// ----------- 优化 --------------------
},
4、完整代码
<template>
<div class="app-container">
<div style="width: 100%; height: 860px">
<div style="cursor: pointer;" @click="addWatermark">网站备案信息真实性承诺书</div>
</div>
</div>
</template>
<script>
import { degrees, PDFDocument, StandardFonts, rgb } from 'pdf-lib'
export default {
data() {
return {}
},
created() {
// 根据不同环境获取IP地址
let baseURL
if (process.env.NODE_ENV === 'production') {
baseURL = window.api.apiURL
} else {
baseURL = process.env.VUE_APP_BASE_API
}
this.base_url = baseURL
},
methods: {
addWatermark() {
// 为防止跨域问题,文件路径进行了拼接
var url = this.base_url + '/review/GW11P22-test/CDCP/%E7%BD%91%E7%AB%99%E5%A4%87%E6%A1%88%E4%BF%A1%E6%81%AF%E7%9C%9F%E5%AE%9E%E6%80%A7%E6%89%BF%E8%AF%BA%E4%B9%A6.pdf'
var title = '网站备案信息真实性承诺书'
// 添加文本水印
// this.modifyPdf(title, url)
// 添加图片水印
// this.embedImages(title, url)
// // 添加 canvas 水印
this.canvasWatermark(title, url)
},
// 添加文本水印
async modifyPdf(title, url) {
const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica) // 只支持英文
// 自定义字体包路径,放在 public 目录
// const fonturl = './font/font.ttf'
// const fontBytes = await fetch(fonturl).then(res => res.arrayBuffer())
// pdfDoc.registerFontkit(fontkit)
// const customFont = await pdfDoc.embedFont(fontBytes)
const pages = pdfDoc.getPages()
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
// width
const { height } = page.getSize()
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
page.drawText('SYCEE', {
x: j * 100,
y: height / 5 + i * 100,
size: 30,
font: helveticaFont,
// font: customFont, // 自定义字体
color: rgb(0.95, 0.1, 0.1),
opacity: 0.2,
rotate: degrees(-35)
})
}
}
}
// 序列化为字节
const pdfBytes = await pdfDoc.save()
// 预览
this.preView(title, pdfBytes)
},
// 添加图片水印
async embedImages(title, url) {
const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(existingPdfBytes)
const pngUrl = 'https://pdf-lib.js.org/img/logo3.png'
const pngImageBytes = await fetch(pngUrl).then(res => res.arrayBuffer())
const pngImage = await pdfDoc.embedPng(pngImageBytes)
const pngDims = pngImage.scale(0.5)
const pages = pdfDoc.getPages()
for (let i = 0; i < pages.length; i++) {
const page = pages[i]
const { width, height } = page.getSize()
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
page.drawImage(pngImage, {
x: width / 2 - pngDims.width / 2 + j * 100,
y: height / 2 - pngDims.height + i * 100,
width: pngDims.width,
height: pngDims.height,
rotate: degrees(-35)
})
}
}
}
// 序列化为字节
const pdfBytes = await pdfDoc.save()
// 预览
this.preView(title, pdfBytes)
},
// 添加 canvas 水印
async canvasWatermark(title, url) {
const existingPdfBytes = await fetch(url).then(res => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(existingPdfBytes)
// 旋转角度大小
const rotateAngle = Math.PI / 6
// labels是要显示的水印文字,垂直排列
const labels = []
labels.push('巨蟹座不吃鱼SYCEE')
const pages = pdfDoc.getPages()
const size = pages[0].getSize()
const pageWidth = size.width
const pageHeight = size.height
// 生成 canvas 文字图片
const canvas = document.createElement('canvas')
let canvasWidth = (canvas.width = pageWidth)
let canvasHeight = (canvas.height = pageHeight)
const context = canvas.getContext('2d')
context.font = '32px Arial'
context.shadowColor = 'rgba(187, 187, 187, .8)'
// 在绕画布逆方向旋转30度
context.rotate(-rotateAngle)
// 获取文本的最大长度
const textWidth = Math.max(...labels.map(item => context.measureText(item).width))
const lineHeight = 20
const fontHeight = 40
let positionY = 0
let i = 0
while (positionY <= pageHeight) {
positionY = positionY + lineHeight * 5
i++
}
canvasWidth += Math.sin(rotateAngle) * (positionY + i * fontHeight) // 给canvas加上画布向左偏移的最大距离
canvasHeight = 2 * canvasHeight
for (positionY = 0, i = 0; positionY <= canvasHeight; positionY = positionY + lineHeight * 5) {
// 进行画布偏移是为了让画布旋转之后水印能够左对齐;
context.translate(-(Math.sin(rotateAngle) * (positionY + i * fontHeight)) - (i % 2) * 50, 0)
for (let positionX = 0; positionX < canvasWidth; positionX += 2 * textWidth - 100) {
let spacing = 0
labels.forEach(item => {
context.fillText(item, positionX, positionY + spacing)
context.fillStyle = 'rgba(187, 187, 187, .8)' // 字体颜色
spacing = spacing + lineHeight
})
}
context.translate(Math.sin(rotateAngle) * (positionY + i * fontHeight), 0)
context.restore()
i++
}
// ---------- 打包发布后 res.target 报错 undefined---------------
// // 图片的base64编码路径
// const dataUrl = canvas.toDataURL('image/png')
// // 使用Xhr请求获取图片Blob
// const xhr = new XMLHttpRequest()
// xhr.open('get', dataUrl, true)
// xhr.responseType = 'blob'
// xhr.onload = res => {
// const imgBlob = res.target.response
// // 获取Blob图片Buffer
// imgBlob.arrayBuffer().then(async buffer => {
// const pngImage = await pdfDoc.embedPng(buffer)
// for (let i = 0; i < pages.length; i++) {
// pages[i].drawImage(pngImage)
// }
// // 序列化为字节
// const pdfBytes = await pdfDoc.save()
// // 预览
// this.preView(title, pdfBytes)
// })
// }
// xhr.send()
// ---------- 打包发布后 res.target 报错 undefined---------------
// ----------- 优化 --------------------
// 图片的base64编码路径
const png = canvas.toDataURL('img/png')
// 获取图片
const imagePDF = await pdfDoc.embedPng(png)
for (let i = 0; i < pages.length; i++) {
pages[i].drawImage(imagePDF)
}
// 序列化为字节
const pdfBytes = await pdfDoc.save()
// 预览
this.preView(title, pdfBytes)
// ----------- 优化 --------------------
},
// 在浏览器新的标签页预览PDF
preView(docTitle, stream) {
const URL = window.URL || window.webkitURL
const href = URL.createObjectURL(new Blob([stream], { type: 'application/pdf;charset=utf-8' }))
// 由于项目要求,需要隐藏工具栏,所以拼接了 '#toolbar=0',删除拼接即可展示工具栏
const wo = window.open(href + '#toolbar=0')
// 设置新打开的页签 document title
const timer = setInterval(() => {
if (wo.closed) {
clearInterval(timer)
} else {
wo.document.title = docTitle
}
}, 500)
}
}
}
</script>