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

注意

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

相关依赖

javascript 复制代码
"canvas": "^2.11.2",
"pdfjs-dist": "^2.16.105",
"core-js-pure": "^3.37.1",
"hammerjs": "^2.0.8", //这个是写手势 双指缩放的 不需要可以去掉

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

javascript 复制代码
  //解决 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(手指缩放功能只写了一点,是不能用的)。

javascript 复制代码
<template>
  <div id="pdf-view" ref="pdfView">
    <!--    <canvas v-for="page in state.pdfPages" :key="page" id="pdfCanvas" />-->
    <div ref="pdfViewContainer">
      <div
        v-for="pageNumber in state.pdfPages"
        v-show="state.pdfPageList.includes(pageNumber)"
        :key="pageNumber"
        :ref="(el) => (pageRefs[pageNumber - 1] = el)"
      ></div>
    </div>
    <je-loading v-show="loading" />
  </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">
  uni-page-body {
    overflow-y: scroll;
  }
</style>
相关推荐
别拿曾经看以后~1 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死1 小时前
导航栏及下拉菜单的实现
前端·css·css3
川石课堂软件测试1 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
科技探秘人1 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人1 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR1 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香1 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q2498596931 小时前
前端预览word、excel、ppt
前端·word·excel
小华同学ai1 小时前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
problc1 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter