现在有一个需求: 这个需求是使用pdfjs对pdf进行预览,预览的同时会有各种用户给pdf加上批注,这个批注不是写在原始pdf上的,因为这个原始pdf存在另一个更高级的服务器上,只给了可读权限。因此,批注是要存在应用本地,比如LocalStorage,当我第二次打开pdf的时候,会把以前的批注重新加载上来。因为这个功能是嵌入在一个比较老的工程上,因此选用的是pdfjs 3.2.146 版本
首先下载提取 pdf.js 3.2.146 的源码:
- pdf.js
- pdf.worker.js
pdf.js 3.2.146 的发行版包(即 build/pdf.js
和 build/pdf.worker.js
)可以从:
👉 github.com/mozilla/pdf... 下载
然后创建一个html文件,命名为 pdf_handle.html
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>PDF.js Scrollable Annotator Demo</title>
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 0;
}
#toolbar {
padding: 10px;
background: #eee;
border-bottom: 1px solid #ccc;
}
#toolbar button, #toolbar input {
margin-right: 5px;
}
#pdf-container {
overflow-y: auto;
height: 90vh;
padding: 10px;
background: #f8f8f8;
}
.pdf-page {
position: relative;
margin-bottom: 20px;
background: #fff;
box-shadow: 0 0 4px rgba(0,0,0,0.2);
}
canvas.rendered {
display: block;
}
canvas.draw-layer {
position: absolute;
top: 0;
left: 0;
z-index: 10;
}
.text-layer {
position: absolute;
top: 0;
left: 0;
z-index: 20;
pointer-events: none;
}
.annotation-layer {
position: absolute;
top: 0;
left: 0;
z-index: 30;
pointer-events: auto;
}
.text-annotation {
position: absolute;
background: rgba(255,255,0,0.8);
padding: 2px 4px;
border: 1px solid #888;
font-size: 14px;
cursor: move;
user-select: none;
pointer-events: auto;
}
</style>
</head>
<body>
<div id="toolbar">
<input type="file" id="file-input" />
<button id="draw-mode">Draw Mode</button>
<button id="text-mode">Text Mode</button>
<button id="save">Save All Annotations</button>
<button id="clear">Clear All Annotations</button>
</div>
<div id="pdf-container"></div>
<script src="pdf.js"></script>
<script>
const pdfjsLib = window['pdfjs-dist/build/pdf'];
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdf.worker.js';
let pdfDoc = null;
let scale = 1.5;
let drawMode = false;
let textMode = false;
let dragMode = false; // 添加拖拽模式状态
let annotationsKeyBase = '';
let allAnnotations = {};
let filenameSafe = '';
let pdfLoaded = false; // 添加PDF加载状态变量
document.getElementById('draw-mode').onclick = () => {
if (!pdfLoaded) {
alert('请先上传PDF文件');
return;
}
drawMode = true;
textMode = false;
dragMode = false; // 关闭拖拽模式
setPointerEvents('draw'); // 修改为传递模式参数
};
document.getElementById('text-mode').onclick = () => {
if (!pdfLoaded) {
alert('请先上传PDF文件');
return;
}
drawMode = false;
textMode = true;
dragMode = false; // 关闭拖拽模式
setPointerEvents('text'); // 修改为传递模式参数
};
document.getElementById('save').onclick = () => {
saveAllAnnotations();
alert('Annotations saved!');
};
document.getElementById('clear').onclick = () => {
if (confirm('Clear all annotations?')) {
allAnnotations = {};
localStorage.removeItem(annotationsKeyBase);
document.getElementById('pdf-container').innerHTML = '';
renderAllPages();
}
};
document.getElementById('file-input').addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function() {
const typedarray = new Uint8Array(this.result);
filenameSafe = file.name.replace(/\W+/g, '_');
annotationsKeyBase = 'annotations_' + filenameSafe;
const saved = localStorage.getItem(annotationsKeyBase);
allAnnotations = saved ? JSON.parse(saved) : {};
loadPDF(typedarray);
};
reader.readAsArrayBuffer(file);
}
});
async function loadPDF(data) {
pdfDoc = await pdfjsLib.getDocument({data}).promise;
document.getElementById('pdf-container').innerHTML = '';
await renderAllPages();
pdfLoaded = true; // 标记PDF已加载
// 默认启用绘图模式
drawMode = true;
textMode = false;
dragMode = false;
setPointerEvents('draw'); // 修改为传递模式参数
}
async function renderAllPages() {
for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) {
const pageDiv = document.createElement('div');
pageDiv.className = 'pdf-page';
pageDiv.dataset.page = pageNum;
const renderCanvas = document.createElement('canvas');
renderCanvas.className = 'rendered';
const drawCanvas = document.createElement('canvas');
drawCanvas.className = 'draw-layer';
const textLayer = document.createElement('div');
textLayer.className = 'text-layer';
const annotationLayer = document.createElement('div');
annotationLayer.className = 'annotation-layer';
pageDiv.appendChild(renderCanvas);
pageDiv.appendChild(drawCanvas);
pageDiv.appendChild(textLayer);
pageDiv.appendChild(annotationLayer);
document.getElementById('pdf-container').appendChild(pageDiv);
const page = await pdfDoc.getPage(pageNum);
const viewport = page.getViewport({scale});
renderCanvas.width = viewport.width;
renderCanvas.height = viewport.height;
drawCanvas.width = viewport.width;
drawCanvas.height = viewport.height;
textLayer.style.width = viewport.width + 'px';
textLayer.style.height = viewport.height + 'px';
annotationLayer.style.width = viewport.width + 'px';
annotationLayer.style.height = viewport.height + 'px';
await page.render({canvasContext: renderCanvas.getContext('2d'), viewport}).promise;
setupDrawLayer(drawCanvas, annotationLayer, pageNum);
loadAnnotationsForPage(drawCanvas, annotationLayer, pageNum);
}
}
// 修改setPointerEvents函数,根据不同模式设置不同的事件处理
function setPointerEvents(mode) {
const drawLayers = document.querySelectorAll('.draw-layer');
const annotationLayers = document.querySelectorAll('.annotation-layer');
// 根据模式设置绘图层的事件处理
drawLayers.forEach(layer => {
layer.style.pointerEvents = (mode === 'draw' || mode === 'text') ? 'auto' : 'none';
});
// 根据模式设置注释层的事件处理
annotationLayers.forEach(layer => {
// 在绘图或文本模式下,注释层不接收事件,以便绘图层能接收事件
layer.style.pointerEvents = (mode === 'drag') ? 'auto' : 'none';
// 设置所有文本注释的事件处理
const annotations = layer.querySelectorAll('.text-annotation');
annotations.forEach(annotation => {
// 在拖拽模式下,文本注释可以接收事件
annotation.style.pointerEvents = (mode === 'drag') ? 'auto' : 'none';
});
});
}
function setupDrawLayer(drawCanvas, annotationLayer, pageNum) {
const ctx = drawCanvas.getContext('2d');
ctx.lineWidth = 2;
ctx.strokeStyle = 'red';
ctx.lineCap = 'round';
let isDrawing = false;
let currentPath = [];
drawCanvas.addEventListener('mousedown', (e) => {
if (drawMode) {
isDrawing = true;
ctx.beginPath();
ctx.moveTo(e.offsetX, e.offsetY);
currentPath = [{x: e.offsetX / drawCanvas.width, y: e.offsetY / drawCanvas.height}];
} else if (textMode) {
const text = prompt('Enter annotation text:');
if (text) {
const x = e.offsetX;
const y = e.offsetY;
const normX = x / drawCanvas.width;
const normY = y / drawCanvas.height;
addTextAnnotation(annotationLayer, text, x, y);
if (!allAnnotations[pageNum]) allAnnotations[pageNum] = { drawing: [], texts: [] };
allAnnotations[pageNum].texts.push({text, x: normX, y: normY});
}
}
});
drawCanvas.addEventListener('mousemove', (e) => {
if (isDrawing && drawMode) {
ctx.lineTo(e.offsetX, e.offsetY);
ctx.stroke();
currentPath.push({x: e.offsetX / drawCanvas.width, y: e.offsetY / drawCanvas.height});
}
});
drawCanvas.addEventListener('mouseup', () => {
if (isDrawing && drawMode) {
isDrawing = false;
if (!allAnnotations[pageNum]) allAnnotations[pageNum] = { drawing: [], texts: [] };
allAnnotations[pageNum].drawing.push(currentPath);
}
});
}
function addTextAnnotation(layer, text, x, y) {
const el = document.createElement('div');
el.className = 'text-annotation';
el.style.left = x + 'px';
el.style.top = y + 'px';
el.textContent = text;
layer.appendChild(el);
// 添加拖拽功能
makeDraggable(el);
// 添加点击事件,切换到拖拽模式
el.addEventListener('click', (e) => {
// 如果不是在拖拽模式下,则切换到拖拽模式
if (!dragMode) {
e.stopPropagation(); // 阻止事件冒泡
dragMode = true;
drawMode = false;
textMode = false;
setPointerEvents('drag');
}
});
}
function makeDraggable(element) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
element.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e.preventDefault();
// 获取鼠标位置
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// 鼠标移动时调用elementDrag
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e.preventDefault();
// 计算新位置
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// 设置元素的新位置
element.style.top = (element.offsetTop - pos2) + 'px';
element.style.left = (element.offsetLeft - pos1) + 'px';
}
function closeDragElement() {
// 停止移动
document.onmouseup = null;
document.onmousemove = null;
// 更新注释位置到存储中
const pageDiv = element.closest('.pdf-page');
if (pageDiv) {
const pageNum = parseInt(pageDiv.dataset.page);
const annotationLayer = pageDiv.querySelector('.annotation-layer');
const annotations = allAnnotations[pageNum];
if (annotations) {
// 找到对应的文本注释
const text = element.textContent;
const x = parseInt(element.style.left) / annotationLayer.offsetWidth;
const y = parseInt(element.style.top) / annotationLayer.offsetHeight;
// 查找并更新注释位置
const textAnnotation = annotations.texts.find(t => t.text === text);
if (textAnnotation) {
textAnnotation.x = x;
textAnnotation.y = y;
}
}
}
}
}
function loadAnnotationsForPage(drawCanvas, annotationLayer, pageNum) {
const ctx = drawCanvas.getContext('2d');
ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);
annotationLayer.innerHTML = '';
const annotations = allAnnotations[pageNum];
if (!annotations) return;
annotations.drawing.forEach(path => {
ctx.beginPath();
path.forEach((pt, idx) => {
const x = pt.x * drawCanvas.width;
const y = pt.y * drawCanvas.height;
if (idx === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
});
annotations.texts.forEach(t => {
addTextAnnotation(annotationLayer, t.text, t.x * drawCanvas.width, t.y * drawCanvas.height);
});
}
function saveAllAnnotations() {
localStorage.setItem(annotationsKeyBase, JSON.stringify(allAnnotations));
}
// 添加点击PDF容器的事件,用于退出拖拽模式
document.getElementById('pdf-container').addEventListener('click', (e) => {
// 如果点击的不是文本注释,并且当前是拖拽模式,则退出拖拽模式
if (dragMode && !e.target.classList.contains('text-annotation')) {
dragMode = false;
// 恢复之前的模式
if (drawMode) {
setPointerEvents('draw');
} else if (textMode) {
setPointerEvents('text');
}
}
});
</script>
</body>
</html>