前言
- 需求:在项目中显示pdf文件并根据page和position显示高亮,首次进入页面时默认滚动到第一高亮页。样式使用pdf默认样式👉。
- 前提:react16+webpack
- 实现方式:pdfjs-dist 或 react-pdf
*版本:*pdfjs-dist@2.5.207
使用原因:@2.5.207 版本为.js文件,未使用mjs,不需要配置webpack,可兼容chrome66及以上版本。
安装pdfjs-dist依赖,通过引用方式使用,需要引入worker加载解析pdf文件 👉pdfjsLib.GlobalWorkerOptions.workerSrc =''; 部署可能会出现worker跨域的问题,需要将worker放在与pdfjs同目录下防止跨域,所以最终选择了直接放文件放在public下通过嵌入iframe显示。
最终实现方式:
- 将pdfjs-dist@2.5.207 文件放在public文件下,通过iframe引入pdfjs文件下的viewer.html实现默认浏览样式;
- 通过获取textLayer元素,并根据页数和点位向父级dom添加高亮标签元素实现高亮;
- 获取第一个高亮数据并获取对应元素,通过scrollIntoView定位滚动到当前页。
实现代码
## 全部代码
import { useEffect, useRef } from 'react';
/**
* @param {string} pdfUrl pdf文件路径
* @param {Array} highlightData 高亮数据【page/position[x,y,width,hight]】
* @returns
*/
const PDFViewer = ({ pdfUrl = '', highlightData = [] }) => {
// refs
const iframeRef = useRef(null);
// 获取源路径 - 根据自己项目进行优化
const getTargetOrigin = (type = 0) => {
let locationOrigin = location.origin;
let pdfdist = `/pdfjs-2.5.207-dist/web/viewer.html?file=`;
let path = `/packages${pdfdist}`;
let localPath = pdfdist;
if (locationOrigin.includes('localhost')) {
return type ? '*' : localPath;
}
if (locationOrigin.includes('xxx.20.xx.xx')) {
return type ? locationOrigin + ':xxxx' : locationOrigin + ':xxxx' + path;
}
if (locationOrigin.includes('xx.xxx.15.xxx')) {
return type ? locationOrigin + '/xxxx' : locationOrigin + '/xxxx' + path;
}
if (
locationOrigin.includes('xx.xxx') ||
locationOrigin.includes('xx.xxxx.xx')
) {
return type ? locationOrigin + '/xxxx' : locationOrigin + '/xxxx' + path;
}
};
// 初始默认滚动到第一个高亮页
const getFirstHighlightPage = () => {
let key = 'page';
let sortHighlightData = highlightData?.sort((a, b) => a[key] - b[key]);
let firstPage = sortHighlightData?.[0]?.[key]; // 获取第一个高亮页
return firstPage;
};
/**
* 发送高亮指令
* @param {number|string} page
* @param {boolean} firstPage
*/
const highlightArea = (page, firstPage = true) => {
let iframeWindow = iframeRef.current.contentWindow;
let lightData = [];
let key = 'page';
// 筛选出符合当前页的高亮位置
highlightData.filter(item => {
if (item[key] == page) {
lightData.push(item);
}
return item[key] == page;
});
try {
let targetOrigin = getTargetOrigin(1);
// 发送消息到 iframe 进行高亮
if (lightData) {
iframeWindow.postMessage(
{
action: 'highlightArea',
highlightData: lightData,
firstPage,
},
targetOrigin,
);
}
} catch (error) {
console.error('Error sending message:', error);
}
};
// 初始默认滚动到第一个高亮位置
const initScrollToFirstHighlight = (iframeDocument, isFirstPage) => {
if (isFirstPage) {
let firstPage = getFirstHighlightPage();
const targetPage = iframeDocument.querySelector(
`.page:nth-child(${firstPage})`,
);
if (targetPage) {
targetPage.scrollIntoView({ behavior: 'smooth' });
}
}
};
// 高两块显示部分
const highlightAreaFn = event => {
const { highlightData, firstPage } = event.data;
// 无高亮内容直接返回
let hlLength = highlightData.length;
if (hlLength === 0) {
return;
}
let iframeDocument = iframeRef.current.contentDocument;
if (iframeDocument) {
initScrollToFirstHighlight(iframeDocument, firstPage);
let key = 'page';
let pageNum = highlightData?.[0]?.[key];
let pageDomToNumberDom = iframeDocument.querySelector(
`.page:nth-child(${pageNum})`,
);
let textLayerDom = pageDomToNumberDom?.querySelector('.textLayer');
if (textLayerDom) {
const tagSpan = textLayerDom.querySelector('#myLightSpan');
if (!tagSpan) {
for (let index = 0; index < highlightData.length; index++) {
const element = highlightData[index];
let key = 'textBbox';
let areaPosition = element?.[key];
let position =
typeof areaPosition === 'string'
? JSON.parse(areaPosition)
: areaPosition;
const [x, y, height, width] = position;
// 创建一个新的 span 来覆盖高亮区域
const highlight = document.createElement('span');
highlight.setAttribute('id', 'myLightSpan');
highlight.style.position = 'absolute';
highlight.style.left = `${x * 100}%`;
highlight.style.top = `${y * 100}%`;
highlight.style.width = `${width * 100}%`;
highlight.style.height = `${height * 100}%`;
highlight.style.backgroundColor = 'rgb(255, 255, 0)'; // 高亮颜色
highlight.style.zIndex = -1; // 设置层级低于文字,鼠标选中文字可选
// 将高亮区域添加到文本层
textLayerDom?.appendChild(highlight);
}
}
}
}
};
// 监听pdf页面宽高变化
const resizeObserver = new ResizeObserver(entries => {
let iframeDocument = iframeRef.current.contentDocument;
let nearestPage = [];
for (let entry of entries) {
const { width, height } = entry.contentRect;
const pageNum = entry.target.getAttribute('data-page-number');
let pageDomToNumberDom = iframeDocument.querySelector(
`.page:nth-child(${pageNum})`,
);
let textLayerDom = pageDomToNumberDom.querySelector('.textLayer');
if (textLayerDom) {
nearestPage?.push(pageNum);
}
}
nearestPage?.forEach(pageNum => {
highlightArea(+pageNum, false);
});
});
// 监听页面滚动变化
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
const pageNum = entry.target.getAttribute('data-page-number');
if (entry.isIntersecting) {
// 页面进入视口时,向iframe发送高亮信息
highlightArea(pageNum, false);
}
});
},
{
threshold: 0.2, // 页面超过50%进入视口时触发
},
);
//
useEffect(() => {
let iframeWindow = iframeRef.current.contentWindow;
// 添加事件监听器
iframeWindow.onload = function() {
iframeWindow.addEventListener('message', highlightAreaFn);
};
iframeWindow.onload();
setTimeout(() => {
let iframeContent = iframeRef.current.contentDocument;
let viewDom = iframeContent.querySelector('#viewer');
const pages = iframeContent.querySelectorAll('.page');
pages.forEach(page => {
observer.observe(page);
// 开始监听目标元素的尺寸变化
resizeObserver.observe(page);
});
// 初始化时,默认滚动到第一高亮页
let firstPage = getFirstHighlightPage();
highlightArea(firstPage, true);
}, 1000);
return () => {
pages.forEach(page => {
observer.unobserve(page);
resizeObserver.unobserve(page);
});
};
}, []);
return (
<iframe
ref={iframeRef}
src={`${getTargetOrigin()}${encodeURIComponent(pdfUrl)}`}
width="100%"
height="100%"
style={{ border: 'none' }}
></iframe>
);
};
export default PDFViewer;
## 通过iframe容器显示pdf文件
通过iframe容器使用pdfjs-web中的viewer.html显示pdf文件 src={`/pdfjs-3.11.174-dist/web/viewer.html?file=${pdfUrl}} 注意: 因为pdfjs-dist文件放在public下,打包部署线上后的pdfjs位置可能会发生变化,找到dist包确认viewer.html位置。必须与当前同源 即ip+viewer.html路径+ ?file= + pdf文件路径
<iframe
ref={iframeRef}
src={`${getTargetOrigin()}${encodeURIComponent(pdfUrl)}`}
width="100%"
height="100%"
style={{ border: 'none' }}
></iframe>
## 在父页面和 iframe 中使用 postMessage 来发送和接收数据
1.同源 iframe同源策略要求父页面和
iframe页面来自相同的域、协议和端口。如果父页面和iframe页面同源,你可以直接通过 JavaScript 获取iframe的内容。2.如果父页面和
iframe页面来自不同的源(即跨域),你不能直接访问iframe中的内容,因为这会违反同源策略。跨域访问受到浏览器的严格限制。
- 同源
iframe:可以直接通过iframe.contentDocument或iframe.contentWindow.document访问iframe的内容。- 跨域
iframe:由于同源策略限制,无法直接访问iframe内容。可以使用postMessageAPI 进行跨域通信,或通过服务器设置 CORS 来访问资源。已经配置同源可以直接通过document.querySelector('iframe')获取dom,我直接使用了postMessage方式。
useEffect(() => {
let iframeWindow = iframeRef.current.contentWindow;
// 添加事件监听器 - 接收信息
iframeWindow.onload = function() {
iframeWindow.addEventListener('message', highlightAreaFn);
};
iframeWindow.onload();
},[])
highlightAreaFn为监听到postMessage发送的数据后执行,向textLayer Dom添加高亮span; highlightArea发送高亮指令,参数为高亮内容和是否为首个高亮;初始挂载时发送一次进行渲染首高亮页并滚动到当前页;因为pdfjs性能优化,仅加载附近5页,在监听视口当前页面时也需要发送一次视口页高亮;由于2.5.207 版本每次缩放会重新渲染pdf页,导致初始渲染的高亮也被销毁,所以需要在监听pdf页宽放发生变化时,发送一次高亮渲染。
// 发送高亮指令
const highlightArea = (page, firstPage = true) => {
let iframeWindow = iframeRef.current.contentWindow;
let lightData = [];
let key = 'page';
// 获取同page的高亮
highlightData.filter(item => {
if (item[key] == page) {
lightData.push(item);
}
return item[key] == page;
});
try {
let targetOrigin = getTargetOrigin(1);
// 发送消息到 iframe 进行高亮
if (lightData) {
iframeWindow.postMessage(
{
action: 'highlightArea',
highlightData: lightData,
firstPage,
},
targetOrigin,
);
}
} catch (error) {
console.error('Error sending message:', error);
}
};
## highlightAreaFn 接收高亮数据进行显示
const highlightAreaFn = event => {
const { highlightData, firstPage } = event.data;
let hlLength = highlightData.length;
if (hlLength === 0) {
return;
}
let iframeDocument = iframeRef.current.contentDocument;
if (iframeDocument) {
// 首次自动滚动到第一个高亮页
initScrollToFirstHighlight(iframeDocument, firstPage);
let key = 'page';
let pageNum = highlightData?.[0]?.[key];
console.log('pageNum', pageNum);
let pageDomToNumberDom = iframeDocument.querySelector(
`.page:nth-child(${pageNum})`,
);
let textLayerDom = pageDomToNumberDom?.querySelector('.textLayer');
initTextLayerObserver(textLayerDom);
if (textLayerDom) {
const tagSpan = textLayerDom.querySelector('#myLightSpan');
if (!tagSpan) {
for (let index = 0; index < highlightData.length; index++) {
const element = highlightData[index];
let key = 'textBbox';
let areaPosition = element?.[key];
let position =
typeof areaPosition === 'string'
? JSON.parse(areaPosition)
: areaPosition;
const [x, y, height, width] = position;
// 创建一个新的 span 来覆盖高亮区域
const highlight = document.createElement('span');
highlight.setAttribute('id', 'myLightSpan');
highlight.style.position = 'absolute';
highlight.style.left = `${x * 100}%`;
highlight.style.top = `${y * 100}%`;
highlight.style.width = `${width * 100}%`;
highlight.style.height = `${height * 100}%`;
highlight.style.backgroundColor = 'rgb(255, 255, 0)'; // 高亮颜色
highlight.style.zIndex = -1;
console.log('highlight-高两块', highlight, 'position', position);
// 将高亮区域添加到文本层
textLayerDom?.appendChild(highlight);
}
}
}
}
};
## 页面滚动&缩放时渲染高亮
pdfjs的性能优化不能渲染所有页,仅渲染视口上下一共五页,随意需要判断当前视口所在pdf文件页来添加高亮。 pdf页面缩放时,2.5.207 版本会销毁当前页面,重新根据缩放渲染页面,导致放在dom中的高亮也被销毁,故需要重新渲染。
// 监听页面滚动变化
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
const pageNum = entry.target.getAttribute('data-page-number');
if (entry.isIntersecting) {
// 页面进入视口时,向iframe发送高亮信息
highlightArea(pageNum, false);
}
});
},
{
threshold: 0.2, // 页面超过50%进入视口时触发
},
);
// 监听pdf页面宽高变化
const resizeObserver = new ResizeObserver(entries => {
let iframeDocument = iframeRef.current.contentDocument;
let nearestPage = [];
for (let entry of entries) {
const { width, height } = entry.contentRect;
const pageNum = entry.target.getAttribute('data-page-number');
let pageDomToNumberDom = iframeDocument.querySelector(
`.page:nth-child(${pageNum})`,
);
let textLayerDom = pageDomToNumberDom?.querySelector('.textLayer');
// 由于pdfjs仅渲染最近几页,可以根据有无page的dom来判断当前视口页数进行渲染高亮。
if (textLayerDom) {
nearestPage?.push(pageNum);
}
}
nearestPage?.forEach(pageNum => {
highlightArea(+pageNum, false);
});
});
useEffect(() => {
let iframeContent = iframeRef.current.contentDocument;
let viewDom = iframeContent.querySelector('#viewer');
const pages = iframeContent.querySelectorAll('.page');
// 向每一页添加监听
pages.forEach(page => {
observer.observe(page);
// 开始监听目标元素的尺寸变化
resizeObserver.observe(page);
});
return{
// 销毁
pages.forEach(page => {
observer.unobserve(page);
resizeObserver.unobserve(page);
});
}
},[])
最终效果
遇到问题
- viewer跨域和iframe源跨域
解决方式:
- 删除viewer.js中的跨域判断代码
- iframe的src&postMessage的第二参数&当前地址端口号同源
- pdfjs-dist 3.11.174版本不兼容chrome91版本,无法识别es6代码
解决方式:
- 1.降低版本到2.5.207
- 2.将pdfjs-dist放在static,配置webpack打包编译为es5(实验未通过)
- 初始打开pdf页面,默认滚动到第一个高亮页,缩略图出现时会使滚动位置不准确
原因:缩略图的滚动和高亮自动滚动都是使用scrollIntoView,当同页面元素使用多个scrollIntoView可能会导致滚动位置不准确。
解决方式:
- 1.localStorage.removeItem('pdfjs.history');移除pdfjs的本地存储,默认每次都不打开缩略图
- 2.将scrollIntoView更换成scrollTo
引入pdfjs方式(不完全仅参考)
单页&缩放&未用viewer
import * as pdfjsLib from 'pdfjs-dist/webpack';
import { useEffect, useRef, useState } from 'react';
import styles from '../index.module.less';
// 通过 CDN 引入 worker
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.worker.min.js';
interface CanvasItem {
height: number;
width: number;
}
let newHighlights = [
{
page: 2,
textContent: 'PDFDEMO',
textType: 'line',
textBbox: [90, 753.891, 50, 12],
// [left,bottom,width,height]
},
{
page: 2,
textContent: '倒数第三个是如何发达发达地方和',
textType: 'line',
textBbox: [90, 566.691, 228, 12],
},
{
page: 4,
textContent: '突然也就特价公开',
textType: 'line',
textBbox: [218.759, 98.69, 108, 12],
},
];
const PDFDemo = props => {
const { pdfUrl = '', highlightData = [], type } = props;
const pdfFile = pdfUrl; // 你的PDF文件地址
// const pdfFile = require('../pdfFile.pdf').default; // 你的PDF文件地址
// refs
const containerRef = useRef<any>(null);
const pdfCanvasRef = useRef<any>(null);
const highlightCanvasRef = useRef<any>(null);
const pageRef = useRef(1);
// 基础状态
let pdfDoc: any = null;
const [currentPdfDoc, setCurrentPdfDoc] = useState<any>();
const [cpdfCtx, setCpdfCtx] = useState<any>(null);
const [highlightPdfCtx, setHighlightPdfCtx] = useState<any>(null);
let [currentPage, setCurrentPage] = useState<number>(1);
const [totalPages, setTotalPages] = useState(0);
const [highlights, setHighlights] = useState<any>([...newHighlights]);
let scale = 1.5;
// canvas contexts
let pdfCtx = null;
let highlightCtx: any = null;
// 获取第一个高亮page
const getFirstHighlightPage = () => {
let key = type === 'liu' ? 'page_id' : 'page';
let firstPage = highlightData?.sort((a, b) => a[key] - b[key])?.[0]
?.page_id;
setCurrentPage(firstPage || 1);
return firstPage;
};
useEffect(() => {
pageRef.current = currentPage; // 每次 count 更新时更新 pageRef
}, [currentPage]);
// 组件挂载
useEffect(() => {
pdfCtx = pdfCanvasRef?.current.getContext('2d');
highlightCtx = highlightCanvasRef?.current.getContext('2d');
// fetchHighlights();
loadPDF();
// setupTextSelection();
setCpdfCtx(pdfCtx);
setHighlightPdfCtx(highlightCtx);
setupZoomWheel();
return () => {
// 组件卸载时清空画布
highlightCtx.clearRect(
0,
0,
pdfCanvasRef?.current.width,
pdfCanvasRef?.current.height,
);
// 释放Canvas资源
pdfCanvasRef.current.width = 0;
pdfCanvasRef.current.height = 0;
};
}, []);
// 监听props变化
useEffect(() => {
let newPdfUrl = props.pdfUrl;
if (newPdfUrl) {
loadPDF();
}
}, [props.pdfUrl, props.highlightData]);
// PDF 加载方法
const loadPDF = async () => {
try {
const loadingTask = pdfjsLib.getDocument(pdfFile);
// const loadingTask = pdfjsLib.getDocument(props.pdfUrl);
pdfDoc = await loadingTask.promise;
setCurrentPdfDoc(pdfDoc);
setTotalPages(pdfDoc.numPages);
// console.log('currentPage', currentPage);
let firstPage = getFirstHighlightPage(); // 获取第一个高亮page
await renderPage(firstPage, pdfDoc);
} catch (error) {
console.error('Error loading PDF:', error);
}
};
// 渲染页面
const renderPage = async (cpage?: number, pdfDocs?: any) => {
let pdfDoc = pdfDocs || currentPdfDoc;
if (!pdfDoc) return;
try {
// 获取页面
const page = await pdfDoc.getPage(cpage || currentPage);
// 获取视口
const viewport = page.getViewport({ scale });
// 设置 canvas 尺寸
const pdfCanvas: CanvasItem = pdfCanvasRef?.current;
const highlightCanvas = highlightCanvasRef?.current;
pdfCanvas['height'] = viewport?.height;
pdfCanvas['width'] = viewport?.width;
highlightCanvas['height'] = viewport?.height;
highlightCanvas['width'] = viewport?.width;
// 渲染 PDF 内容
const renderContext = {
canvasContext: pdfCtx || cpdfCtx,
viewport,
};
await page.render(renderContext).promise;
const textObj = await page.getTextContent();
// 渲染高亮
renderHighlights(viewport, cpage || currentPage);
} catch (error) {
console.error('Error rendering page:', error);
}
};
// 渲染高亮
const renderHighlights = (viewport, page) => {
let HLContext = highlightPdfCtx || highlightCtx;
if (HLContext) {
HLContext?.clearRect(
0,
0,
highlightCanvasRef?.current.width,
highlightCanvasRef?.current.height,
);
let key = type === 'liu' ? 'page_id' : 'page';
const currentHighlights = highlightData.filter(h => h[key] === page);
HLContext.fillStyle = 'rgba(255, 255, 0, 0.5)';
// 设置矩形的边框颜色
// HLContext.strokeStyle = 'rgba(21, 255, 0, 0.5)';
// HLContext.lineWidth = 2; // 边框线宽
currentHighlights.forEach((highlight: any, index) => {
let position = highlight?.object_bbox || highlight?.textBbox;
let textBbox =
typeof position === 'string'
? JSON.parse(highlight?.object_bbox)
: highlight?.object_bbox;
// "[0.05,0.11,0.58,0.87]"
// [left(X),TOP(Y),height.width]
const [x1, y1, x2, y2] = textBbox;
const x = x1 * viewport.width;
const y = y1 * viewport.height;
const w = y2 * viewport.width;
const h = x2 * viewport.height;
const rect = [
x,
y,
w,
h,
];
HLContext.fillRect(rect[0], rect[1], rect[2], rect[3]);
// 绘制矩形的边框
// HLContext.strokeRect(rect[0], rect[1], rect[2], rect[3]);
});
}
};
// 页面导航
const prevPage = async () => {
if (currentPage > 1) {
let newPage = currentPage - 1;
setCurrentPage(newPage);
await renderPage(newPage);
}
};
const nextPage = async () => {
if (currentPage < totalPages) {
let newPage = currentPage + 1;
setCurrentPage(newPage);
await renderPage(newPage);
}
};
// 缩放功能
const zoomIn = async () => {
scale *= 1.1;
await renderPage();
};
const zoomOut = async () => {
scale /= 1.1;
await renderPage();
};
// 缩放滚轮控制
const setupZoomWheel = () => {
window.addEventListener(
'wheel',
e => {
if (e.ctrlKey) {
e.preventDefault();
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
}
},
{ passive: false },
);
};
return (
<>
<div id="pdf_viewer"></div>
<div className={styles['pdf-viewer']}>
<div className={styles['control-bar']}>
<button onClick={prevPage} disabled={currentPage <= 1}>
上一页
</button>
<span style={{ color: '#fff', margin: '0 10px' }}>
{currentPage} / {totalPages}
</span>
<button onClick={nextPage} disabled={currentPage >= totalPages}>
下一页
</button>
<button onClick={zoomIn} disabled={currentPage >= totalPages}>
放大
</button>
<button onClick={zoomOut} disabled={currentPage >= totalPages}>
缩小
</button>
</div>
<div className={styles['pdf-container']} ref={containerRef}>
<canvas ref={pdfCanvasRef}></canvas>
<canvas
ref={highlightCanvasRef}
className={styles['highlight-layer']}
></canvas>
</div>
</div>
</>
);
};
export default PDFDemo;
多页&仿viewer样式&缩放无高亮&性能渲染问题
import * as pdfjsLib from 'pdfjs-dist/webpack';
import React, { useEffect, useRef, useState } from 'react';
import PDFWorker from 'pdfjs-dist/build/pdf.worker';
// import PDFWorker from './pdfWorker.mjs';
// import PDFWorker from '@/services/pdf.worker.min.js';
console.log('PDFWorker', PDFWorker)
// 设置 pdf.js 工作线程的路径
// pdfjsLib.GlobalWorkerOptions.workerSrc =
// 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.9.155/pdf.worker.min.js';
// pdfjsLib.GlobalWorkerOptions.workerSrc = PDFWorker;
// 假设你知道要高亮的区域的坐标和页数
const highlightAreas = [
{
page: 10,
textBbox: [0.3, 0.3, 0.3, 0.3],
},
{
page: 3,
textBbox: [0.5, 0.1, 0.58, 0.87],
},
{
page: 5,
textBbox: [0.0594, 0.1159, 0.5886, 0.8735],
},
];
const PDFDist = ({ file, highlightData = [], type = '' }) => {
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [pages, setPages] = useState([]); // 存储渲染的页面内容
const pdfDoc = useRef(null);
const canvasHeightRef = useRef(null);
const currentHeightLightPageRef = useRef(null);
const heightLightPageListRef = useRef(null);
const scaleRef = useRef(1.65);
const canvasContext = useRef(null);
const thumbnailsRef = useRef([]); // 存储每页的缩略图
const clickImgRef = useRef(null);
let debounceTimeout;
const debounceDelay = 200; // 200ms 延时
/**
* 添加高亮效果
* @param {number} pageNumber 当前页码
* @param {*} viewport 视图
* @param {*} ctx canvas上下文
*/
const addHighlight = (pageNumber, viewport, ctx) => {
ctx.globalAlpha = 0.5; // 设置高亮透明度
ctx.fillStyle = 'yellow'; // 设置高亮颜色
// 绘制高亮矩形
highlightData.forEach(area => {
let key = type === 'liu' ? 'page_id' : 'page';
// const { page, position } = area;
let page = area[key];
// object_bbox: "[0.0594,0.1159,0.5886,0.8735]"
let areaPosition = area?.object_bbox || area?.textBbox;
let position =
typeof areaPosition === 'string'
? JSON.parse(areaPosition)
: areaPosition;
if (page === pageNumber) {
const [x1, y1, x2, y2] = position;
const start_X = x1 * viewport.width;
const start_Y = y1 * viewport.height;
const w = y2 * viewport.width;
const h = x2 * viewport.height;
let rectPosition = [
start_X,
start_Y,
w,
h,
];
let defalutRect = [
position?.[0] * viewport.scale,
position?.[1] * viewport.scale,
position?.[2] * viewport.scale,
position?.[3] * viewport.scale,
];
// 将 PDF 坐标转换为 canvas 坐标
ctx.fillRect(...rectPosition);
}
});
ctx.globalAlpha = 1.0; // 恢复透明度
};
// 生成每一页的缩略图
const createThumbnail = async (pageNumber, scale = 0.2) => {
const page = await pdfDoc.current.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
const renderContext = {
canvasContext: context,
viewport: viewport,
};
await page.render(renderContext).promise;
return canvas.toDataURL(); // 返回缩略图的 base64 图片
};
// 获取第一个高亮页
const getFirstHighlightPage = () => {
let key = type === 'liu' ? 'page_id' : 'page';
let sortHighlightData = highlightData?.sort((a, b) => a[key] - b[key]);
let firstPage = sortHighlightData?.[0]?.[key];
let pageList = [];
sortHighlightData?.map(item => {
pageList.push(item[key]);
});
heightLightPageListRef.current = pageList;
setCurrentPage(firstPage || 1);
currentHeightLightPageRef.current = firstPage || 1;
pageScrollTop(firstPage, canvasHeightRef.current);
return firstPage;
};
// 创建canvas
const createCanvas = (canvasId, container, pageNumber, viewport) => {
const canvas = document.createElement('canvas');
canvas.id = `${canvasId}-${pageNumber}`;
canvas.style.border = 'solid 15px #525659';
// 设置下边距
// canvas.style.marginBottom = '30px';
canvas.style.margin = '0 auto';
document.getElementById(container).appendChild(canvas);
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
return { context, canvas };
};
const removeAllCanvases = () => {
const canvases = document.querySelectorAll('canvas');
canvases.forEach(canvas => {
canvas.remove(); // 删除canvas元素
});
};
// 渲染页面并生成缩略图
const renderPage = async (totalPages, pdf) => {
// 清空所有旧的canvas元素
removeAllCanvases();
const newPages = [];
const newThumbnails = [];
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber++) {
// 渲染每一页
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale: +scaleRef.current });
let canvasResult = createCanvas(
'pdfcanvas',
'containerId',
pageNumber,
viewport,
);
const { context, canvas } = canvasResult;
canvasContext.current = canvasResult;
// 渲染当前页面到 canvas
await page.render({ canvasContext: context, viewport }).promise;
newPages.push(canvas);
// 添加高亮效果
addHighlight(pageNumber, viewport, context);
// 生成缩略图
const thumbnailData = await createThumbnail(pageNumber);
newThumbnails.push(thumbnailData);
canvasHeightRef.current = canvas.height;
}
thumbnailsRef.current = newThumbnails;
setPages(newPages);
};
const loadPDF = async () => {
const loadingTask = pdfjsLib.getDocument(file);
const pdf = await loadingTask.promise;
let totalPages = pdf.numPages;
setTotalPages(totalPages);
pdfDoc.current = pdf;
await renderPage(totalPages, pdf);
getFirstHighlightPage(); // 获取并设置第一个高亮页
};
const pageScrollTop = (page, canvasHeight) => {
let scrollTop = (page - 1) * (canvasHeight + 20) + page * 10;
const pdfPage = document.getElementById('pdfPage');
pdfPage.scrollTo({ top: scrollTop, behavior: 'smooth' });
};
const hlPrevPage = () => {
handleHighlightPage('-', highlightData, currentHeightLightPageRef.current);
};
const hlNextPage = () => {
handleHighlightPage('+', highlightData, currentHeightLightPageRef.current);
};
const handleScroll = () => {
const pdfPage = document.getElementById('pdfPage');
const getCurrentPage = e => {
let scrollTop = e.target.scrollTop;
let canvasHeight = canvasHeightRef.current;
let pageNumber = Math.floor(scrollTop / canvasHeight) + 1;
setCurrentPage(pageNumber);
};
pdfPage.onscroll = function(e) {
getCurrentPage(e);
};
};
const onThumbnailClick = pageNumber => {
setCurrentPage(pageNumber);
pageScrollTop(pageNumber, canvasHeightRef.current);
clickImgRef.current = pageNumber;
};
// 高亮页的上一页、下一页
const handleHighlightPage = (key, hlList, cpage) => {
let keys = type === 'liu' ? 'page_id' : 'page';
let sorthlList = hlList?.sort((a, b) => a[keys] - b[keys]);
let sorthlListAllIndex = sorthlList?.length - 1; // 高亮页最大索引
let index = sorthlList?.findIndex(item => item[keys] === cpage);
let hlPrevPageButton = document.getElementById('hlPrevPage');
let hlNextPageButton = document.getElementById('hlNextPage');
switch (key) {
case '-':
if (index > 0) {
let prevPage = hlList[index - 1]?.[keys];
currentHeightLightPageRef.current = prevPage;
pageScrollTop(prevPage, canvasHeightRef.current);
hlNextPageButton.disabled = false;
} else {
hlPrevPageButton.disabled = true;
}
break;
case '+':
if (index < sorthlListAllIndex) {
let nextPage = hlList[index + 1]?.[keys];
currentHeightLightPageRef.current = nextPage;
pageScrollTop(nextPage, canvasHeightRef.current);
hlPrevPageButton.disabled = false;
} else {
hlNextPageButton.disabled = true;
}
break;
default:
break;
}
};
useEffect(() => {
loadPDF();
handleScroll();
}, [file]);
// 滚动到顶部或底部
const quickScrolling = type => {
const pdfPage = document.getElementById('pdfPage');
if (type === 'top') {
pdfPage.scrollTo({ top: 0, behavior: 'smooth' });
}
if (type === 'bottom') {
console.log('最底部', pdfPage, pdfPage?.scrollHeight);
pdfPage.scrollTo({ top: pdfPage?.scrollHeight, behavior: 'smooth' });
}
};
const clearTimeoutFn = () => {
// 清除上次的定时器
clearTimeout(debounceTimeout);
};
// 缩放功能
const zoomIn = () => {
clearTimeoutFn();
// 设置新的定时器
debounceTimeout = setTimeout(async () => {
let scaleVal = scaleRef.current * 1.1;
scaleRef.current = scaleVal.toFixed(2);
window.requestAnimationFrame(() =>
renderPage(totalPages, pdfDoc.current),
);
// await renderPage(totalPages, pdfDoc.current);
}, debounceDelay);
};
const zoomOut = () => {
clearTimeoutFn();
debounceTimeout = setTimeout(async () => {
let scaleMin = scaleRef.current / 1.1 <= 0.2;
if (scaleMin) return false;
let scaleVal = scaleMin ? 0.2 : scaleRef.current / 1.1;
scaleRef.current = scaleVal.toFixed(2);
window.requestAnimationFrame(() =>
renderPage(totalPages, pdfDoc.current),
);
// await renderPage(totalPages, pdfDoc.current);
});
};
const divStyle = {
width: '200px',
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
};
const divStyle2 = {
width: '100px',
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
};
const divStyle3 = {
width: '340px',
display: 'flex',
alignItems: 'center',
paddingLeft: '220px',
justifyContent: 'space-evenly',
};
const barDivStyle = {
backgroundColor: '#323639',
height: '50px',
width: '100%',
position: 'fixed',
top: '0px',
left: '0px',
zIndex: '999',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0 55px',
};
const loading = {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
};
return (
<>
{/* <div id="loading" class="loading">
<div style={{ ...loading }}></div>
<p>Loading...</p>
</div> */}
<div>
<div style={barDivStyle}>
<div style={{ ...divStyle2, justifyContent: 'flex-start' }}>
<span style={{ color: '#fff', margin: '0 10px' }}>
{currentPage} / {totalPages}
</span>
</div>
<div style={divStyle3}>
<button onClick={zoomIn}>+</button>
<button onClick={zoomOut}>-</button>
</div>
<div style={divStyle}>
<button id="hlPrevPage" onClick={hlPrevPage}>
⬅
</button>
<button id="hlNextPage" onClick={hlNextPage}>
➡
</button>
<button onClick={() => quickScrolling('top')}>⬆</button>
<button onClick={() => quickScrolling('bottom')}>⬇</button>
</div>
</div>
<div
style={{
display: 'flex',
marginTop: '50px',
height: '100vh',
justifyContent: 'center',
}}
>
{/* 左侧预览框 */}
<div>
<div
style={{
width: '180px',
height: '100%',
overflowY: 'scroll',
borderRight: '1px solid #ddd',
position: 'fixed',
top: '0',
left: '0',
padding: '60px 30px 0 30px',
}}
>
{thumbnailsRef.current.map((thumbnail, index) => (
<div
key={index}
onClick={() => onThumbnailClick(index + 1)}
style={{
cursor: 'pointer',
marginBottom: '10px',
padding: '5px',
}}
>
<img
id={`img_${index + 1}`}
src={thumbnail}
alt={`Thumbnail for page ${index + 1}`}
style={{
width: '100%',
border:
clickImgRef.current === index + 1
? 'solid 3px #ffff7f'
: 'none',
}}
/>
</div>
))}
</div>
</div>
{/* 右侧 PDF 内容 */}
<div
id="pdfPage"
style={{
marginLeft: '160px',
height: '100%',
overflowY: 'auto',
width: '100%',
}}
>
<div
id="containerId"
style={{ position: 'relative', margin: '10px' }}
/>
</div>
</div>
</div>
</>
);
};
export default PDFDist;
总结:
实现pdf高亮和定位方式:
方式一:安装react-pdfjs或pdfjs-dist,使用方法实现pdf高亮和定位思路:
首先渲染pdf文件,然后根据页数和位置通过canvas绘制高亮,获取第一高亮页数或dom,通过 pdfPage.scrollTo()或targetPage.scrollIntoView({ behavior: 'smooth' });滚动到页面。
方式二:将pdfjs-dist文件放在public下,通过iframe嵌入实现
首先在iframe的src中找到public下的viewer.html,通过?传参给file,将pdf文件路径通过file传入viewer.html来显示视图,获取viewer中的dom,通过页数和位置向textLayer追加高亮块来实现高亮;通过第一高亮页数获取对应dom,通过scrollIntoView自动定位到第一高亮页。
(注意:打包线上的viewer的文件路径是否一致,以及postmessage与iframe通讯时的源是否一致,防止跨域问题)