光出现的原因
公司最近有这么个需求,预览pdf、excel、word、图片,因为前段时间太忙了,正好最近有点时间,直接废寝忘食、头悬梁最刺骨的开启知识的补充。
准备知识
要较好的完成这个功能的话,需要做一些前期的学习工作。有些知识点我描述起来感觉不太通俗易懂的都被我换成官方的描述或者是网上看起来比较官方和严谨的描述,并且描述中会带一些学习地址的链接,方便大家深入学习和理解。
pdfjs-dist
pdfjs-dist是一个通用的 PDF.js 构建库,这是 PDF.js 源码的预先构建版本,可以通过构建脚本自动生成 PDF.js。
pdf.js
pdf.js 是一个技术原型主要用于在 HTML5 平台上展示 PDF 文档,无需任何本地技术支持。我们的目标是创建一个通用的基于 web 标准的平台,并支持分析和展示 PDFs。
FileReader
FileReader
对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File
或 Blob
对象指定要读取的文件或数据。
FileReader MDN
Canvas
Canvas
是一个可以使用脚本 (通常为JavaScript) 来绘制图形的 HTML 元素
Canvas MDN
完整代码
实现过程本来是要分成好几个目录写的,我发现我在学习的时候每一行代码都写上了注释,属实有点强迫症,所以现在代码就是最好的讲解。
小提示:
不想看文章其他内容的,vue3.2以上可以直接ctrl+A -> ctrl+C -> ctrl+V前端三部曲,直接进行使用测试。其他vue版本需要改一改变量的定义,以及代码的位置,通用。
<template>
<div>
<input id="fileinput" type="file" @change="uploadFile" />
<div id="canvasCont">
<canvas v-for="index in canvasTotalPage" :id="`myVancas${index}`" :key="index"></canvas>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import * as pdfjsLib from 'pdfjs-dist/build/pdf';
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.6.347/pdf.worker.min.js';
/**
* pdf下载以及加载函数(异步)
*/
const pdfLoadTask = ref();
/**
* 需要绘制pdf的总页数
*/
const canvasTotalPage = ref<number>(1);
/**
* 引入pdf.js的字体,插件无法解析某些特殊字体,所以需要加载特定字体cmap文件
* 使用cdn的的形式,内网或者没网的时候会失效,建议直接把这个目录下的文件下载到本地,引用本地的资源
*/
const cmapUrl = ref<string>('https://unpkg.com/pdfjs-dist@2.6.347/cmaps/');
/**
* 上传pdf文件
*/
function uploadFile() {
// 获取到上传文件input组件的dom实例,实际上用ref也行,但是vue2和vue3使用ref的写法上有区别,就不麻烦搞ref了,统一用id了
const file = document.getElementById('fileinput').files[0];
// FileReader是一个强大的读取文件的api,创建一个FileReader实例来读取文件
const reader = new FileReader();
// 将文件内容转换为base64编码的url,方便pdfjs-dist插件加载pdf文档
reader.readAsDataURL(file);
// FileReader读取文件完成后触发,此时拿到base64编码的url了
reader.onload = () => {
// 直接用atob代码ts会报错,因为网上说这个已经弃用了,但是在代码里面还是能用,不过ts会报错
const data = window.atob(reader.result.substring(reader.result.indexOf(',') + 1));
// 拿到base64编码的url去加载pdf文档
loadPdfData(data);
};
}
/**
* 通过pdfjs-dist插件生产pdf
* @param data base64编码的url
*/
function loadPdfData(data: string) {
// pdfjsLib.getDocument是获取pdf文档的方法,返回的是premise对象,对象包含一些pdf文档的信息以及操作pdf文档的api
pdfLoadTask.value = pdfjsLib.getDocument({
data: data,
cMapUrl: cmapUrl.value,
cMapPacked: true,
});
// 渲染页面,至少一页
renderPage(1);
}
/**
* 渲染指定页码的pdf文档
* @param num 指定页码
*/
function renderPage(num: number) {
// 异步函数结束后返回pdf的基本信息以及一些api,是一个对象
pdfLoadTask.value.promise.then((pdf: any) => {
// 记录一下总页数,多页的情况,每页都需要新建一个画布
canvasTotalPage.value = pdf.numPages;
// 通过调用pdf对象的getPage方法,将指定的页码传入,可以获取传入页码的引用
pdf.getPage(num).then((page: any) => {
// 获取canvas的DOM对象
const canvas: any = document.getElementById(`myVancas${num}`);
// 获取canvas的渲染上下文(包含canvas的引用以及绘图功能)
const ctx = canvas.getContext('2d');
// 获取页面的像素比率
const ratio = getRatio(ctx);
// 页面的视口宽度,也就是元素的可视宽度
// 不太理解用offsetWidth,可以看下这篇文章https://zhuanlan.zhihu.com/p/603633893
const viewWidth = document.getElementById('canvasCont').offsetWidth;
// pdf文档的宽度,不懂为啥用page.view[2]的话,可以打印一下page看看page返回的具体是啥信息
const pdfWidth = page.view[2];
// 根据视口的宽度/pdf文档宽度得到缩放比
const scale = viewWidth / pdfWidth;
// 获取pdf文档的缩放后基本信息
const viewport = page.getViewport({ scale });
// 画布的宽高需要根据实际像素调整,避免出现模糊的情况
canvas.width = viewport.width * ratio;
canvas.height = viewport.height * ratio;
// 准备page.render()函数需要的参数
const renderContext = {
canvasContext: ctx,
viewport: viewport,
};
// 将数据渲染到画布上
page.render(renderContext).promise.then(() => {
// 添加水印
addWatermark(num, canvas.width, canvas.height);
});
// 多页pdf的情况
if (num < pdf.numPages) {
renderPage(num + 1);
}
});
});
}
/**
* 在画布上添加水印
* @param num 画布索引
*/
function addWatermark(num: number, width: number, height: number) {
const canvas: any = document.getElementById(`myVancas${num}`);
const ctx = canvas.getContext('2d');
// 方法在指定的方向内重复指定的元素,元素可以是图片、视频,或者其他 <canvas> 元素,被重复的元素可用于绘制/填充矩形、圆形或线条等等。
const pattern = ctx.createPattern(initWatermark(), 'repeat');
// 创建矩形
ctx.rect(0, 0, width, height);
// fillStyle 属性设置或返回用于填充绘画的颜色、渐变或模式,也可以设置pattern。
ctx.fillStyle = pattern;
// 当前的图像
ctx.fill();
}
/**
* 初始化水印元素
*/
function initWatermark() {
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;
const ctx: any = canvas.getContext('2d');
ctx.rotate((45 * Math.PI) / 180);
ctx.font = '15px Verdana';
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillText('我是水印', 30, 30);
return canvas;
}
/**
* 获取页面的比率
* @param ctx canvas绘图环境的上下文
*/
function getRatio(ctx: any) {
// window对象中的属性devicePixelRatio,这个属性决定了浏览器会用多少实际的像素来渲染一个像素(也可以理解为css像素)
// 至于为什么要获取这个,不同设备的像素比不同,当这个像素比为2时,会用2个实际像素来渲染一个css像素,就相当于放大了视图一倍,会导致屏幕显示模糊
const dpr = window.devicePixelRatio || 1;
// canvas的context上下文中也存在一些关于像素比的属性,逻辑是canvas是用几个像素来存储画布信息
// webkit、moz、ms、o这些代码的是不同浏览器之间的内核识别码
const bsr =
ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio ||
1;
return dpr / bsr;
}
</script>
最终实现的功能
- 支持单张pdf的预览
- 支持多张pdf滚动预览,实际上就是书本预览,pdf的电子书可以直接预览