vue2 使用pdf.js 实现pdf预览,并可复制文本
'# vue2 使用pdf.js 实现pdf预览,并可复制文本
一、背景与问题
在Web应用中展示PDF文件时,开发者常常面临两个核心需求:1)实现PDF的可视化预览;2)支持文本的复制功能。传统做法是使用浏览器内置的PDF查看器,但其功能受限,无法自定义交互。而使用pdf.js(Mozilla开发的PDF.js库)可以实现更灵活的控制,但需要解决两个关键问题:
- 文本提取与复制:pdf.js本身不提供文本选择功能,需要手动实现文本层叠加
- 性能优化:处理大量PDF页时需要避免内存溢出和渲染卡顿
本文将深入探讨如何结合Vue2框架和pdf.js实现这两个功能,并分析实际应用中的最佳实践。
二、基本原理
pdf.js的工作原理可以分为三个核心阶段:
- PDF解析阶段:通过
getDocument方法加载PDF文件,解析其内容结构 - 页面渲染阶段:使用canvas逐页渲染PDF内容,支持缩放、翻页等操作
- 文本提取阶段:通过text layer技术提取文本内容,实现文本选择和复制
关键在于需要同时维护两个层:canvas渲染层和文本层。通过监听文本层的点击事件,可以实现文本复制功能。
三、环境准备
在开始开发前,需要准备以下依赖:
安装pdf.js:
npm install pdfjs-dist在Vue项目中引入:
import * as pdfjsLib from 'pdfjs-dist'; import { getWorkerPath } from 'pdfjs-dist/build/build'; pdfjsLib.GlobalWorkerOptions.workerSrc = getWorkerPath();基础依赖:
import { ref, onMounted, onBeforeUnmount } from 'vue';
四、核心实现
1. PDF加载与渲染
<template>
<div ref="pdfContainer" class="pdf-container">
<canvas ref="canvas" class="pdf-page"></canvas>
<div ref="textLayer" class="text-layer"></div>
</div>
</template>
<script>
import * as pdfjsLib from 'pdfjs-dist';
import { getWorkerPath } from 'pdfjs-dist/build/build';
pdfjsLib.GlobalWorkerOptions.workerSrc = getWorkerPath();
export default {
setup() {
const pdfContainer = ref(null);
const canvas = ref(null);
const textLayer = ref(null);
const pdf = ref(null);
const pageNum = ref(1);
const scale = ref(1.5);
const textContent = ref([]);
const renderPage = async (pageNumber = 1) => {
const page = await pdf.promise.then(p => p.getPage(pageNumber));
const viewport = page.getViewport({ scale: scale.value });
const canvasContext = canvas.value.getContext('2d');
canvasContext.clearRect(0, 0, canvas.value.width, canvas.value.height);
const renderContext = {
canvasContext,
viewport,
transform: [scale.value, 0, 0, scale.value, 0, 0]
};
await page.render(renderContext).promise;
// 渲染文本层
const textLayerContext = textLayer.value.getContext('2d');
textLayerContext.clearRect(0, 0, textLayer.value.width, textLayer.value.height);
await page.getTextContent().then(content => {
textContent.value = content.items;
// 这里需要计算文本位置并绘制
});
};
const init = async () => {
const file = await new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/pdf';
input.onchange = (e) => resolve(e.target.files[0]);
input.click();
});
const loadingTask = pdfjsLib.getDocument({
data: await file.arrayBuffer()
});
pdf.value = loadingTask;
await loadingTask.promise;
await renderPage();
};
return {
pdfContainer,
canvas,
textLayer,
init
};
}
};
</script>关键点解释:
- 使用
pdfjsLib.GlobalWorkerOptions.workerSrc设置Worker路径 - 使用
getDocument方法加载PDF文件 - 通过
getPage获取具体页面 - 使用
render方法将页面渲染到canvas - 通过
getTextContent获取文本内容
2. 文本提取与复制
const textLayer = ref(null);
const textContent = ref([]);
const renderTextLayer = async (page) => {
const textLayerContext = textLayer.value.getContext('2d');
textLayerContext.clearRect(0, 0, textLayer.value.width, textLayer.value.height);
const textContent = await page.getTextContent();
const textItems = textContent.items;
textContent.value = textItems;
// 绘制文本
textItems.forEach((item, index) => {
const text = item.str;
const { top, bottom, width } = item.transform;
textLayerContext.font = `${item.size}px serif`;
textLayerContext.fillText(text, 0, top);
// 添加点击事件
const rect = textLayerContext.measureText(text);
textLayer.value.addEventListener('click', (e) => {
if (e.clientX >= rect.left && e.clientX <= rect.right) {
navigator.clipboard.writeText(text);
}
});
});
};关键点:
- 通过
getTextContent获取文本内容 - 使用
measureText计算文本位置 - 为每个文本项添加点击事件,实现复制功能
- 使用
navigator.clipboard.writeText实现复制
3. 动态加载与分页
const currentPage = ref(1);
const totalPages = ref(0);
const goToPage = async (pageNumber) => {
if (pageNumber < 1 || pageNumber > totalPages.value) return;
currentPage.value = pageNumber;
await renderPage(pageNumber);
};
const updatePageCount = async () => {
totalPages.value = (await pdf.value.promise).numPages;
};关键点:
- 使用
numPages属性获取总页数 - 实现分页导航功能
- 通过
renderPage方法动态加载指定页
五、完整案例
创建一个完整的PDF预览组件:
<template>
<div class="pdf-preview">
<input type="file" @change="handleFileChange" accept="application/pdf">
<div ref="pdfContainer" class="pdf-container">
<canvas ref="canvas" class="pdf-page"></canvas>
<div ref="textLayer" class="text-layer"></div>
<div class="page-controls">
<button @click="prevPage">上一页</button>
<span>第 {{ currentPage }} 页 / {{ totalPages }}</span>
<button @click="nextPage">下一页</button>
</div>
</div>
</div>
</template>
<script>
import * as pdfjsLib from 'pdfjs-dist';
import { getWorkerPath } from 'pdfjs-dist/build/build';
pdfjsLib.GlobalWorkerOptions.workerSrc = getWorkerPath();
export default {
setup() {
const pdfContainer = ref(null);
const canvas = ref(null);
const textLayer = ref(null);
const pdf = ref(null);
const currentPage = ref(1);
const totalPages = ref(0);
const scale = ref(1.5);
const textContent = ref([]);
const handleFileChange = async (event) => {
const file = event.target.files[0];
if (!file) return;
const loadingTask = pdfjsLib.getDocument({
data: await file.arrayBuffer()
});
pdf.value = loadingTask;
await loadingTask.promise;
await updatePageCount();
await renderPage();
};
const updatePageCount = async () => {
if (!pdf.value) return;
totalPages.value = (await pdf.value.promise).numPages;
};
const renderPage = async (pageNumber = currentPage.value) => {
if (!pdf.value) return;
const page = await pdf.value.promise.then(p => p.getPage(pageNumber));
const viewport = page.getViewport({ scale: scale.value });
const canvasContext = canvas.value.getContext('2d');
canvasContext.clearRect(0, 0, canvas.value.width, canvas.value.height);
const renderContext = {
canvasContext,
viewport,
transform: [scale.value, 0, 0, scale.value, 0, 0]
};
await page.render(renderContext).promise;
await renderTextLayer(page);
};
const renderTextLayer = async (page) => {
if (!textLayer.value) return;
const textLayerContext = textLayer.value.getContext('2d');
textLayerContext.clearRect(0, 0, textLayer.value.width, textLayer.value.height);
const textContent = await page.getTextContent();
const textItems = textContent.items;
textContent.value = textItems;
textItems.forEach((item, index) => {
const text = item.str;
const { top, bottom, width } = item.transform;
textLayerContext.font = `${item.size}px serif`;
textLayerContext.fillText(text, 0, top);
const rect = textLayerContext.measureText(text);
textLayer.value.addEventListener('click', (e) => {
if (e.clientX >= rect.left && e.clientX <= rect.right) {
navigator.clipboard.writeText(text).catch(err => {
console.error('复制失败:', err);
});
}
});
});
};
const prevPage = () => {
currentPage.value = Math.max(1, currentPage.value - 1);
renderPage();
};
const nextPage = () => {
currentPage.value = Math.min(totalPages.value, currentPage.value + 1);
renderPage();
};
return {
pdfContainer,
canvas,
textLayer,
handleFileChange,
prevPage,
nextPage,
currentPage,
totalPages,
scale
};
}
};
</script>
<style scoped>
.pdf-preview {
padding: 20px;
font-family: sans-serif;
}
.pdf-container {
position: relative;
width: 800px;
height: 600px;
border: 1px solid #ccc;
overflow: hidden;
}
.pdf-page {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
}
.text-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: auto;
user-select: none;
font-family: sans-serif;
font-size: 14px;
}
.page-controls {
margin-top: 10px;
text-align: center;
}
</style>六、源码解析
PDF加载流程:
- 使用
getDocument加载PDF文件 - 通过
getPage获取具体页面 - 使用
render方法将页面绘制到canvas
- 使用
文本提取机制:
- 调用
getTextContent()获取文本内容 - 使用
measureText计算文本位置 - 为每个文本项添加点击事件
- 调用
复制功能实现:
- 使用
navigator.clipboard.writeText实现复制 - 通过计算文本区域位置判断点击位置
- 添加错误处理防止复制失败
- 使用
七、进阶使用
1. 文本高亮与搜索
const highlightText = (text, highlightClass) => {
const spans = textLayer.value.querySelectorAll('span');
spans.forEach(span => {
if (span.textContent.includes(highlightClass)) {
span.style.backgroundColor = 'yellow';
}
});
};2. 动态缩放支持
const setScale = (newScale) => {
scale.value = newScale;
renderPage();
};3. 文本选择与复制优化
const handleTextSelection = (e) => {
const text = e.target.textContent;
navigator.clipboard.writeText(text).catch(err => {
console.error('复制失败:', err);
});
};八、性能与工程实践
1. 性能优化策略
- 分页加载:只加载当前显示的页面
- 懒加载:按需加载PDF内容
- 缓存机制:对已渲染的页面进行缓存
- Web Worker:使用Worker处理PDF解析
2. 异常处理
try {
await page.render(renderContext).promise;
} catch (err) {
console.error('渲染失败:', err);
}3. 安全注意事项
- 文件验证:确保上传的PDF文件符合规范
- 沙箱环境:使用Web Worker隔离PDF解析过程
- 内容过滤:过滤潜在恶意内容
九、常见问题与踩坑
1. 文本复制失败
navigator.clipboard.writeText(text).catch(err => {
console.error('复制失败:', err);
});解决方案:添加错误处理,提示用户使用浏览器的复制功能
2. 文本位置计算错误
const rect = textLayerContext.measureText(text);解决方案:确保字体大小和字体类型一致
3. PDF文件无法加载
const loadingTask = pdfjsLib.getDocument({
data: await file.arrayBuffer()
});解决方案:检查文件是否完整,确保使用正确的Worker路径
十、最佳实践
- 使用Web Worker:分离PDF解析和UI渲染
- 添加加载状态:提升用户体验
- 实现分页导航:支持多页PDF文件
- 添加错误处理:增强健壮性
- 使用缓存机制:优化性能
十一、总结
在Vue2中使用pdf.js实现PDF预览并支持文本复制,需要深入理解PDF解析机制和文本提取技术。通过结合canvas渲染和文本层叠加,可以实现完整的PDF查看器功能。实际应用中需要注意性能优化、安全防护和用户体验,特别是在处理大型PDF文件时需要特别注意内存管理和渲染效率。建议在需要自定义PDF展示、支持文本交互的场景中使用此方案,而在对安全性和性能要求极高的场景中应考虑其他替代方案。
评论已关闭