vue项目pdf文件预览组件(pdfjs-dist踩坑)

8,298 阅读1分钟

最近项目中需要使用pdf的预览功能,最初选用的vue-pdf,vue-pdf中有一些问题,改用vue-pdf-signature,该插件解决了vue-pdf的签章显示问题&反复预览pdf无法重载。但是最近有了新的问题,最新iphone14手机会出现大文件加载不全的问题,小文件正常显示(iphone14的适配真的头疼)。于是重新封装了pdf预览的组件。

之前封装过word,pdf和图片的预览组件,分别使用mammoth,vue-pdf-signature和vant自带的图片预览,先看下vue-pdf-signature实现的代码

<template>
	<div>
		<div class="viewBox">
			<div class="word-box" ref="docPreview" v-if="type === 'docx'"></div>
			<div class="txt-box" v-if="type === 'txt'" ref="txtPreview"></div>
			<!-- pdf组件 -->
			<div class="pdf-box" v-if="type === 'pdf'">
				<pdf
					:src="newSrc"
					v-for="i in numPages"
					:key="i"
					:page="i"
					style="display: inline-block; width: 100%"
				>
				</pdf>
			</div>
		</div>
	</div>
</template>

<script>
import pdf from 'vue-pdf-signature';
import CMapReaderFactory from 'vue-pdf-signature/src/CMapReaderFactory';
import mammoth from 'mammoth';
import { ImagePreview, Toast } from 'vant';
export default {
	name: 'ComPre',
	components: { pdf, CMapReaderFactory },
	data() {
		return {
			type: '',
			isShow: false,
			fileIdList: [],
			fileList: [],
			newSrc: '',
			sheetNames: [],
			wsObj: {},
			isAllowed: true,
			numPages: 1,
			pdfURL: null, //pdf预览url
		};
	},
	created() {
        //获取预览文件preData,type为文件类型,file为二进制流
		this.preData = this.$route.params.preData;
		this.type = this.preData.type;
		this.showFile(this.preData.type);
	},
	methods: {
		onLoading() {
			Toast.loading({
				duration: 500,
				forbidClick: true,
				message: '加载中...',
				className: 'loading',
			});
		},
		showFile(type) {
			const typeObj = {
				docx: 'word',
				pdf: 'pdf',
				txt: 'txt',
				jpg: 'img',
				jpeg: 'img',
				png: 'img',
				gif: 'img',
			};
			const fileType = typeObj[type] || '';
			const file = this.preData.file;
			if (fileType == 'word') {
				const val = new Blob([file], { type: 'application/msword' });
				mammoth
					.convertToHtml({ arrayBuffer: val })
					.then((result) => {
						this.$refs.docPreview.innerHTML = result.value || '';
					})
					.done();
			}
			if (fileType == 'txt') {
				const val = new Blob([file], { type: 'text/plain' });
				const reader = new FileReader();
				const that = this;
				reader.readAsText(val);
				reader.onload = function () {
					that.$refs.txtPreview.innerHTML = reader.result || '';
				};
			}
			if (fileType == 'pdf') {
				const val = new Blob([file], { type: 'application/pdf' });
				const url = window.URL.createObjectURL(val);
				this.pdfURL = url;
			}
			if (fileType == 'img') {
				const val = new Blob([file], { type: 'image/jpeg' });
				this.newSrc = window.URL.createObjectURL(val);
				const that = this;
				ImagePreview({
					// vant自带图片预览组件
					images: [that.newSrc],
					showIndex: false,
					onClose() {
						that.$router.go(-1);
					},
				});
			}
		},
		download() {
			// 下载文件
			const data = this.preData.file;
			const url = window.URL.createObjectURL(data);
			const link = document.createElement('a');
			link.style.display = 'none';
			link.href = url;
			link.setAttribute('download', this.preData.name);
			document.body.appendChild(link);
			link.click();
			document.body.removeChild(link);
		},
		blobToDataURI(blob, callback) {
			var reader = new FileReader();
			reader.readAsDataURL(blob);
			reader.onload = function (e) {
				callback(e.target.result);
			};
		},
	},
};
</script>

<style lang="scss" scoped>
.viewBox {
	margin-bottom: 1.34rem;
}
.txt-box {
	width: 100vw;
	height: 100vh;
}
.word-box {
	padding: 15px 15px 10px 15px;
	box-sizing: border-box;
	overflow: auto;
}
</style>

更改PDF预览组件

重新封装pdf相关预览部分,改用pdfjs-dist

安装pfdjs

npm install pdfjs@2.2.228 --save

最好安装2.2.228版本,稍后在解释这个版本问题

组件编写

这里可以参考pdfjs的示例 <pdfjs示例,下面是完整组件代码(Loading组件可自己封装,不用可去除)

<template>
  <div>
    <Loading v-model="loading">加载中...</Loading>
    <div ref="pdfViewerContainer" class="pdfViewerContainer"></div>
  </div>
</template>

<script>
import Loading from "@/components/common/Loading.vue";
import * as PDFJS from "pdfjs-dist";
import pdfjsWorker from "pdfjs-dist/build/pdf.worker.entry";

PDFJS.GlobalWorkerOptions.workerSrc = pdfjsWorker;
export default {
  name: "PdfViewer",
  components: { Loading },
  props: {
    /**
     * pdf地址
     */
    pdfUrl: {
      type: String,
      default: "",
    },
    /**
     * cMap的url地址(用于解决中文乱码或中文显示为空白的问题). 该资源的物理路径在: node_modules/pdfjs-dist/cmaps
     * 2.2.228版本可解决iphone14大文件显示不全问题
     */
    cMapUrl: {
      type: String,
      default: "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.2.228/cmaps/",
    },
    /**
     * pdf缩放比例
     */
    scale: {
      type: Number,
      default: 1,
    },
    /**
     * 携带的特定http请求头
     */
    httpHeaders: {
      type: Object,
      default: () => ({}),
    },
  },
  data() {
    return {
      pdfDocRef: null,
      loading: false,
    };
  },
  computed: {
    urlObj() {
      return { pdfUrl: this.pdfUrl, cMapUrl: this.cMapUrl };
    },
  },
  watch: {
    urlObj() {
      this.renderPdf();
    },
    scale() {
      this.renderPdf();
    },
  },
  mounted() {
    this.loading = true;
    // 创建画布
    this.renderPdf();
    setTimeout(() => {
      console.log(document.querySelector(".pdfViewerContainer"), "dom done");
      this.loading = false;//渲染pdf时会影响这里异步代码执行,在这设置loading结束
    }, 200);
  },
  methods: {
    /**
     * 渲染pdf文件的指定页到画板
     * @param pdfViewerDom 承载pdf画板的dom容器
     * @param pdfDoc pdf文件
     * @param pageNum 需要渲染的页码
     * @param scale 缩放比例
     */
    renderPdfOnePage(pdfViewerDom, pdfDoc, pageNum, scale) {
      // 创建画布
      const canvas = document.createElement("canvas");
      pdfViewerDom.appendChild(canvas);
      // 获取2d上下文
      const context = canvas.getContext("2d");
      pdfDoc.getPage(pageNum).then((page) => {
        let viewport = page.getViewport({ scale: scale });
        let newScale = 375 / viewport.width;//这里根据实际情况来,设计稿为宽375的iphone
        viewport = page.getViewport({ scale: newScale }); //按设备宽等比例缩放pdf文件
        const realCanvas = context.canvas;
        let outputScale = window.devicePixelRatio || 1;//按像素缩放调节清晰度
        realCanvas.width = Math.floor(viewport.width * outputScale);//将pdf缩放到对应屏幕大小
        realCanvas.height = Math.floor(viewport.height * outputScale);
        realCanvas.style.width = "100%"; //显示区域宽度撑满
        realCanvas.style.height = "100%";  //显示区域高度撑满
        let transform =
          outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null;
        page.render({ canvasContext: context, viewport, transform });
      });
    },
    /**
     * 渲染pdf的画布
     * @param pdfViewerDom 承载pdf画布的dom容器
     * @param pdfDoc pdf文档
     * @param scale 缩放比例
     */
    renderPdfCanvas(pdfViewerDom, pdfDoc, scale) {
      // 清除原来的pdf画布
      pdfViewerDom.innerHTML = "";
      // 获取总页数
      const totalPage = pdfDoc.numPages;
      // 获取显示容器
      for (let i = 1; i <= totalPage; i++) {
        // 循环处理pdf的每页
        this.renderPdfOnePage(pdfViewerDom, pdfDoc, i, scale);
      }
    },
    renderPdf() {
      const pdfViewerDom = this.$refs.pdfViewerContainer;
      if (this.pdfUrl) {
        // 获取pdf文件
        const pdfLoadingTask = PDFJS.getDocument({
          url: this.pdfUrl,
          withCredentials: true, // 携带凭证
          httpHeaders: this.httpHeaders,
          cMapUrl: this.cMapUrl,
          cMapPacked: true,
        });
        pdfLoadingTask.promise.then((pdfDoc) => {
          if (pdfDoc && pdfViewerDom) {
            // 缓存pdf内容
            this.pdfDocRef = pdfDoc;
            this.renderPdfCanvas(pdfViewerDom, pdfDoc, this.scale);
          }
        });
      }
    },
  },
};
</script>

<style scoped></style>

开始使用的时候没有问题,但是iphone14会出现大文件显示不全问题,后来发现是pdfjs新版本对iphone兼容性不太好,这里需要使用 pdfjs@2.2.228 版本,这个版本相对比较稳定,目前多个机型暂未发现问题。

总结

iphone14无法加载大文件需要更改pdfjs的cmap文件,这里采用的cdn引入的方式cdn.jsdelivr.net/npm/pdfjs-d… ,或者直接使用2.2.228版本,可以解决乱码及空白页问题。这里还有个难点是根据pdfjs的实例,发现pdf文件很模糊,需要更改canvas的缩放,这里用到了transform属性,在组件中已经设置为根据设备实际像素调整,可以自行摸索。