使用pdfjs_3.2.146 预览并本地存储批注demo

现在有一个需求: 这个需求是使用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.jsbuild/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>
相关推荐
90后的晨仔1 分钟前
零基础快速搭建 Vue 3 开发环境(附官方推荐方法)
前端·vue.js
洛_尘14 分钟前
Java EE进阶2:前端 HTML+CSS+JavaScript
java·前端·java-ee
孤独的根号_17 分钟前
Vite背后的技术原理🚀:为什么选择Vite作为你的前端构建工具💥
前端·vue.js·vite
吹牛不交税1 小时前
Axure RP Extension for Chrome插件安装使用
前端·chrome·axure
薛定谔的算法1 小时前
# 前端路由进化史:从白屏到丝滑体验的技术突围
前端·react.js·前端框架
拾光拾趣录2 小时前
Element Plus表格表头动态刷新难题:零闪动更新方案
前端·vue.js·element
Adolf_19933 小时前
React 中 props 的最常用用法精选+useContext
前端·javascript·react.js
前端小趴菜053 小时前
react - 根据路由生成菜单
前端·javascript·react.js
喝拿铁写前端3 小时前
`reduce` 究竟要不要用?到底什么时候才“值得”用?
前端·javascript·面试
空の鱼3 小时前
js与vue基础学习
javascript·vue.js·学习