王玉姝 發表於 2026-2-10 11:52:00

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

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<h2 data-id="heading-0">基于PDF.js的安全PDF预览组件实现:从虚拟滚动到水印渲染</h2>
<blockquote>
<p>本文将详细介绍如何基于Mozilla PDF.js实现一个功能完善、安全可靠的PDF预览组件,重点讲解虚拟滚动、双模式渲染、水印实现等核心技术。</p>
</blockquote>
<h3 data-id="heading-1">前言</h3>
<p>在Web应用中实现PDF预览功能是常见需求,尤其是在线教育、文档管理等场景。然而,简单的PDF预览往往无法满足实际业务需求,特别是在安全性方面。本文将介绍如何基于PDF.js实现一个功能完善的PDF预览组件,并重点讲解如何添加自定义防下载和水印功能,为文档安全提供保障。</p>
<h3 data-id="heading-2">功能概览</h3>
<p>我们的PDF预览组件实现了以下核心功能:</p>
<ol>
<li><strong>基础功能</strong>:PDF文件加载与渲染、自定义尺寸控制、页面缩放规则配置、主题切换</li>
<li><strong>安全增强</strong>:动态水印添加、防下载功能、右键菜单禁用、打印控制</li>
<li><strong>用户体验</strong>:页面渲染事件通知、响应式布局适配、加载状态反馈</li>
</ol>
<h3 data-id="heading-3">技术实现</h3>
<h4 data-id="heading-4">1. 虚拟滚动加载</h4>
<p>对于大型PDF文件,一次性渲染所有页面会导致严重的性能问题。我们通过虚拟滚动技术优化大文档的加载性能,只渲染当前可见区域和附近的页面:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 页面缓存管理
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 &gt; this.#size) {
      this.#destroyFirstView();// 超出限制时销毁最早的页面
    }
}
}</pre>
</div>
<p>优势:</p>
<ul>
<li>内存优化:只保留有限数量的页面在内存中</li>
<li>性能提升:减少不必要的渲染操作</li>
<li>流畅体验:滚动时动态加载页面</li>
</ul>
<h4 data-id="heading-5">2. 双模式渲染:Canvas与HTML</h4>
<p>PDF.js支持两种渲染模式,可根据不同需求选择。两种渲染方式在视觉效果和性能上有明显差异:</p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260210113909076-409546459.png" alt="ScreenShot_2026-02-10_112550_802" loading="lazy"></p>
<p>&nbsp;<em>图:HTML渲染模式下的PDF显示效果</em></p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260210113916909-1697876788.png" alt="ScreenShot_2026-02-10_112559_413" loading="lazy"></p>
<p>&nbsp;</p>
<p><em>图:Canvas渲染模式下的PDF显示效果</em></p>
<h5 data-id="heading-6">Canvas渲染(默认)</h5>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 创建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);</pre>
</div>
<h5 data-id="heading-7">HTML渲染</h5>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// HTML渲染模式(文本层)
if (!this.textLayer &amp;&amp; this.#textLayerMode !== TextLayerMode.DISABLE) {
this.textLayer = new TextLayerBuilder({
    pdfPage,
    highlighter: this._textHighlighter,
    accessibilityManager: this._accessibilityManager,
    enablePermissions: this.#textLayerMode === TextLayerMode.ENABLE_PERMISSIONS,
    onAppend: (textLayerDiv) =&gt; {
      this.#addLayer(textLayerDiv, "textLayer");
    }
});
}</pre>
</div>
<h3>两种模式对比:</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202602/2149129-20260210114856035-31373123.png" alt="ScreenShot_2026-02-10_112610_375" loading="lazy"></p>
<p>&nbsp;</p>
<h4 data-id="heading-8">3. 水印渲染实现</h4>
<p>水印是保护文档版权的重要手段。我们在PDF页面渲染完成后,直接在Canvas上添加水印,确保水印与内容融为一体:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 在渲染完成后添加水印
const resultPromise = renderTask.promise.then(async () =&gt; {
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) =&gt; {
    ctx.save();
    ctx.translate(pos.x, pos.y);
    ctx.rotate(-rotate * Math.PI / 180);
    ctx.fillText(fontText, 0, 0);
    ctx.restore();
});

// 恢复状态
ctx.restore();
}
</pre>
</div>
<div>
<div>
<p><strong>水印技术亮点</strong>:</p>
<ul>
<li>响应式设计:根据Canvas宽度自动调整水印尺寸</li>
<li>多点布局:四个位置分布水印,覆盖整个页面</li>
<li>旋转效果:每个水印独立旋转30度,增加覆盖范围</li>
<li>透明度处理:使用半透明颜色,不影响内容可读性</li>
</ul>
<h4 data-id="heading-9">4. 防下载与打印控制</h4>
<p>为了增强文档安全性,我们实现了全面的防下载和打印控制功能:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 禁用右键菜单
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 &amp;&amp; (event.ctrlKey || event.metaKey) &amp;&amp;
      !event.altKey &amp;&amp; (!event.shiftKey || window.chrome || window.opera)) {
    // 自定义打印行为或完全禁用
    event.preventDefault();
    event.stopImmediatePropagation();
}
}, true);</pre>
</div>
<h3 data-id="heading-10">Vue组件实现</h3>
<p>基于以上技术,我们实现了一个功能完善的Vue3 PDF预览组件:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;template&gt;
&lt;iframe
    :width="viewerWidth"
    :height="viewerHeight"
    id="ifra"
    frameborder="0"
    :src="`/pdfJs/web/viewer.html?file=${src}&amp;waterMark=${waterMark}`"
    @load="pagesRendered"
/&gt;
&lt;/template&gt;

&lt;script setup&gt;
import { computed } from 'vue'
import { useUserStore } from '~/store/user'

const props = defineProps({
src: String,
width: ,
height: ,
pageScale: ,
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(() =&gt; {
if (typeof props.width === 'number') {
    return props.width + 'px'
} else {
    return props.width
}
})

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

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

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

// 页面渲染事件
function pagesRendered(pdfApp) {
emit('loaded', pdfApp)
}
&lt;/script&gt;

&lt;style scoped&gt;
#ifra {
max-width: 100%;
height: 100%;
margin-left: 50%;
transform: translateX(-50%);
}
&lt;/style&gt;</pre>
</div>
<h3 data-id="heading-11">使用方法</h3>
<h4 data-id="heading-12">基本使用</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;template&gt;
&lt;PDFViewer
    src="path/to/your/pdf/file.pdf"
    :width="800"
    :height="600"
    @loaded="handlePdfLoaded"
/&gt;
&lt;/template&gt;

&lt;script setup&gt;
import PDFViewer from '@/components/PDFViewer/index.vue'

function handlePdfLoaded(pdfApp) {
console.log('PDF已加载完成', pdfApp)
}
&lt;/script&gt;</pre>
</div>
<h4 data-id="heading-13">高级配置</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;template&gt;
&lt;PDFViewer
    src="path/to/your/pdf/file.pdf"
    width="100%"
    height="90vh"
    page-scale="page-fit"
    theme="light"
    file-name="自定义文件名.pdf"
    @loaded="handlePdfLoaded"
/&gt;
&lt;/template&gt;</pre>
</div>
<h3 data-id="heading-14">性能优化</h3>
<h4 data-id="heading-15">1. 渲染性能优化</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 设置合理的maxCanvasPixels
const maxCanvasPixels = isHighEndDevice ?
16777216 * 4 :// 4K显示器
8388608 * 2;   // 普通显示器

const pdfViewer = new PDFViewer({
container: document.getElementById('viewer'),
maxCanvasPixels: maxCanvasPixels
});</pre>
</div>
<h4 data-id="heading-16">2. 内存管理优化</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 限制缓存页面数量,防止内存溢出
pdfViewer.setDocument(pdfDocument);
pdfViewer.currentScaleValue = 'auto';

// 定期清理不可见页面
setInterval(() =&gt; {
const visiblePages = pdfViewer._getVisiblePages();
// 清理不可见页面的缓存
}, 30000);</pre>
</div>
<h4 data-id="heading-17">3. 按需渲染</h4>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 只渲染可见页面
pdfViewer.onPagesLoaded = () =&gt; {
const visiblePages = pdfViewer._getVisiblePages();
// 只渲染可见页面,延迟渲染其他页面
};</pre>
</div>
<div>
<div>
<h3 data-id="heading-18">注意事项</h3>
<ol>
<li><strong>PDF.js版本</strong>:确保使用兼容的PDF.js版本,不同版本API可能有差异</li>
<li><strong>跨域处理</strong>:PDF文件可能存在跨域问题,需确保服务器配置了正确的CORS头</li>
<li><strong>大文件处理</strong>:对于大型PDF文件,考虑添加加载进度提示</li>
<li><strong>移动端适配</strong>:在移动设备上可能需要额外的样式调整</li>
<li><strong>安全限制</strong>:虽然实现了防下载和水印,但无法完全防止技术用户获取PDF内容</li>
</ol>
<h3 data-id="heading-19">扩展功能建议</h3>
<ol>
<li><strong>页面跳转</strong>:添加页面导航功能,支持直接跳转到指定页面</li>
<li><strong>文本搜索</strong>:实现PDF内容搜索功能</li>
<li><strong>注释工具</strong>:添加PDF注释、标记功能</li>
<li><strong>水印样式自定义</strong>:支持更多水印样式和位置配置</li>
<li><strong>访问控制</strong>:基于用户角色限制PDF访问权限</li>
</ol>
<h3 data-id="heading-20">总结</h3>
<p>本文介绍了如何基于Mozilla PDF.js实现一个功能完善的PDF预览组件,并重点讲解了如何添加自定义的防下载和水印功能。通过合理的技术选型和组件设计,我们实现了一个既美观又安全的PDF预览解决方案。</p>
<p>在实际应用中,您可以根据具体需求进一步扩展功能,如添加页面导航、文本搜索等高级特性,为用户提供更丰富的PDF阅读体验,同时确保文档内容的安全性。</p>
<p>希望本文对您在Vue3项目中实现安全PDF预览功能有所帮助!</p>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19598927
頁: [1]
查看完整版本: 基于PDF.js的安全PDF预览组件实现:从虚拟滚动到水印渲染