首页 » 网站建设 » phpvuepdf预览技巧_VUE 实现高机能的 PDF 在线预览

phpvuepdf预览技巧_VUE 实现高机能的 PDF 在线预览

访客 2024-12-10 0

扫一扫用手机浏览

文章目录 [+]

PDF 文档的预览,总的便是要加载速率快,尽最快的速率完成渲染,呈现给用户看,不要涌现永劫光的白屏或 Loading 状态的征象,其余 PDF 文档须要支持翻页等操作。
详细看看一步步的实现。

文档分片下载速率

phpvuepdf预览技巧_VUE 实现高机能的 PDF 在线预览

PDF 文档上传

分片上传文档,支持秒传,VUE 支持分片上传的插件一搜一大把,可以采取 vue-simple-uploader 等,详细如何实现,这里不详细论述,大略贴一下秒传校验的实现。

phpvuepdf预览技巧_VUE 实现高机能的 PDF 在线预览
(图片来自网络侵删)

import SparkMD5 from 'spark-md5';/ 文件秒传 MD5 校验 @param file 上传的文件信息 /md5File(file) { const fileReader = new FileReader(), blobSlice = File.prototype.slice, chunkSize = 1024 1000,// 分片大小 chunks = Math.floor(file.size / chunkSize),// 总的分片数量 spark = new SparkMD5.ArrayBuffer();// 三方库 SparkMD5 let currentChunk = 0; // 加载分片 const loadNext = () => { const start = currentChunk chunkSize; let end = file.size; if (currentChunk < chunks - 1) end = start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end)); } // 停息文件上传 file.pause(); // 开始校验文件MD5 loadNext(); fileReader.onload = (e) => { spark.append(e.target.result); // 小于总分片, 连续加载 if (currentChunk < chunks - 1) { currentChunk++; loadNext(); } else { // 分片全部加载完成, 天生 MD5 const md5 = spark.end(); // 开始做事端校验 MD5 ( 秒传 ) this.md5Success(md5, file); } }; fileReader.onerror = () => { // 文件读取出错, 取消上传 console.log(`文件${file.name}读取出错,请检讨该文件`); file.cancel(); };}

文件秒传MD5校验

PDF 文档分片

一个 PDF 文档,无法一次就预览所有内容,在有限的可视区域内,只能显示有限的内容,那我们就获取能在有限区域内所能展示的那部分内容,以加快 Content Download 的速率,减少用户第一次打开时的 Loading 韶光。

假设一个 PDF 文档有 1000 页,以 5 页为一片,将该文档切分成 200 个分片,首次打开默认要求第一个分片,其后根据翻页来确定是否连续加载后续的分片信息(如需刷新后仍旧展示刚刚所在页,则需记录当前页,根据该值与分片的页数来确定当前属于第几个分片,进而再要求相应分片即可)。

做事端如何进行分片,则交给做事端就好了,这里就不详细说了(得把稳下中文乱码的情形)。
假设文件信息格式及单个分片的要求地址如下所示:

/ 文件信息. 在文件上传后即可拿到. /const file = { id: 1, md5: 'e10adc3949ba59abbe56e057f20f883e', total: 1000, name: 'VUE 如何实现高性能的 PDF 在线预览', // ...}/ 要求分片. $http 是我针对 axios 的一些常用方法,拦截器等重新封装后工具类库 /

文件信息格式及分片要求地址格式

要求 PDF 分片

pdf.js 接口中,getDocument 可用于获取远程文档,返回 PDFDocumentLoadingTask 工具,该工具是一个下载远程 PDF 文档的任务,供应了一些监听方法,可通过 promise 拿到下载完成的 PDF 工具,终极会天生并返回 PDFDocumentProxy 工具,我们接下来所有的操作都是基于该代理类进行的。

把稳在 PDF 文档中存在有中文时,会涌现不显示的情形,掌握台也会报如下的缺点提示

Warning: The CMap "baseUrl" parameter must be specified, ensure that the "cMapUrl" and "cMapPacked" API parameters are provided.

紧张是 PDF 文档内容存在不支持的字体,暂且引入三方字体来办理该问题

const url = `https://www.makeit.vip/${md5}-${page}.pdf?id=${fid}&token=${token}`,cMapUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.4.456/cmaps/';const promise = PDFJS.getDocument({url,cMapUrl,cMapPacked: true}).promise;渲染 PDF 文档

由于我实现的是 PDF 一页的内容,按屏幕尺寸 100% 宽度来显示的,这样很随意马虎高度就超出可视范围了,以是单页采取滚动形式,多页则采取按钮触发翻页的形式来展示(为了更好的适配不同尺寸的屏幕,显示的效果与主讲人完备同步),并非采取一字往下无限排,滚动翻页的形式。

/ 获取分片 @param fid 文件ID @param md5 文件唯一标识 @param token 授权码 @param num 第N个分片 /public getFragmentation(fid: number,md5: string,token: string,num = 0) {// 要求分片, 得到 promise...// ...promise.then((pdf: any) => { for (let i = 1; i <= pdf.numPages; i++) { pdf.getPage(i).then((page) => { const pagination = num 5 + i; this.renderPage(pagination, page); }); }});}/ 渲染分页内容. @param pagination 第N页 @param page 分页属性 /protected renderPage(pagination: number,page: any) {// 根据缩放比例, 获取文档的可视属性const viewport = page.getViewport({scale: 1});// 创建用于渲染的Canvas元素const canvas = document.createElement('canvas'),context = canvas.getContext('2d');canvas.width = viewport.width;canvas.height = viewport.height;// 渲染文档const renderContext = {canvasContext: context,viewport};return page.render(renderContext).promise;}PDF 文档翻页

在上一页/下一页的不断操作中,1000页的内容,不断的进行渲染,难不成要渲染1000个DOM出来?显然不合理,非得把浏览器给搞崩了才肯罢休吗?几十个 Canvas 就让你卡的不要不要的了。
详细实现也大略,担保只显示 5 个的条件下,根据上一页或下一页的操作,增加或删除相应的 DOM即可。
末了贴一下轻微完全一些的代码(轻微加了一些注释)。

获取分片

/ 获取分片 @param fid 文件ID @param md5 文件唯一标识 @param token 授权码 @param num 第N个分片 @param showPage 显示第N个分片中的第X页 @param speaker 是否为主讲人 @param render 是否直接渲染 @param clear 是否打消原有内容 /public getFragmentation(fid: number,md5: string,token: string,num: number = 0,showPage = 1,speaker?: boolean,render?: boolean,clear?: boolean): Promise<any> {const url = `${process.env.VUE_APP_PROXY_SERVER}/${md5}-${num}.pdf?id=${fid}&token=${token}`,cMapUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.4.456/cmaps/';let promise = PDFJS.getDocument({url,cMapUrl,cMapPacked: true}).promise;promise.then((pdf: any) => {/ 记录 PDFDocumentProxy 工具, 可避免重复要求已经要求过的分片 /this.files.page = showPage;if (!this.files.pdfs) this.files.pdfs = {} as any;this.files.pdfs[num] = pdf;});/ 是否实行渲染操作 /if (render) {/ 打消 /if (clear) {const documents = this.getContainer() as HTMLDivElement;if (documents) documents.innerHTML = '';}/ 渲染 - 重新赋值 promise, 担保加载完成后的操作时序 /promise = new Promise((resolve) => {promise.then((pdf: any) => {/ 开始遍历循环 /for (let i = 1; i <= pdf.numPages; i++) {pdf.getPage(i).then((page) => {const pagination = num 5 + i;if (!this.files.paginations[pagination]) {/ 存储分页信息(宽/高/ID等 - ID可用于判断是否已经渲染及打消DOM操作) /this.files.paginations[pagination] = {} as any;}this.files.paginations[pagination].id = Utils.uid();const renderFinish = this.renderPage(pagination, page, speaker);if (renderFinish) {renderFinish.then(() => {if (i === pdf.numPages) {/ 1. 当前为末了一页或倒数第2页, 要求下一分片 2. 当前为第一页或第2页, 要求上一分片 /const left = showPage % 5;if (left === 0 ||left === 4) {/ 回调 - 要求下一个分片 /if (num + 1 <= this.files.total) {this.getFragmentation(fid,md5,token,num + 1,showPage,speaker).then(() => {/ 下一个分片要求成功后的处理 根据剩余个数, 决定连续渲染下一分片的1页还是2页 /this.getFragmentationSuccess(showPage,left ? 1 : 0);});}} else if (left === 1 ||left === 2) {/ 回调 - 要求上一个分片 /if (num - 1 >= 0) {this.getFragmentation(fid,md5,token,num - 1,showPage,speaker).then(() => {this.getFragmentationSuccess(showPage,left === 2 ? 1 : 0,'prev');});}}/ 渲染完成后返回. 由于我须要5页全部渲染完成后,初始化每一页上面的涂鸦功能, 以是我在末了才返回 Promise, 以担保时序的精确性. 若仅仅是展示, 没有其它功能的话, 无需返回 Promise. /resolve();}});}});}});});}return promise;}渲染页面

/ 渲染分页内容. @param pagination 第N页 @param page 分页属性 @param speaker 是否为主讲人 @param type 类型(高下页区分) /protected renderPage(pagination: number,page: any,speaker = false,type = 'next'): Promise<any> | void {const documents = this.getContainer() as HTMLDivElement;if (documents) {const item = this.createPage(pagination),pageView = page.view,scale = this.getScale(pageView, speaker, item),viewport = page.getViewport({scale});if (this.files.page !== pagination) item.style.display = 'none';/ 这个便是保存一些用得到的属性, 详细实当代码就不贴出来了. /this.setPaginationAttrs(pagination,viewport,pageView,scale);/ 创建元素 /const canvas = document.createElement('canvas'),context = canvas.getContext('2d');canvas.width = viewport.width;canvas.height = viewport.height;item.appendChild(canvas);/ 判断是要插入还是追加元素 /if (type === 'next') documents.appendChild(item);else if (documents.firstChild) documents.insertBefore(item, documents.firstChild);/ 渲染文档 /const renderContext = {canvasContext: context,viewport};return page.render(renderContext).promise;}}获取缩放比

/ 获取缩放比. @param origin 文档原始尺寸 @param speaker 是否为主讲人(主讲人默认以宽为基准) @param wrapper 画布容器(超出宽度的话, 须要手动设置高度) @param width 待变更元素的宽度 /protected getScale(origin: any,speaker: boolean,wrapper?: HTMLDivElement,width?: number): number {width = width ?? 0;const documents = this.getContainer() as HTMLDivElement;if (documents && !width) width = documents.offsetWidth;if (speaker) {/ 旁边两边增加了一些偏移量(主讲人与普通用户大小不一样, 这个函数的代码也没啥好贴) 主讲人默认因此屏幕宽度为基准进行文档缩放的. /const offsetWidth = this.getOffsetWidth(width);return Math.round(offsetWidth / origin[2] 100) / 100;} else {/ 非主讲人为了与主讲人显示内容同等, 默认采取高度为基准, 但有分外情形, 便是高度担保同等的情形下, 宽度却超出了屏幕的可视区域, 这时候就要将 文档显示区域所在的容器高度进一步缩短, 担保宽度是在可视区域内, 详细 实现就看 getHeightAndScale 这个方法了. /const heightAndScale = this.getHeightAndScale(documents, origin),scale = heightAndScale.scale;if (wrapper && heightAndScale.height) wrapper.style.height = `${heightAndScale.height}px`;return scale;}}剖断宽度是否超出可视区域

/ 获取文档显示高度与缩放比例. @param documents @param origin /protected getHeightAndScale(documents?: HTMLDivElement,origin?: any): {height: number;scale: number;} {documents = documents ?? this.getContainer() as HTMLDivElement;let wrapperHeight = 0, lastScale = 0;if (documents) {const size = this.files.speaker,// 所记录的主讲人的屏幕尺寸width = documents.offsetWidth,// 当前用户显示容器的可视宽度height = documents.offsetHeight;// 当前用户显示容器的可视高度let originWidth;/ 获取原始文档宽度 /if (!origin) {const pagination = this.getActivePagination();originWidth = pagination.originWidth;} else {originWidth = origin[2];}/ 打算主讲人的缩放比. 往下涌现的 200 / 150 之类的常数, 为设定好的显示偏移量. /const speakerRatio = Math.round((size.width - 200) / originWidth 100) / 100;/ 非主讲人默认以高度为基准来打算文档显示的缩放比例 /lastScale = Math.round(speakerRatio / (size.height - 150) (height - 100) 100) / 100;/ 如果以高度为基准的情形下, 剖断宽度是否超出可视区域 /const destWidth = Math.round(originWidth lastScale 100) / 100,offsetWidth = width - (this.isSpeaker() ? 200 : 120),diffWidth = destWidth - offsetWidth;if (diffWidth > 0) {/ 如果超出可视区域, 重新设定缩放比, 文档内容显示所在的DIV容器, 将进一步缩小, 以担保宽度在正常的可视区域内 /wrapperHeight = (Math.round((offsetWidth (size.height - 150)) / (size.width - 200) 100) / 100);lastScale = Math.round(offsetWidth / originWidth 100) / 100;}}return {height: wrapperHeight,scale: lastScale};}总结

翻页掌握的代码我就不贴出来了,与要求分片中的剖断类似。
总的实现,没有太大的难点,理清思路之后就很好实现了,上传速率快慢先不说,秒传校验通过的情形下,基本在 16ms 内完成 Content Download,直至页面渲染出来,全体过程大概 1s 旁边,有个条件是我的实现是等 5 个分页都渲染完成后又进行了一系列的涂鸦初始化操作后的韶光,不做其它处理,只做展示,速率将会更快。

标签:

相关文章