pdf.js实现web h5预览pdf文件(兼容低版本浏览器)

HHH 917 2024-08-20 12:03:01 阅读 57

注意

使用的是pdf.js 版本为 v2.16.105。因为新版本 兼容性不太好,部分手机预览不了,所以采用v2版本。

相关依赖

<code>"canvas": "^2.11.2",

"pdfjs-dist": "^2.16.105",

"core-js-pure": "^3.37.1",

"hammerjs": "^2.0.8", //这个是写手势 双指缩放的 不需要可以去掉

解决部分浏览器或者手机系统的兼容问题

//解决 structuredClone

// https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#browser_compatibility

// https://gitcode.com/zloirock/core-js/overview?utm_source=csdn_github_accelerator

import structuredClone from 'core-js-pure/actual/structured-clone';

// 解决 TypeError: key.split(...).at is not a function

// https://github.com/wojtekmaj/react-pdf/issues/1465

import 'core-js/features/array/at';

window.structuredClone = structuredClone;

代码

以下为在uniapp vue3 实现 h5 预览pdf文件的代码 有使用vant(手指缩放功能只写了一点,是不能用的)。

<template>

<div id="pdf-view" ref="pdfView">code>

<!-- <canvas v-for="page in state.pdfPages" :key="page" id="pdfCanvas" />-->code>

<div ref="pdfViewContainer">code>

<div

v-for="pageNumber in state.pdfPages"code>

v-show="state.pdfPageList.includes(pageNumber)"code>

:key="pageNumber"code>

:ref="(el) => (pageRefs[pageNumber - 1] = el)"code>

></div>

</div>

<je-loading v-show="loading" />code>

</div>

</template>

<script setup>

//解决 structuredClone

// https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#browser_compatibility

// https://gitcode.com/zloirock/core-js/overview?utm_source=csdn_github_accelerator

import structuredClone from 'core-js-pure/actual/structured-clone';

// 解决 TypeError: key.split(...).at is not a function

// https://github.com/wojtekmaj/react-pdf/issues/1465

import 'core-js/features/array/at';

import * as pdfjsWorker from 'pdfjs-dist/lib/pdf.worker.js';

// 解决 pdfjsWorker 未定义

window.pdfjsWorker = pdfjsWorker;

window.structuredClone = structuredClone;

// if (!Array.prototype.at) { -- -->

// Array.prototype.at = function (index) {

// if (index < 0) {

// index = this.length + index;

// }

// if (index >= 0 && index < this.length) {

// return this[index];

// }

// return undefined;

// };

// }

import Hammer from 'hammerjs';

import * as pdfjsWorker from 'pdfjs-dist/lib/pdf.worker.js';

// 解决 pdfjsWorker 未定义

window.pdfjsWorker = pdfjsWorker;

import 'pdfjs-dist/web/pdf_viewer.css';

import * as PDF from 'pdfjs-dist';

// import * as PDF from 'pdfjs-dist/build/pdf.js';

import { useRoute } from 'vue-router';

import { ref, reactive, onMounted, nextTick, defineProps } from 'vue';

import { showFailToast } from 'vant';

const route = useRoute();

const props = defineProps({

src: {

type: String,

default: '',

},

});

const pdfViewContainer = ref(null);

const pdfView = ref(null);

const pageRefs = ref([]);

const loading = ref(false);

const state = reactive({

// 总页数

pdfPages: 1,

pdfPageList: [], //有效页码列表

// 页面缩放

pdfScale: 1,

});

let pdfDoc = null;

async function loadFile(url) {

// {

// url,

// cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.16.105/cmaps/',

// cMapPacked: true,

// }

loading.value = true;

// 设置配置选项 手势缩放

PDF?.DefaultViewerConfig?.set({

handToolOnDblClick: true,

mouseWheelScale: true,

});

let arrayBufferPDF;

//

// if (navigator.userAgent.indexOf('QQ')) {

// const pdfData = await fetch(url);

// arrayBufferPDF = await pdfData.arrayBuffer();

// }

// 解决部分机型浏览器 undefined is not an object(evaluating 'response.body.getReader')

// https://www.qingcong.tech/technology/javascript/a-pdfjs-bug-in-qq.html#%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95

fetch(url).then(async (pdfData) => {

console.log('pdfData', pdfData);

if (!pdfData.ok) {

loading.value = false;

showFailToast({

message: '预览地址不存在或已失效',

duration: 0,

});

// window.JE.alert('预览地址不存在', 'error');

return;

}

arrayBufferPDF = await pdfData.arrayBuffer();

const loadingTask = arrayBufferPDF

? PDF.getDocument({ data: arrayBufferPDF })

: PDF.getDocument(url);

loadingTask.promise.then((pdf) => {

pdfDoc = pdf;

// 获取pdf文件总页数

state.pdfPages = pdf.numPages;

nextTick(() => {

for (let i = 0; i < state.pdfPages; i++) {

renderPage(i + 1); // 从第一页开始渲染

}

});

});

});

}

function initPinchZoom() {

const pdfViewEl = pdfView.value;

const hammer = new Hammer(pdfViewEl);

// 启用捏合缩放手势

hammer.get('pinch').set({ enable: true });

// 启用拖动手势,设置拖动方向为所有方向,阈值为0

hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL, threshold: 0 });

let initialScale = 1; // 初始缩放比例

let deltaX = 0; // 当前水平拖动距离

let deltaY = 0; // 当前垂直拖动距离

let startX = 0; // 拖动开始时的水平位置

let startY = 0; // 拖动开始时的垂直位置

const MIN_SCALE = 1; // 最小缩放比例

const MAX_SCALE = 4; // 最大缩放比例

let lastPinchTime = 0; // 上一次捏合事件的时间戳

let lastPanTime = 0; // 上一次拖动事件的时间戳

// 捏合开始事件处理函数

hammer.on('pinchstart', (event) => {

initialScale = state.pdfScale; // 记录初始缩放比例

startX = deltaX; // 记录拖动开始时的水平位置

startY = deltaY; // 记录拖动开始时的垂直位置

});

// 捏合移动事件处理函数

hammer.on('pinchmove', (event) => {

const currentTime = Date.now();

// 节流控制,限制事件触发频率

if (currentTime - lastPinchTime > 50) {

event.preventDefault();

const scale = event.scale; // 获取当前捏合的缩放比例

const newScale = Math.min(Math.max(initialScale * scale, MIN_SCALE), MAX_SCALE); // 计算新的缩放比例,限制在最小和最大缩放比例之间

state.pdfScale = newScale; // 更新缩放比例状态

applyTransform(); // 应用变换

lastPinchTime = currentTime; // 更新上一次捏合事件的时间戳

}

});

// 捏合结束事件处理函数

hammer.on('pinchend', (event) => {

initialScale = state.pdfScale; // 更新初始缩放比例为当前缩放比例

limitPanPosition(); // 限制拖动位置范围

renderPages(); // 重新渲染页面

});

// 拖动开始事件处理函数

hammer.on('panstart', (event) => {

pdfViewEl.style.transition = 'none'; // 禁用拖动过渡效果

startX = deltaX; // 记录拖动开始时的水平位置

startY = deltaY; // 记录拖动开始时的垂直位置

});

// 拖动移动事件处理函数

hammer.on('panmove', (event) => {

const currentTime = Date.now();

// 节流控制,限制事件触发频率

if (currentTime - lastPanTime > 50) {

const dx = event.deltaX; // 获取当前拖动的水平距离

const dy = event.deltaY; // 获取当前拖动的垂直距离

deltaX = startX + dx; // 计算新的水平拖动距离

deltaY = startY + dy; // 计算新的垂直拖动距离

applyTransform(); // 应用变换

lastPanTime = currentTime; // 更新上一次拖动事件的时间戳

}

});

// 拖动结束事件处理函数

hammer.on('panend', (event) => {

pdfViewEl.style.transition = 'transform 0.3s ease'; // 启用拖动过渡效果

limitPanPosition(); // 限制拖动位置范围

});

// 限制拖动位置范围的函数

function limitPanPosition() {

const pdfWidth = pdfViewEl.clientWidth * state.pdfScale; // 计算PDF页面的实际宽度

const containerWidth = pdfViewContainer.value.clientWidth; // 获取容器的宽度

const containerHeight = pdfViewContainer.value.clientHeight; // 获取容器的高度

// 计算单个页面的平均高度

const averagePageHeight =

pageRefs.value.reduce((totalHeight, pageRef) => {

return totalHeight + (pageRef ? pageRef.clientHeight : 0);

}, 0) / state.pdfPageList.length;

// 估算总高度,使用PDF文档的总页数乘以单个页面的平均高度

const estimatedTotalHeight = state.pdfPages * averagePageHeight * state.pdfScale;

// 限制水平拖动距离,确保PDF页面在容器内部

deltaX = Math.min(0, Math.max(deltaX, containerWidth - pdfWidth));

// 限制垂直拖动距离,确保PDF页面在容器内部,使用估算的总高度

deltaY = Math.min(0, Math.max(deltaY, containerHeight - estimatedTotalHeight));

applyTransform(); // 应用变换

}

// 应用变换的函数

function applyTransform() {

pdfViewEl.style.transform = `translate(${ deltaX}px, ${ deltaY}px) scale(${ state.pdfScale})`; // 设置PDF页面的变换样式

}

}

function renderPages() {

state.pdfPageList = [];

for (let i = 0; i < state.pdfPages; i++) {

renderPage(i + 1);

}

}

function renderPage(num) {

pdfDoc.getPage(num).then((page) => {

// 获取当前页面对应的DOM容器元素

const container = pageRefs.value[num - 1];

// 创建一个新的canvas元素

const canvas = document.createElement('canvas');

// 获取canvas的2D渲染上下文

const ctx = canvas.getContext('2d');

// 获取设备像素比

let devicePixelRatio = window.devicePixelRatio || 1;

// 获取画布的backing store ratio

let backingStoreRatio =

ctx.webkitBackingStorePixelRatio ||

ctx.mozBackingStorePixelRatio ||

ctx.msBackingStorePixelRatio ||

ctx.oBackingStorePixelRatio ||

ctx.backingStorePixelRatio ||

1;

// 获取pdfViewContainer元素的宽度

const pdfWrapperElWidth =

pdfViewContainer.value.clientWidth ||

pdfViewContainer.value.offsetWidth ||

pdfViewContainer.value.style.width;

// 获取PDF页面的初始视口,缩放比例为1

const intialisedViewport = page.getViewport({ scale: 1 });

// 计算缩放比例,使PDF页面宽度与容器宽度一致

const scale = pdfWrapperElWidth / intialisedViewport.width;

// 计算设备像素比与backing store ratio的比值

let ratio = devicePixelRatio / backingStoreRatio;

// 根据缩放比例获取PDF页面的视口

const viewport = page.getViewport({ scale });

// 设置canvas的宽度为容器宽度乘以ratio,确保高分辨率下的清晰度

canvas.width = pdfWrapperElWidth * ratio;

// 设置canvas的高度为视口高度乘以ratio,确保高分辨率下的清晰度

canvas.height = viewport.height * ratio;

// 设置canvas的样式宽度为100%,与容器宽度一致

canvas.style.width = '100%';

// 设置canvas的样式高度为auto,根据宽度自适应

canvas.style.height = 'auto';

// 缩放画布的渲染上下文,根据ratio进行缩放,确保在高分辨率下绘制的清晰度

ctx.scale(ratio, ratio);

const renderContext = {

canvasContext: ctx,

viewport,

};

// 设置页面容器的高度为视口高度

container.style.height = `${ viewport.height}px`;

page

.render(renderContext)

.promise.then(() => {

state.pdfPageList.push(num);

// 如果 container 存在 canvas元素 覆盖canvas元素

container?.firstChild && container.removeChild(container.firstChild);

container && container.appendChild(canvas);

})

.finally(() => {

if (num === state.pdfPages) {

loading.value = false;

}

});

});

}

onMounted(() => {

const file = route.query.file && JSON.parse(decodeURIComponent(route.query.file));

const { relName, previewUrl } = file || { };

if (relName) {

// 设置 uniapp 当前页面标题

uni.setNavigationBarTitle({

title: relName,

});

}

if (previewUrl) {

loadFile(previewUrl);

// nextTick(() => {

// initPinchZoom();

// });

} else {

showFailToast({

message: '预览地址不存在',

duration: 0,

});

}

});

</script>

<style scoped lang="less">code>

uni-page-body { -- -->

overflow-y: scroll;

}

</style>



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。