
摘要本文基于 pdfjs-dist2.16.105 实现了一个功能完整的 PDF 预览组件涵盖分页、按钮/快捷键缩放、复制文字、缩略图跳转定位、360°旋转、一键放大/还原、水印文字及自适应预览区域等能力。文章先展示各功能的效果图再给出组件 props 配置项并详细说明水印处理、分页信息计算、缩略图生成、PDF 加载以及通过 TextLayerBuilder 实现文字复制等核心代码实现最后列出标注、关键词高亮、PDF 编辑、下载、搜索、OCR 识别等后续待完善功能。一、背景近期接了个需求需要实现分页、缩放、复制文字、一键放大/还原支持自适应预览区域后面琢磨了许久考虑了基于 pdfjs-dist 实现分页、按钮/快捷键缩放、复制文字、缩略图跳转定位、360°旋转、一键放大/还原、水印文字等功能该项目使用 pdfjs-dist2.16.105 版本的插件如果更新版本的逻辑可共用无非 API 的变化。实现这些功能一千多行代码还有很大优化空间……注意兼容性问题该功能组件支持 Chrome88如果您不考虑兼容性问题可以使用最新版本的插件二、直接上效果图1、初始化加载时2、加载完之后3、复制文字4、旋转5、占满全屏三、代码组件支持props配置项defaultPropsData { pdfSrc: { type: [String, Blob, Uint8Array], required: true, description: PDF源可以是URL、Blob或Uint8Array }, containerHeight: { type: String, default: 80vh, description: 容器高度 }, initialScale: { type: Number, default: 1.3, description: 初始缩放比例 }, current: { type: Number, default: 1, description: 当前页码 }, minScale: { type: Number, default: 0.5, description: 最小缩放比例 }, maxScale: { type: Number, default: 10.0, description: 最大缩放比例 }, pagesPerLoad: { type: Number, default: -1, description: 每次加载的页数 }, showPagination: { type: Boolean, default: true, description: 是否显示分页控件 }, showToolbar: { type: Boolean, default: true, description: 是否显示工具栏 }, allowCopy: { type: Boolean, default: true, description: 是否允许复制文本 }, isShowFooter: { type: Boolean, default: true, description: 是否展示底部区域 }, isScrollZoomWheel: { type: Boolean, default: true, description: 是否滚轮放大 }, isWatermark: { type: Boolean, default: true, description: 水印 }, watermarkTxt: { type: String, default: 佛系xxx版权所有, description: 水印 } }处理水印// 水印处理 function renderWatermark(canvas, context, viewport,watermarkText) { const pattern context.createPattern(_createWatermarkCanvas(watermarkText), repeat); context.save(); context.fillStyle pattern; context.fillRect(0, 0, viewport.width, viewport.height); context.restore(); } function _createWatermarkCanvas(text) { const canvas document.createElement(canvas); canvas.width 200; canvas.height 200; const ctx canvas.getContext(2d); ctx.rotate((-18 * Math.PI) / 180); ctx.font 20px Vedana; ctx.fillStyle rgba(0, 0, 0, 0.2); ctx.textAlign center; ctx.textBaseline middle; ctx.fillText(text, 100, 100); return canvas; }处理分页信息/** * 计算分页信息 * param {Object} options 分页参数 * param {number} options.totalPages 总条数 * param {number} options.pagesPerLoad 每页加载条数 * param {number} options.currentPage 当前页码 * returns {Array} 分页计算结果 */ function calculatePagination(options {}) { let currentPage options?.currentPage || 1 let pagesPerLoad options?.pagesPerLoad || 1 let total options.totalPages || 0 // 基本参数校验与修正 const validTotal Math.max(0, Math.floor(total)); const validPagesPerLoad Math.max(1, Math.floor(pagesPerLoad)); const totalPages Math.ceil(validTotal / currentPage) || 1; const validCurrentPage Math.min(Math.max(1, Math.floor(currentPage)), totalPages); // 计算偏移量 const offset (validCurrentPage - 1) * validPagesPerLoad; const pages Array.from({ length: total }, (_, index) index 1).sort((a, b) a - b); let newPageArray JSON.parse(JSON.stringify(pages)) let _paramsData paginate(newPageArray, validCurrentPage, validPagesPerLoad) return { previousPageData: _paramsData.previousPageData, nextPageData: _paramsData.nextPageData, currentPageData: newPageArray.slice(offset, (validCurrentPage - 0) * validPagesPerLoad) } }生成指定页面的缩略图/** * 创建缩略图 */ function createThumbnailHtmlImage(thumbnailUrl, pageNumber) { const imgElement document.createElement(img); imgElement.classList.add(thumbnail); imgElement.src thumbnailUrl; imgElement.dataset.page pageNumber; imgElement.style.width 100%; // imgElement.style.height px; return imgElement; } /** 获取缩略图容器 */ function getThumbnailHtmlDiv(pageNumber) { let thumbnailDiv document.getElementById(ThumbnailHtml-item${pageNumber}) if (!thumbnailDiv) { thumbnailDiv document.createElement(div); thumbnailDiv.className ThumbnailHtml-item__image thumbnailDiv.dataset.page pageNumber; document.querySelector(.ThumbnailHtml-item).appendChild(thumbnailDiv); } return thumbnailDiv; } /** 生成指定页面的缩略图 param currentPageNumber param scale */ async function generateThumbnail(currentPageNumber, scale) { if (thumbnailData.thumbnailCache.has(currentPageNumber)) { return false; // 如果已经缓存了缩略图则直接返回 } const page await window.pdfDocument.getPage(currentPageNumber); const viewport page?.getViewport({ scale }); const canvas thumbnailData.thumbnailCanvas || document.createElement(canvas); thumbnailData.thumbnailCanvas canvas; canvas.width viewport.width; canvas.height viewport.height; const context canvas.getContext(2d); const renderContext { canvasContext: context, viewport }; await page?.render(renderContext).promise; const thumbnailBlob await new Promise(resolve canvas.toBlob(resolve, image/jpeg)); const thumbnailUrl URL.createObjectURL(thumbnailBlob); thumbnailData.thumbnailCache.set(currentPageNumber, thumbnailUrl); const thumbnailDiv getThumbnailHtmlDiv(currentPageNumber); const imgElement createThumbnailHtmlImage(thumbnailUrl, currentPageNumber); thumbnailDiv.appendChild(imgElement); // 释放 Blob URL 以避免内存泄漏 imgElement.onload () { URL.revokeObjectURL(thumbnailUrl); }; }pdf区域加载pdf方法通过TextLayerBuilder实现文字复制功能缩略图点击处理逻辑三、待完善功能及后续会新增功能1、标注2、pdf关键词高亮3、pdf可编辑4、下载5、内存监控6、加载预览pdf性能优化7、搜索8、pdf加载失败自动重载9、内置纯前端OCR识别10、签名及拖拽印章到指定位置11、新增单页pdf画布拖拽12、新增添加笔记/备忘录13、新增复制历史记录操作及内容并支持生成TXT文件下载以上这些功能会慢慢去实现拭目以待奥利给路还有很长需要一步一步走如果您有什么好的建议或者技术可以留言探讨该文有不足地方请大佬们赐教欢迎指正共同进步~~~~~