pdf.js 前端pdf预览 渲染文本图层支持复制 保证手机端清晰度 双指缩放 alloyfinger(手势)
预览地址: https://feiyefeihua.gitee.io/
一、渲染pdf核心代码
首先安装 pdf.js 的 npm 版本:
npm i pdfjs-dist@2.6.347
使用:
let testObj = { name: '可选链', tips: '防止访问不存在的属性而报错' } console.log(testObj.names?.nickname) // undefined console.log(testObj.names.nickname) // Uncaught TypeError: Cannot read property 'nickname' of undefined
这会导致某些不支持的浏览器显示空白页面。我使用最新版的 pdfjs-dist , vue 打包的demo页面,在 QQ、UC 微信内置浏览器打开都是空白的。
所以使用了 2.6.347 版本,再新的都有用新语法。
2. pdf.worker.js 依赖问题。pdf.js 不是非要用 npm 形式使用, 直接在页面使用 script 标签引用也可以,并且可以只引用 pdf.js 即可(保证 pdf.worker.js 的名字,并且和 pdf.s 同路径)。
pdf.js 源码里有做判断,有 pdf.worker.js 的地址就是使用;没有,就判断全局是否有 pdfjsWorker 对象;再没有,就通过 引用 pdf.js 的地址拼接出来 pdf.worker.js 的地址。
使用:
const canvasBoxEl = document.getElementById('canvas-box') this.boxWidth = canvasBoxEl.width this.boxHeight = canvasBoxEl.height console.log(canvasBoxEl) var url = './static/react-native.pdf' PDFJS.getDocument(url) .promise.then(pdf => { // pdf.numPages 总页数 return pdf.getPage(1) }) .then(page => { // 设置展示比例 var scale = 1.5 // 获取pdf尺寸 var viewport = page.getViewport({ scale }) console.log(viewport) // 获取需要渲染的元素 var canvas = document.createElement('canvas') var context = canvas.getContext('2d') canvas.height = viewport.height canvas.width = viewport.width canvasBoxEl.appendChild(canvas) var renderContext = { canvasContext: context, viewport: viewport } page.render(renderContext) })
如果有多页,看需求渲染。在 PC 这样差不多就可以了,但是在手上打开,可能就很模糊。
PS:比较乱,基本功能都实现了,还需要优化。
二、保证pdf清晰度
pdf.js 是 mozilla 的开源库(地址:https://mozilla.github.io/pdf.js/),并且有一个完整的web展示页面,在手机上看也清晰。就从它的源码里面去看。
其中渲染pdf有三个地方,一个是渲染的缩略图,一个是打印的样式,剩下的那个才是正文:
主要是在渲染之前做了一些计算,canvas的 标签属性 width、height 和 css属性 width、height。还可以用svg渲染的样子?
计算使用的是一些提前确认的变量和写好的工具函数,提取出来:
const CSS_UNITS = 96.0 / 72.0; // const PRINT_UNITS = 150 / 72.0; let userAgent = (typeof navigator !== "undefined" && navigator.userAgent) || ""; let platform = (typeof navigator !== "undefined" && navigator.platform) || ""; let maxTouchPoints = (typeof navigator !== "undefined" && navigator.maxTouchPoints) || 1; let maxCanvasPixels = 16777216; let isAndroid = /Android/.test(userAgent); let isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || (platform === "MacIntel" && maxTouchPoints > 1); (function checkCanvasSizeLimitation() { if (isIOS || isAndroid) { maxCanvasPixels = 5242880; } })(); function approximateFraction(x) { if (Math.floor(x) === x) { return [x, 1]; } var xinv = 1 / x; var limit = 8; if (xinv > limit) { return [1, limit]; } else if (Math.floor(xinv) === xinv) { return [1, xinv]; } var x_ = x > 1 ? xinv : x; var a = 0, b = 1, c = 1, d = 1; while (q < limit) { var p = a + c, q = b + d; if (q > limit) { break; } if (x_ <= p / q) { c = p; d = q; } else { a = p; b = q; } } var result; if (x_ - a / b < c / d - x_) { result = x_ === x ? [a, b] : [b, a]; } else { result = x_ === x ? [c, d] : [d, c]; } return result; }, function roundToDivide(x, div) { var r = x % div; return r === 0 ? x : Math.round(x - r + div); }
其中还有个根据 canvas 的 ctx 计算 pixelRatio 的:
let devicePixelRatio = window.devicePixelRatio || 1 let backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1 const pixelRatio = devicePixelRatio / backingStoreRatio
修改过后的完整代码:
<template> <div class="pdf-touch-box"> <div class="scale-btn-box" :style="{ width: btnWidth + 'px' }"> <button class="scale-btn" @click="scaleCanvas(1.5)">1.5</button> <button class="scale-btn" @click="scaleCanvas(0.5)">0.5</button> <button class="scale-btn" @click="scaleCanvas(1)">还原(1)</button> </div> <div v-show="!loading" class="pdf-canvas-wrap" :style="{ width: viewWidth + 'px', height: viewHeight + 'px' }"></div> <p class="pdf-canvas-tips" v-show="loading">正在加载...</p> </div> </template> <script> import * as PDFJS from 'pdfjs-dist' // 本地 // window.pdfjsWorker = require("pdfjs-dist/build/pdf.worker.js"); // cdn 2.8.335 2.6.347 2.5.207 PDFJS.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.6.347/build/pdf.worker.min.js' // https://github.com/mozilla/pdf.js/blob/master/examples/node/getinfo.js // Requires single file built version of PDF.js -- please run // `gulp singlefile` before running the example. // const pdfjsLib = require("pdfjs-dist/legacy/build/pdf.js"); const CSS_UNITS = 96.0 / 72.0 // const PRINT_UNITS = 150 / 72.0; let userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '' let platform = (typeof navigator !== 'undefined' && navigator.platform) || '' let maxTouchPoints = (typeof navigator !== 'undefined' && navigator.maxTouchPoints) || 1 let maxCanvasPixels = 16777216 // PDF之外占据的宽度 -18 padding -18减去滚动条宽度(不确定) let autoWidth = 36 let isAndroid = /Android/.test(userAgent) let isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || (platform === 'MacIntel' && maxTouchPoints > 1) ;(function checkCanvasSizeLimitation() { if (isIOS || isAndroid) { maxCanvasPixels = 5242880 autoWidth -= 18 } })() export default { data() { return { src: './static/react-native.pdf', loading: true, pdfDoc: null, boxEl: null, wrapEl: null, areaWidth: 0, btnWidth: 0, viewWidth: 0, viewHeight: 0, pixelRatio: 2, isFirstTimeRender: true, viewport: null, canvasEles: [], canvasCtxs: [], totallPage: 1, pageScale: 1, // pdf 适应窗口产生的 scale curCanvasCSSWh: null, transform: null, pageRendered: false } }, mounted() { this.init() }, methods: { async init() { this.boxEl = document.querySelector('.pdf-touch-box') this.wrapEl = document.getElementsByClassName('pdf-canvas-wrap')[0] this.btnWidth = this.areaWidth = this.boxEl.clientWidth const loadingState = await this.getPDF() if (loadingState === 'success') this.initRenderOneByOne() else this.boxEl.innerText = loadingState }, scaleCanvas(scale) { if (!this.pageRendered) return // 改变 viewport 大小 this.viewport = this.viewport.clone({ scale: this.pageScale * scale * CSS_UNITS }) const { styleWidth, styleHeight } = this.getCanvasCSSWH() // 先改变CSS canvas 会变模糊 this.canvasEles.forEach((canvas, index) => { // 不修改 width height 不然会重置 canvas canvas.style.width = styleWidth + 'px' canvas.style.height = styleHeight + 'px' console.log(index) }) // 重新渲染 变清晰 this.scaleRenderAll() }, // 好像改变也不是很明显 // scaleCanvas(scale) { // // 改变 viewport 大小 // this.viewport = this.viewport.clone({ // scale: this.pageScale * scale * CSS_UNITS, // }); // // 逐个重新渲染 // this.renderSinglePage(this.canvasEles[0], 1); // }, getPDF() { let that = this return new Promise(reslove => { PDFJS.getDocument(that.src).promise.then( function (pdfDoc_) { that.pdfDoc = pdfDoc_ that.totallPage = pdfDoc_.numPages that.loading = false reslove('success') }, function (reason) { console.log(reason.message) that.loading = false reslove(reason.name) } ) }) }, initRenderOneByOne() { for (let pageNum = 1; pageNum <= this.totallPage; pageNum++) { let canvas = document.createElement('canvas') canvas.setAttribute('id', `pdf-canvas${pageNum}`) canvas.setAttribute('class', `pdfcanvas`) // alpha 设定 canvas 背景总是不透明,可以加快浏览器绘制透明的内容和图片 初始化出来 canvas 为黑色背景 // 实际上 导致 重新渲染的时候 闪黑屏 // let ctx = canvas.getContext("2d", { // alpha: false, // }); let ctx = canvas.getContext('2d') this.canvasCtxs.push(ctx) this.canvasEles.push(canvas) this.wrapEl.appendChild(canvas) } this.renderSinglePage(this.canvasEles[0], 1) }, renderSinglePage(canvas, pageNum) { let ctx = this.canvasCtxs[pageNum - 1] let that = this this.pdfDoc.getPage(pageNum).then(function (page) { if (that.isFirstTimeRender) that.initView(page, ctx) if (pageNum === 1) that.getCanvasCSSWH() canvas.width = that.curCanvasCSSWh.width canvas.height = that.curCanvasCSSWh.height canvas.style.width = that.curCanvasCSSWh.styleWidth + 'px' canvas.style.height = that.curCanvasCSSWh.styleHeight + 'px' canvas.style['border'] = '#d6d6d6 solid 1px' canvas.style.margin = '9px 0 0 0' let renderContext = { canvasContext: ctx, transform: that.transform, viewport: that.viewport, enableWebGL: false, renderInteractiveForms: false } let renderTask = page.render(renderContext) renderTask.promise.then(function () { if (that.totallPage >= ++pageNum) { that.renderSinglePage(that.canvasEles[pageNum - 1], pageNum) } else { that.pageRendered = true } }) }) }, scaleRenderAll() { const len = this.canvasEles.length for (let pageNum = 0; pageNum < len; pageNum++) { let canvas = this.canvasEles[pageNum] let ctx = this.canvasCtxs[pageNum] let that = this this.pdfDoc.getPage(pageNum + 1).then(function (page) { canvas.width = that.curCanvasCSSWh.width canvas.height = that.curCanvasCSSWh.height let renderContext = { canvasContext: ctx, transform: that.transform, viewport: that.viewport, enableWebGL: false, renderInteractiveForms: false } let renderTask = page.render(renderContext) renderTask.promise.then(function (context) { console.log(context) }) }) } }, getCanvasCSSWH() { let outputScale = { sx: this.pixelRatio, sy: this.pixelRatio, scaled: this.pixelRatio !== 1 } let pixelsInViewport = this.viewport.width * this.viewport.height let maxScale = Math.sqrt(maxCanvasPixels / pixelsInViewport) if (outputScale.sx > maxScale || outputScale.sy > maxScale) { outputScale.sx = maxScale outputScale.sy = maxScale outputScale.scaled = true } let sfx = (0, this.approximateFraction)(outputScale.sx) let sfy = (0, this.approximateFraction)(outputScale.sy) const width = (0, this.roundToDivide)(this.viewport.width * outputScale.sx, sfx[0]) const height = (0, this.roundToDivide)(this.viewport.height * outputScale.sy, sfy[0]) const styleWidth = (0, this.roundToDivide)(this.viewport.width, sfx[1]) const styleHeight = (0, this.roundToDivide)(this.viewport.height, sfy[1]) if (this.pixelRatio !== 1) this.transform = [this.pixelRatio, 0, 0, this.pixelRatio, 0, 0] this.viewWidth = styleWidth + 2 // 12 加上 canvas border margin 误差?2 + 9 + 1 this.viewHeight = this.totallPage * (this.viewport.height + 12) + 9 this.curCanvasCSSWh = { width, height, styleWidth, styleHeight } return this.curCanvasCSSWh }, approximateFraction(x) { if (Math.floor(x) === x) { return [x, 1] } var xinv = 1 / x var limit = 8 if (xinv > limit) { return [1, limit] } else if (Math.floor(xinv) === xinv) { return [1, xinv] } var x_ = x > 1 ? xinv : x var a = 0, b = 1, c = 1, d = 1 while (q < limit) { var p = a + c, q = b + d if (q > limit) { break } if (x_ <= p / q) { c = p d = q } else { a = p b = q } } var result if (x_ - a / b < c / d - x_) { result = x_ === x ? [a, b] : [b, a] } else { result = x_ === x ? [c, d] : [d, c] } return result }, roundToDivide(x, div) { var r = x % div return r === 0 ? x : Math.round(x - r + div) }, initView(page, ctx) { let devicePixelRatio = window.devicePixelRatio || 1 let backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1 this.pixelRatio = devicePixelRatio / backingStoreRatio this.viewport = page.getViewport({ scale: CSS_UNITS }) console.log(this.viewport) this.pageScale = (this.areaWidth - autoWidth) / this.viewport.width let curViewport = page.getViewport({ scale: this.pageScale * CSS_UNITS }) this.viewport = curViewport this.isFirstTimeRender = false }, drawBorder(canvas, ctx) { ctx.save() ctx.fillStyle = 'rgb(255, 255, 255)' ctx.strokeRect(0, 0, canvas.width, canvas.height) ctx.restore() } } } </script> <style scoped> .pdf-touch-box { padding: 9px; width: calc(100% - 18px); height: calc(100% - 18px); display: flex; flex-direction: column; justify-content: center; } .scale-btn-box { position: fixed; top: 0; left: 0; height: 44px; display: flex; justify-content: space-around; } .scale-btn { width: 25%; height: 100%; display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; background: #fff; border: 1px solid #dcdfe6; color: #606266; -webkit-appearance: none; text-align: center; box-sizing: border-box; outline: none; margin: 0; transition: 0.1s; font-weight: 500; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; padding: 12px 20px; font-size: 14px; border-radius: 4px; } .pdf-canvas-wrap { display: flex; flex-direction: column; align-items: center; overflow: hidden; margin-top: 44px; padding-top: 9px; } .pdf-canvas-tips { margin-top: 44px; } </style>
这样手机打开就也清晰了。
三、渲染文本图层,支持手势缩放
要渲染文本图层还需要额外的依赖:
let renderContext = { canvasContext: ctx, transform: that.transform, viewport: that.viewport, enableWebGL: false, renderInteractiveForms: false, }; let renderTask = page.render(renderContext); renderTask.promise .then(function () { if (that.totallPage >= ++pageNum) { that.renderSinglePage(that.canvasEles[pageNum - 1], pageNum); return page.getTextContent(); } else { that.pageRendered = true; } }) .then((textContent) => { const textLayerDiv = document.createElement("div"); textLayerDiv.setAttribute("class", "textLayer"); textLayerDiv.setAttribute("style", "top: 12px"); canvas.parentElement.appendChild(textLayerDiv); var textLayer = new TextLayerBuilder({ eventBus: new EventBus(), textLayerDiv: textLayerDiv, pageIndex: pageNum, viewport: that.viewport, }); textLayer.setTextContent(textContent); textLayer.render(); });
支持手势缩放使用 alloyfinger.js :
import AlloyFinger from "alloyfinger"; //包装一下 不然 eslint 报警告 class FingerTouch { constructor(element, options) { Object.assign(this, AlloyFinger.prototype); AlloyFinger.call(this, element, options); } }
使用:
this.alloyFinger = new FingerTouch(this.wrapEl, {}) this.alloyFinger.on('pinch', e => { let zoom = e.zoom let curScale = this.lastStyleScale * zoom if (curScale <= this.pageScale / 2 || curScale >= 5) return this.scaleEvent(curScale) }) this.alloyFinger.on('pressMove', e => { this.viewTop += e.deltaY this.viewLeft += e.deltaX })
这里使用了 pressMove 事件,因为 canvas 使用了 absolute 绝对定位,支持 在容器里移动。如果不需要,就让它自适应(通过滚动条移动),就不用 pressMove 事件。
完整代码:
<template> <div class="pdf-touch-box"> <div class="scale-btn-box" :style="{ width: btnWidth + 'px' }"> <button class="scale-btn" @click="scaleEvent(3.5)">3.5</button> <button class="scale-btn" @click="scaleEvent(2.5)">2.5</button> <button class="scale-btn" @click="scaleEvent(1.8)">1.8</button> <button class="scale-btn" @click="scaleEvent(1.3)">1.3</button> <button class="scale-btn" @click="scaleEvent(1)">1</button> <button class="scale-btn" @click="scaleEvent(0.5)">0.5</button> </div> <div v-show="!loading" class="pdf-canvas-wrap" :style="{ top: viewTop + 'px', left: viewLeft + 'px', width: viewWidth + 'px', height: viewHeight + 'px' }" ></div> <p class="pdf-canvas-tips" v-show="loading">正在加载...</p> </div> </template> <script> import * as PDFJS from 'pdfjs-dist' console.log(PDFJS) import { TextLayerBuilder, EventBus } from 'pdfjs-dist/web/pdf_viewer' import 'pdfjs-dist/web/pdf_viewer.css' console.log(TextLayerBuilder) // 本地 // window.pdfjsWorker = require("pdfjs-dist/build/pdf.worker.js"); // cdn 2.8.335 2.6.347 2.5.207 PDFJS.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.6.347/build/pdf.worker.min.js' // https://github.com/mozilla/pdf.js/blob/master/examples/node/getinfo.js // Requires single file built version of PDF.js -- please run // `gulp singlefile` before running the example. // const pdfjsLib = require("pdfjs-dist/legacy/build/pdf.js"); const CSS_UNITS = 96.0 / 72.0 // const PRINT_UNITS = 150 / 72.0; let userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '' let platform = (typeof navigator !== 'undefined' && navigator.platform) || '' let maxTouchPoints = (typeof navigator !== 'undefined' && navigator.maxTouchPoints) || 1 let maxCanvasPixels = 16777216 // PDF之外占据的宽度 -18 padding -18减去滚动条宽度(不确定) let autoWidth = 36 let textLayerTop = 3 let scaleInterval = 0.05 let isAndroid = /Android/.test(userAgent) let isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || (platform === 'MacIntel' && maxTouchPoints > 1) ;(function checkCanvasSizeLimitation() { if (isIOS || isAndroid) { maxCanvasPixels = 5242880 autoWidth -= 18 textLayerTop -= 1 // 手机上面缩放对清晰度影响更小 scaleInterval = 0.4 } })() import AlloyFinger from 'alloyfinger' //包装一下 不然 eslint 报警告 class FingerTouch { constructor(element, options) { Object.assign(this, AlloyFinger.prototype) AlloyFinger.call(this, element, options) } } export default { data() { return { src: './static/react-native.pdf', loading: true, pdfDoc: null, boxEl: null, wrapEl: null, areaWidth: 0, btnWidth: 0, viewWidth: 0, viewHeight: 0, pixelRatio: 2, isFirstTimeRender: true, viewport: null, canvasEles: [], canvasCtxs: [], totallPage: 0, pageScale: 1, // pdf 适应窗口产生的 scale curCanvasCSSWh: null, transform: null, pageRenderedNum: 0, scaleTimer: null, lastStyleScale: 1, lastRerenderScale: 1, alloyFinger: null, viewTop: 0, viewLeft: 9, textEls: [] } }, mounted() { this.init() }, methods: { async init() { //禁止下拉刷新 document.addEventListener( 'touchmove', function (ev) { ev.preventDefault() }, { passive: false } ) this.boxEl = document.querySelector('.pdf-touch-box') this.wrapEl = document.getElementsByClassName('pdf-canvas-wrap')[0] this.btnWidth = this.areaWidth = this.boxEl.clientWidth const loadingState = await this.getPDF() if (loadingState === 'success') { this.initRenderOneByOne() this.initTouch() } else this.boxEl.innerText = loadingState }, initTouch() { this.alloyFinger = new FingerTouch(this.wrapEl, {}) this.alloyFinger.on('pinch', e => { let zoom = e.zoom let curScale = this.lastStyleScale * zoom if (curScale <= this.pageScale / 2 || curScale >= 5) return this.scaleEvent(curScale) }) this.alloyFinger.on('pressMove', e => { this.viewTop += e.deltaY this.viewLeft += e.deltaX }) }, scaleEvent(scale) { // 渲染中 不让缩放 也不让重绘 if (this.pageRenderedNum != this.totallPage || this.totallPage === 0) return // 没在渲染中 随意缩放 this.scaleCanvas(scale) // 说明是第一次事件 或重绘完成 开始计时 if (this.scaleTimer === null) { this.scaleTimer = this.renderDelayer(666) } //时间间隔内再次触发缩放 重新计时 clearTimeout(this.scaleTimer) this.scaleTimer = this.renderDelayer(666) }, renderDelayer(interval) { return setTimeout(() => { this.scaleRenderAll() this.scaleTimer = null }, interval) }, scaleTopLeft(width, height) { if (Math.abs(this.viewTop) > height / 2) this.viewTop *= 1 / 2 if (Math.abs(this.viewLeft) > width / 2) this.viewLeft *= 1 / 2 }, scaleCanvas(scale) { this.lastStyleScale = scale console.log(scale) // 改变 viewport 大小 this.viewport = this.viewport.clone({ scale: (this.pageScale * CSS_UNITS * scale).toFixed(3) }) const { styleWidth, styleHeight } = this.getCanvasCSSWH() // 计算一下 top left 不然可能会显示到 窗口外面 看不到了 this.scaleTopLeft(styleWidth, styleHeight) // 改变CSS canvas 会变模糊 this.canvasEles.forEach(canvas => { // 不修改 width height 不然会重置 canvas 变空白 canvas.style.width = styleWidth + 'px' canvas.style.height = styleHeight + 'px' }) }, // 使用新渲染的 canvas 替换 缩放过后不清晰的 canvas scaleRenderAll() { let curInterval = Math.abs(this.lastStyleScale - this.lastRerenderScale) let curScaleInterval = scaleInterval let isNarrow = this.lastStyleScale < this.lastRerenderScale // 如果是变小 变化不大时 清晰度影响更小 if (isNarrow) curScaleInterval = scaleInterval * 2 console.log('scaleRenderAll', curScaleInterval, curInterval) // 变化很小的时候就不计时重新渲染了 清晰度影响不大 1.1 - 1 = 0.10000000000000009 if (curInterval <= curScaleInterval) return this.lastRerenderScale = this.lastStyleScale this.pageRenderedNum = 0 const len = this.canvasEles.length for (let pageNum = 1; pageNum <= len; pageNum++) { let newCanvas = document.createElement('canvas') let newCtx = newCanvas.getContext('2d', { alpha: false }) newCanvas.setAttribute('id', `pdf-canvas${pageNum}`) this.canvasCtxs[pageNum - 1] = newCtx let that = this this.pdfDoc.getPage(pageNum).then(function (page) { that.setCanvasCSSWh.call(that, newCanvas) let renderTask = that.pageRender.call(that, page, newCtx) renderTask.promise .then(function () { let oldCanvas = that.canvasEles[pageNum - 1] oldCanvas.parentElement.replaceChild(newCanvas, oldCanvas) that.canvasEles[pageNum - 1] = newCanvas that.pageRenderedNum++ return page.getTextContent() }) .then(textContent => that.textRerender.call(that, pageNum, textContent)) .catch(e => console.log(e)) }) } }, getPDF() { let that = this return new Promise(reslove => { PDFJS.getDocument(that.src).promise.then( function (pdfDoc_) { that.pdfDoc = pdfDoc_ that.totallPage = 1 // that.totallPage = pdfDoc_.numPages; that.loading = false reslove('success') }, function (reason) { console.log(reason.message) that.loading = false reslove(reason.name) } ) }) }, initRenderOneByOne() { for (let pageNum = 1; pageNum <= this.totallPage; pageNum++) { let canvas = document.createElement('canvas') canvas.setAttribute('id', `pdf-canvas${pageNum}`) canvas.setAttribute('class', `pdfcanvas`) // alpha 设定 canvas 背景总是不透明,可以加快浏览器绘制透明的内容和图片 初始化出来 canvas 为黑色背景 // 实际上 导致 重新渲染的时候 闪黑屏 // let ctx = canvas.getContext("2d", { // alpha: false, // }); let ctx = canvas.getContext('2d') this.canvasCtxs.push(ctx) this.canvasEles.push(canvas) // this.wrapEl.appendChild(canvas); let pageDiv = document.createElement('div') pageDiv.setAttribute('id', 'page-' + pageNum) pageDiv.setAttribute('style', 'position: relative;') this.wrapEl.appendChild(pageDiv) pageDiv.appendChild(canvas) } this.renderSinglePage(this.canvasEles[0], 1) }, renderSinglePage(canvas, pageNum) { let ctx = this.canvasCtxs[pageNum - 1] let that = this this.pdfDoc.getPage(pageNum).then(function (page) { if (that.isFirstTimeRender) that.initView(page, ctx) if (pageNum === 1) that.getCanvasCSSWH() that.setCanvasCSSWh.call(that, canvas) let renderTask = that.pageRender.call(that, page, ctx) renderTask.promise .then(function () { if (that.totallPage > pageNum) { that.renderSinglePage(that.canvasEles[pageNum], pageNum + 1) } that.pageRenderedNum++ return page.getTextContent() }) .then(textContent => that.textRender.call(that, canvas, pageNum, textContent)) }) }, textRerender(pageIndex, textContent) { const oldDiv = this.textEls[pageIndex - 1] const newDiv = document.createElement('div') newDiv.setAttribute('class', 'textLayer') newDiv.setAttribute('style', `top: ${textLayerTop}px`) oldDiv.parentElement.replaceChild(newDiv, oldDiv) this.textEls[pageIndex - 1] = newDiv this.renderTextLayer(newDiv, pageIndex, textContent) }, textRender(canvas, pageIndex, textContent) { const textLayerDiv = document.createElement('div') textLayerDiv.setAttribute('class', 'textLayer') textLayerDiv.setAttribute('style', `top: ${textLayerTop}px`) canvas.parentElement.appendChild(textLayerDiv) this.textEls[pageIndex - 1] = textLayerDiv this.renderTextLayer(textLayerDiv, pageIndex, textContent) }, renderTextLayer(el, index, content) { var textLayer = new TextLayerBuilder({ eventBus: new EventBus(), textLayerDiv: el, pageIndex: index, viewport: this.viewport }) textLayer.setTextContent(content) textLayer.render() }, setCanvasCSSWh(canvas) { canvas.width = this.curCanvasCSSWh.width canvas.height = this.curCanvasCSSWh.height canvas.style.width = this.curCanvasCSSWh.styleWidth + 'px' canvas.style.height = this.curCanvasCSSWh.styleHeight + 'px' canvas.style['border'] = '#d6d6d6 solid 1px' canvas.style.margin = '0 0 9px 0' }, pageRender(page, ctx) { return page.render({ canvasContext: ctx, transform: this.transform, viewport: this.viewport, enableWebGL: false, renderInteractiveForms: false }) }, drawBorder(canvas, ctx) { ctx.save() ctx.fillStyle = 'rgb(255, 255, 255)' ctx.strokeRect(0, 0, canvas.width, canvas.height) ctx.restore() }, getCanvasCSSWH() { let outputScale = { sx: this.pixelRatio, sy: this.pixelRatio, scaled: this.pixelRatio !== 1 } let pixelsInViewport = this.viewport.width * this.viewport.height let maxScale = Math.sqrt(maxCanvasPixels / pixelsInViewport) if (outputScale.sx > maxScale || outputScale.sy > maxScale) { // 这里触发会出错 // outputScale.sx = maxScale; // outputScale.sy = maxScale; // outputScale.scaled = true; } let sfx = (0, this.approximateFraction)(outputScale.sx) let sfy = (0, this.approximateFraction)(outputScale.sy) const width = (0, this.roundToDivide)(this.viewport.width * outputScale.sx, sfx[0]) const height = (0, this.roundToDivide)(this.viewport.height * outputScale.sy, sfy[0]) const styleWidth = (0, this.roundToDivide)(this.viewport.width, sfx[1]) const styleHeight = (0, this.roundToDivide)(this.viewport.height, sfy[1]) if (this.pixelRatio !== 1) this.transform = [this.pixelRatio, 0, 0, this.pixelRatio, 0, 0] this.viewWidth = styleWidth + 2 // 12 加上 canvas border margin 误差?2 + 9 + 1 this.viewHeight = this.totallPage * (this.viewport.height + 12) + 9 this.curCanvasCSSWh = { width, height, styleWidth, styleHeight } return this.curCanvasCSSWh }, approximateFraction(x) { if (Math.floor(x) === x) { return [x, 1] } var xinv = 1 / x var limit = 8 if (xinv > limit) { return [1, limit] } else if (Math.floor(xinv) === xinv) { return [1, xinv] } var x_ = x > 1 ? xinv : x var a = 0, b = 1, c = 1, d = 1 // eslint-disable-next-line while (true) { var p = a + c, q = b + d if (q > limit) { break } if (x_ <= p / q) { c = p d = q } else { a = p b = q } } var result if (x_ - a / b < c / d - x_) { result = x_ === x ? [a, b] : [b, a] } else { result = x_ === x ? [c, d] : [d, c] } return result }, roundToDivide(x, div) { var r = x % div return r === 0 ? x : Math.round(x - r + div) }, initView(page, ctx) { let devicePixelRatio = window.devicePixelRatio || 1 let backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1 this.pixelRatio = devicePixelRatio / backingStoreRatio this.viewport = page.getViewport({ scale: CSS_UNITS }) this.pageScale = (this.areaWidth - autoWidth) / this.viewport.width let curViewport = page.getViewport({ scale: this.pageScale * CSS_UNITS }) this.viewport = curViewport this.isFirstTimeRender = false } } } </script> <style scoped> .pdf-touch-box { padding: 9px; width: calc(100% - 18px); height: calc(100% - 18px); position: relative; } .scale-btn-box { position: fixed; top: 0; left: 0; height: 44px; display: flex; justify-content: space-around; z-index: 99; } .scale-btn-box::after { content: ''; width: 100%; height: 100%; position: absolute; background: #fff; top: 0; left: 0; filter: blur(18px); opacity: 0.8; } .scale-btn { position: relative; z-index: 2; width: 25%; height: 100%; display: inline-block; line-height: 1; white-space: nowrap; cursor: pointer; background: #fff; border: 1px solid #dcdfe6; color: #606266; -webkit-appearance: none; text-align: center; box-sizing: border-box; outline: none; margin: 0; transition: 0.1s; font-weight: 500; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none; padding: 12px 20px; font-size: 14px; border-radius: 4px; } .pdf-canvas-wrap { display: flex; flex-direction: column; align-items: center; overflow: hidden; margin-top: 44px; padding-top: 9px; position: absolute; } .pdf-canvas-tips { margin-top: 44px; } </style>