跳到主要内容前端 PDF 渲染方案:基于 pdf.js 的两种实现路径 | 极客日志TypeScript大前端
前端 PDF 渲染方案:基于 pdf.js 的两种实现路径
pdf.js 是前端渲染 PDF 的核心库,提供 viewer.html 预览与 canvas 自定义渲染两种方案。viewer.html 模式集成官方工具栏,适合快速落地;canvas 模式支持文本选择、搜索高亮及目录提取,灵活性更高。本文详解两种模式的部署步骤、关键代码实现及坐标系统处理技巧,帮助开发者在 React 项目中高效集成 PDF 功能。
RedisGeek0 浏览 前言
在前端开发中,PDF 文件的渲染一直是一项具有挑战性的任务。借助 pdf.js 库,我们可以轻松实现在浏览器中查看、搜索和操作 PDF 文档。本文将详细介绍两种主流的实现方式:基于官方 viewer.html 的集成预览,以及基于 Canvas 的自定义渲染。
简介
pdf.js 介绍
pdf.js 是一款基于 JavaScript 的开源 PDF 阅读器组件,支持在网页中直接显示和操作 PDF 文件。目前已知的前端渲染 PDF 组件大多基于此库进行封装。
GitHub 地址:https://github.com/mozilla/pdf.js
版本参数
| 插件 | 版本 |
|---|
| Node | v22.13.0 |
| @types/react | ^18.0.33 |
| @types/react-dom | ^18.0.11 |
| pdfjs-2.5.207-es5-dist.zip (viewer 模式) | 2.5.207 |
| pdfjs-dist (Canvas 模式) | 3.6.172 |
通过 viewer.html 实现预览(推荐)
这种方式除了 PDF 预览外,还配套了工具栏,支持搜索、缩放、目录、打印等功能。
部署
下载插件包
从 GitHub Release 页面下载对应版本的压缩包。
客户端方式
将解压后的 pdfjs-2.5.207-es5-dist 文件夹放在项目的 public 目录下。
服务端方式
将 pdf.js 包放在服务器目录下,构建获取 PDF 文件二进制流的接口(需处理同源策略)。
const pdfServerUrl = '/pdfjs-2.5.207-es5-dist/web/viewer.html';
const pdfInfoUrl = `${location.origin}/xxx/xx.pdf`;
const url = `${pdfServerUrl}?file=${encodeURIComponent(pdfInfoUrl)}`;
使用方法
预览 PDF 文件
1. 客户端方式(以 React 为例)
直接将 PDF 路径作为参数传递给 viewer.html。
;
: . = {
pdfUrl = ;
pdfServerUrl = ;
url = ;
(
);
};
;
import
React
from
'react'
const
ViewPDF
React
FC
() =>
const
'/A.pdf'
const
'/pdfjs-2.5.207-es5-dist/web/viewer.html'
const
`${pdfServerUrl}?file=${pdfUrl}`
return
<>
<h1>PDF 预览</h1>
<iframe src={url} style={{ width: '100%', height: '600px' }} />
</>
export
default
ViewPDF
2. 服务端方式
通过 axios 获取文件的 arraybuffer,转换为 blob URL 后传给 viewer.html。
import React, { useState } from 'react';
import axios from 'axios';
const ViewPDF: React.FC = () => {
const [pdfUrl, setPdfUrl] = useState<string>('');
const pdfServerUrl = '/pdfjs-2.5.207-es5-dist/web/viewer.html';
const getPDFViewUrl = (fileName: string) => {
axios({
method: 'get',
url: fileName,
responseType: 'arraybuffer',
}).then((response) => {
const blob = new Blob([response.data], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
setPdfUrl(`${pdfServerUrl}?file=${blobUrl}`);
});
};
return (
<>
<h1>PDF 预览</h1>
<iframe src={pdfUrl} />
</>
);
};
export default ViewPDF;
外部搜索条件触发 pdf.js 搜索逻辑
如果需要实现外部搜索框并高亮匹配内容,可以通过 postMessage 与 iframe 通信。
import React, { useEffect, useRef } from 'react';
const ViewPDFWithSearch: React.FC = () => {
const pdfUrl = '/A.pdf';
const pdfServerUrl = '/pdfjs-2.5.207-es5-dist/web/viewer.html';
const url = `${pdfServerUrl}?file=${pdfUrl}`;
let pdfContentWindow: any = null;
const getPdfContent = () => {
const pdfFrame = document.getElementById('pdfIframe');
if (!pdfFrame) return;
pdfContentWindow = pdfFrame.contentWindow;
};
const onSearchForOut = (searchText: string) => {
if (!pdfContentWindow) return;
pdfContentWindow.postMessage(searchText, '*');
const handler = (e: any) => {
if (pdfContentWindow.PDFViewerApplication?.findBar) {
pdfContentWindow.PDFViewerApplication.findBar.findField.value = e.data;
pdfContentWindow.PDFViewerApplication.findBar.highlightAll.checked = true;
pdfContentWindow.PDFViewerApplication.findBar.dispatchEvent('highlightallchange');
pdfContentWindow.PDFViewerApplication.findBar.dispatchEvent('again', false);
}
};
window.addEventListener('message', handler, false);
};
useEffect(() => {
getPdfContent();
setTimeout(() => {
onSearchForOut('阳区 CBD 核心区');
}, 3000);
return () => {
window.removeEventListener('message', () => {});
};
}, []);
return (
<div>
<h1>PDF 搜索</h1>
<iframe id="pdfIframe" src={url} style={{ width: '100%', height: '600px' }} />
</div>
);
};
export default ViewPDFWithSearch;
把 pdf 渲染为 canvas 实现预览
安装
npm install pdfjs-dist --save
功能实现
1. 基础预览
引入 pdfjs-dist,配置 worker 源,加载 PDF 并逐页渲染到 Canvas。
import React, { useState, useEffect, useRef } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
const pdfUrl = '/zyk.pdf';
const workerUrl = `/pdf.worker.min.js`;
pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;
const ViewPdf: React.FC<{ height: string }> = ({ height }) => {
const pdfContainerRef = useRef<HTMLDivElement>(null);
const [pagesList, setPagesList] = useState<any[]>([]);
const scale = 2;
const renderPage = async (page: any, pageNumber: number) => {
const viewport = page.getViewport({ scale });
const pageContentDom = document.createElement('div');
pageContentDom.id = `pdfPage-content-${pageNumber}`;
pageContentDom.style.width = `${viewport.width}px`;
pageContentDom.style.height = `${viewport.height}px`;
pageContentDom.style.position = 'relative';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.id = `pdfPage-${pageNumber}`;
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.border = '1px solid black';
pageContentDom.appendChild(canvas);
pdfContainerRef.current?.appendChild(pageContentDom);
await page.render({ canvasContext: context, viewport }).promise;
};
const loadPdf = async (url: string) => {
const pdf = await pdfjsLib.getDocument(url).promise;
const pages: any[] = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
pages.push({ page, textContent });
}
setPagesList(pages);
pages.forEach(({ page }: any, index: number) => renderPage(page, index));
};
useEffect(() => {
loadPdf(pdfUrl);
}, []);
return (
<div>
<h1>PDF 自定义渲染</h1>
<div style={{ height: height || '500px' }}>
<div ref={pdfContainerRef} style={{ position: 'relative', height: '100%', overflowY: 'scroll' }} />
</div>
</div>
);
};
export default ViewPdf;
2. 实现文本可选复制
通过 renderTextLayer API 创建文本层,覆盖在 Canvas 之上,即可实现文字选中复制。
const renderPage = async (page: any, pageNumber: number) => {
const viewport = page.getViewport({ scale });
const pageContentDom = document.createElement('div');
pageContentDom.className = 'pdfViewer';
pageContentDom.style.width = `${viewport.width}px`;
pageContentDom.style.height = `${viewport.height}px`;
pageContentDom.style.position = 'relative';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.id = `pdfPage-${pageNumber}`;
canvas.width = viewport.width;
canvas.height = viewport.height;
pageContentDom.appendChild(canvas);
await page.render({ canvasContext: context, viewport }).promise;
const textLayerDiv = document.createElement('div');
textLayerDiv.style.width = viewport.width;
textLayerDiv.style.height = viewport.height;
textLayerDiv.className = 'textLayer';
const textContent = await page.getTextContent();
pdfjsLib.renderTextLayer({
textContentSource: textContent,
container: textLayerDiv,
viewport: viewport,
textDivs: [],
});
pageContentDom.appendChild(textLayerDiv);
pdfContainerRef.current?.appendChild(pageContentDom);
};
3. 实现搜索、匹配内容高亮及跳转
这需要手动计算坐标并在 Canvas 上绘制高亮层。注意 pdf.js 的坐标系原点在左下角,而 DOM 通常在左上角,转换时需减去高度。
import React, { useState, useEffect, useRef } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
const ViewPdfWithHighlight: React.FC<{ height: string }> = ({ height }) => {
const [searchText, setSearchText] = useState('');
const pdfContainerRef = useRef<HTMLDivElement>(null);
const [pagesList, setPagesList] = useState<any[]>([]);
const scale = 2;
const createHighlightCanvas = (viewport: any, pageNumber: number, parentDom: any) => {
const highlightCanvas = document.createElement('canvas');
highlightCanvas.id = `highlightCanvas-${pageNumber}`;
highlightCanvas.className = 'highlightCanvas';
highlightCanvas.width = viewport.width;
highlightCanvas.height = viewport.height;
highlightCanvas.style.position = 'absolute';
highlightCanvas.style.top = '0';
highlightCanvas.style.left = '0';
highlightCanvas.style.zIndex = '1';
parentDom.appendChild(highlightCanvas);
};
const jumpToPage = (pageIndex: number) => {
let beforeCanvasHeight = 0;
for (let i = 0; i < pageIndex; i++) {
const dom = pdfContainerRef.current?.querySelector(`#pdfPage-content-${i}`);
if (dom) {
const h = dom.style.height.replace('px', '');
beforeCanvasHeight += Number(h);
}
}
pdfContainerRef.current?.scrollTo({ top: beforeCanvasHeight, behavior: 'smooth' });
};
const getCurrentTextContentY = (canvas: any, match: any) => {
const { transform, height } = match.textBlock;
return canvas.height - (transform[5] + height - 2) * scale;
};
const drawHighlights = async (canvas: any, matchesList: any[]) => {
if (matchesList.length === 0) return;
const context = canvas.getContext('2d');
context.fillStyle = 'rgba(255, 255, 0, 0.5)';
matchesList.forEach((match: any) => {
const { textBlock } = match;
const { transform, width, height, str } = textBlock;
const charWidth = width / str.length;
const lightWidth = (match.textEndIndex - match.textStartIndex) * charWidth;
const x = transform[4] + match.textStartIndex * charWidth;
const y = getCurrentTextContentY(canvas, match);
context.fillRect(
Math.floor(x * scale),
Math.floor(y),
Math.ceil(lightWidth * scale),
Math.ceil(height * scale)
);
});
};
const findAllOccurrences = (items: any[], searchStr: string): any[] => {
const results: any[] = [];
items.forEach((item, index) => {
if (item.str.includes(searchStr)) {
results.push({ blockIndex: index, textStartIndex: item.str.indexOf(searchStr), textEndIndex: item.str.indexOf(searchStr) + searchStr.length, textBlock: item });
}
});
return results;
};
const handleSearch = async () => {
if (!searchText) return;
const highlightCanvases = Array.from(pdfContainerRef.current?.querySelectorAll('.highlightCanvas') || []);
highlightCanvases.forEach((c: any) => c.getContext('2d').clearRect(0, 0, c.width, c.height));
const newMatches: any[] = [];
pagesList.forEach(({ textContent }: any, pageIndex: number) => {
const pageMatches = findAllOccurrences(textContent.items, searchText);
newMatches.push({ pageIndex, matchList: pageMatches });
});
if (newMatches.every(m => m.matchList.length === 0)) {
alert('未找到匹配项');
return;
}
newMatches.forEach(({ pageIndex, matchList }: any) => {
const highlightCanvas = pdfContainerRef.current?.querySelector(`#highlightCanvas-${pageIndex}`);
if (highlightCanvas && matchList.length > 0) {
drawHighlights(highlightCanvas, matchList);
}
});
const firstMatch = newMatches.find(m => m.matchList.length > 0);
if (firstMatch) {
jumpToPage(firstMatch.pageIndex);
}
};
return (
<div>
<h1>PDF 搜索与高亮</h1>
<input type="text" value={searchText} onChange={(e) => setSearchText(e.target.value)} placeholder="输入要搜索的内容" />
<button onClick={handleSearch}>搜索</button>
<div style={{ height: height || '500px' }}>
<div ref={pdfContainerRef} style={{ position: 'relative', height: '100%', overflowY: 'scroll' }} />
</div>
</div>
);
};
export default ViewPdfWithHighlight;
4. 获取 PDF 目录数据结构
使用 getOutline 方法可以提取 PDF 的书签或目录结构。
const getOutlineData = async (url: string) => {
const pdf = await pdfjsLib.getDocument(url).promise;
const outline = await pdf.getOutline();
console.log('目录数据:', outline);
};
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online