阅读视图

发现新文章,点击刷新页面。

基于PDF.js的安全PDF预览组件实现:从虚拟滚动到水印渲染

基于PDF.js的安全PDF预览组件实现:从虚拟滚动到水印渲染

本文将详细介绍如何基于Mozilla PDF.js实现一个功能完善、安全可靠的PDF预览组件,重点讲解虚拟滚动、双模式渲染、水印实现等核心技术。

前言

在Web应用中实现PDF预览功能是常见需求,尤其是在线教育、文档管理等场景。然而,简单的PDF预览往往无法满足实际业务需求,特别是在安全性方面。本文将介绍如何基于PDF.js实现一个功能完善的PDF预览组件,并重点讲解如何添加自定义防下载和水印功能,为文档安全提供保障。

功能概览

我们的PDF预览组件实现了以下核心功能:

  1. 基础功能:PDF文件加载与渲染、自定义尺寸控制、页面缩放规则配置、主题切换
  2. 安全增强:动态水印添加、防下载功能、右键菜单禁用、打印控制
  3. 用户体验:页面渲染事件通知、响应式布局适配、加载状态反馈

技术实现

1. 虚拟滚动加载

对于大型PDF文件,一次性渲染所有页面会导致严重的性能问题。我们通过虚拟滚动技术优化大文档的加载性能,只渲染当前可见区域和附近的页面:

// 页面缓存管理
class PDFPageViewBuffer {
  #buf = new Set();
  #size = 0;

  constructor(size) {
    this.#size = size;  // 缓存页面数量限制
  }

  push(view) {
    const buf = this.#buf;
    if (buf.has(view)) {
      buf.delete(view);
    }
    buf.add(view);
    if (buf.size > this.#size) {
      this.#destroyFirstView();  // 超出限制时销毁最早的页面
    }
  }
}

优势

  • 内存优化:只保留有限数量的页面在内存中
  • 性能提升:减少不必要的渲染操作
  • 流畅体验:滚动时动态加载页面

2. 双模式渲染:Canvas与HTML

PDF.js支持两种渲染模式,可根据不同需求选择。两种渲染方式在视觉效果和性能上有明显差异:

在这里插入图片描述

图:HTML渲染模式下的PDF显示效果

在这里插入图片描述

图:Canvas渲染模式下的PDF显示效果

Canvas渲染(默认)
// 创建Canvas元素
const canvas = document.createElement("canvas");
canvas.setAttribute("role", "presentation");

// 获取2D渲染上下文
const ctx = canvas.getContext("2d", {
  alpha: false,           // 禁用透明度通道,提高性能
  willReadFrequently: !this.#enableHWA  // 根据硬件加速设置优化
});

// 渲染PDF页面到Canvas
const renderContext = {
  canvasContext: ctx,
  transform,
  viewport,
  // 其他参数...
};
const renderTask = pdfPage.render(renderContext);
HTML渲染
// HTML渲染模式(文本层)
if (!this.textLayer && this.#textLayerMode !== TextLayerMode.DISABLE) {
  this.textLayer = new TextLayerBuilder({
    pdfPage,
    highlighter: this._textHighlighter,
    accessibilityManager: this._accessibilityManager,
    enablePermissions: this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS,
    onAppend: (textLayerDiv) => {
      this.#addLayer(textLayerDiv, "textLayer");
    }
  });
}

两种模式对比

特性 Canvas渲染 HTML渲染
性能 中等
文本选择 不支持 支持
缩放质量 中等
内存使用
兼容性 极好

3. 水印渲染实现

水印是保护文档版权的重要手段。我们在PDF页面渲染完成后,直接在Canvas上添加水印,确保水印与内容融为一体:

// 在渲染完成后添加水印
const resultPromise = renderTask.promise.then(async () => {
  showCanvas?.(true);
  await this.#finishRenderTask(renderTask);

  // 添加水印
  createWaterMark({ fontText: warterMark, canvas, ctx });

  // 其他处理...
});

// 水印绘制函数
function createWaterMark({
  ctx,
  canvas,
  fontText = '默认水印',
  fontFamily = 'microsoft yahei',
  fontSize = 30,
  fontcolor = 'rgba(218, 218, 218, 0.5)',
  rotate = 30,
  textAlign = 'left'
}) {
  // 保存当前状态
  ctx.save();

  // 计算响应式字体大小
  const canvasW = canvas.width;
  const calfontSize = (fontSize * canvasW) / 800;
  ctx.font = `${calfontSize}px ${fontFamily}`;
  ctx.fillStyle = fontcolor;
  ctx.textAlign = textAlign;
  ctx.textBaseline = 'Middle';

  // 添加多个水印
  const pH = canvas.height / 4;
  const pW = canvas.width / 4;
  const positions = [
    { x: pW, y: pH },
    { x: 3 * pW, y: pH },
    { x: pW * 1.3, y: 3 * pH },
    { x: 3 * pW, y: 3 * pH }
  ];

  positions.forEach((pos) => {
    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.rotate(-rotate * Math.PI / 180);
    ctx.fillText(fontText, 0, 0);
    ctx.restore();
  });

  // 恢复状态
  ctx.restore();
}

水印技术亮点

  • 响应式设计:根据Canvas宽度自动调整水印尺寸
  • 多点布局:四个位置分布水印,覆盖整个页面
  • 旋转效果:每个水印独立旋转30度,增加覆盖范围
  • 透明度处理:使用半透明颜色,不影响内容可读性

4. 防下载与打印控制

为了增强文档安全性,我们实现了全面的防下载和打印控制功能:

// 禁用右键菜单
document.addEventListener('contextmenu', function(e) {
  e.preventDefault();
  return false;
});

// 禁用文本选择
document.addEventListener('selectstart', function(e) {
  e.preventDefault();
  return false;
});

// 禁用拖拽
document.addEventListener('dragstart', function(e) {
  e.preventDefault();
  return false;
});

// 拦截Ctrl+P打印快捷键
window.addEventListener("keydown", function (event) {
  if (event.keyCode === 80 && (event.ctrlKey || event.metaKey) && 
      !event.altKey && (!event.shiftKey || window.chrome || window.opera)) {
    // 自定义打印行为或完全禁用
    event.preventDefault();
    event.stopImmediatePropagation();
  }
}, true);

Vue组件实现

基于以上技术,我们实现了一个功能完善的Vue3 PDF预览组件:

<template>
  <iframe
    :width="viewerWidth"
    :height="viewerHeight"
    id="ifra"
    frameborder="0"
    :src="`/pdfJs/web/viewer.html?file=${src}&waterMark=${waterMark}`"
    @load="pagesRendered"
  />
</template>

<script setup>
import { computed } from 'vue'
import { useUserStore } from '~/store/user'

const props = defineProps({
  src: String,
  width: [String, Number],
  height: [String, Number],
  pageScale: [String, Number],
  theme: String,
  fileName: String
})

const emit = defineEmits(['loaded'])

// 默认值设置
const propsWithDefaults = withDefaults(props, {
  width: '100%',
  height: '100vh',
  pageScale: 'page-width',
  theme: 'dark',
  fileName: ''
})

// 尺寸计算
const viewerWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  } else {
    return props.width
  }
})

const viewerHeight = computed(() => {
  if (typeof props.height === 'number') {
    return props.height + 'px'
  } else {
    return props.height
  }
})

// 用户信息和水印
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)

const waterMark = computed(() => {
  const { userName, phoneNum } = userInfo.value
  const phoneSuffix = phoneNum && phoneNum.substring(phoneNum.length - 4)
  return userName + phoneSuffix
})

// 页面渲染事件
function pagesRendered(pdfApp) {
  emit('loaded', pdfApp)
}
</script>

<style scoped>
#ifra {
  max-width: 100%;
  height: 100%;
  margin-left: 50%;
  transform: translateX(-50%);
}
</style>

使用方法

基本使用

<template>
  <PDFViewer
    src="path/to/your/pdf/file.pdf"
    :width="800"
    :height="600"
    @loaded="handlePdfLoaded"
  />
</template>

<script setup>
import PDFViewer from '@/components/PDFViewer/index.vue'

function handlePdfLoaded(pdfApp) {
  console.log('PDF已加载完成', pdfApp)
}
</script>

高级配置

<template>
  <PDFViewer
    src="path/to/your/pdf/file.pdf"
    width="100%"
    height="90vh"
    page-scale="page-fit"
    theme="light"
    file-name="自定义文件名.pdf"
    @loaded="handlePdfLoaded"
  />
</template>

性能优化

1. 渲染性能优化

// 设置合理的maxCanvasPixels
const maxCanvasPixels = isHighEndDevice ? 
  16777216 * 4 :  // 4K显示器
  8388608 * 2;   // 普通显示器

const pdfViewer = new PDFViewer({
  container: document.getElementById('viewer'),
  maxCanvasPixels: maxCanvasPixels
});

2. 内存管理优化

// 限制缓存页面数量,防止内存溢出
pdfViewer.setDocument(pdfDocument);
pdfViewer.currentScaleValue = 'auto';

// 定期清理不可见页面
setInterval(() => {
  const visiblePages = pdfViewer._getVisiblePages();
  // 清理不可见页面的缓存
}, 30000);

3. 按需渲染

// 只渲染可见页面
pdfViewer.onPagesLoaded = () => {
  const visiblePages = pdfViewer._getVisiblePages();
  // 只渲染可见页面,延迟渲染其他页面
};

注意事项

  1. PDF.js版本:确保使用兼容的PDF.js版本,不同版本API可能有差异
  2. 跨域处理:PDF文件可能存在跨域问题,需确保服务器配置了正确的CORS头
  3. 大文件处理:对于大型PDF文件,考虑添加加载进度提示
  4. 移动端适配:在移动设备上可能需要额外的样式调整
  5. 安全限制:虽然实现了防下载和水印,但无法完全防止技术用户获取PDF内容

扩展功能建议

  1. 页面跳转:添加页面导航功能,支持直接跳转到指定页面
  2. 文本搜索:实现PDF内容搜索功能
  3. 注释工具:添加PDF注释、标记功能
  4. 水印样式自定义:支持更多水印样式和位置配置
  5. 访问控制:基于用户角色限制PDF访问权限

总结

本文介绍了如何基于Mozilla PDF.js实现一个功能完善的PDF预览组件,并重点讲解了如何添加自定义的防下载和水印功能。通过合理的技术选型和组件设计,我们实现了一个既美观又安全的PDF预览解决方案。

在实际应用中,您可以根据具体需求进一步扩展功能,如添加页面导航、文本搜索等高级特性,为用户提供更丰富的PDF阅读体验,同时确保文档内容的安全性。

希望本文对您在Vue3项目中实现安全PDF预览功能有所帮助!

需要源码的评论区回复6666

❌