查看: 121|回复: 1

基于pdf-lib和JSZip的前端PDF拆分工具实现

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在线PDF拆分工具的前端实现中,核心挑战在于将用户的拆分意图统一转换为稳定的页码分组,再通过pdf-lib复制页面生成多个PDF文件,最后根据结果数量决定直接下载或打包为ZIP。本文围绕这一流程,解析JavaScript层面的关键代码设计。
  1. // 文件类型判断:同时检查MIME与后缀,兼容部分浏览器file.type为空的情况
  2. export function isPdfSplitFile(file) {
  3.   if (!file) return false;
  4.   var fileType = String(file.type || '').toLowerCase();
  5.   var fileName = String(file.name || '');
  6.   return fileType === 'application/pdf' || /\.pdf$/i.test(fileName);
  7. }
复制代码

加载PDF时,将原始字节拷贝两份:一份用于拆分操作(splitBytes),一份用于预览及书签读取(previewBytes),避免主链路与辅助链路相互影响。

页码输入解析成统一结构——{label, indices},label用于文件命名,indices是pdf-lib需要的零基页码数组。支持逗号分隔、倒序区间(如8-6),利用buildPageIndices生成序列。
  1. function buildPageIndices(start, end) {
  2.   var indices = [];
  3.   if (start <= end) {
  4.     for (var page = start; page <= end; page++) {
  5.       indices.push(page - 1);
  6.     }
  7.   } else {
  8.     for (var page = start; page >= end; page--) {
  9.       indices.push(page - 1);
  10.     }
  11.   }
  12.   return indices;
  13. }
复制代码

工具提供7种拆分模式:按页码范围、每N页、每页单独、奇偶页、可视化选择、书签、平均拆成N份。每种模式最终都归一化为groups数组,拆分主函数只消费groups,不关心来源。

可视化选择模式下,对用户点选的离散页码先排序去重,再将连续页合并成一个输出段。例如选择1,2,3,7,9,10,合并为1-3、7、9-10三个文件。
  1. export function buildPdfSplitVisualGroups(selectedPages) {
  2.   var uniquePages = Array.isArray(selectedPages) ? selectedPages
  3.     .map(p => Number(p))
  4.     .filter(p => Number.isInteger(p) && p > 0)
  5.     .sort((a, b) => a - b)
  6.     .filter((p, i, arr) => i === 0 || p !== arr[i-1]) : [];
  7.   if (!uniquePages.length) throw createPdfSplitInputError('emptySelection');
  8.   var groups = [];
  9.   var start = uniquePages[0], end = uniquePages[0];
  10.   for (var i = 1; i < uniquePages.length; i++) {
  11.     if (uniquePages[i] === end + 1) { end = uniquePages[i]; continue; }
  12.     pushMergedSelectionGroup(groups, start, end);
  13.     start = uniquePages[i]; end = uniquePages[i];
  14.   }
  15.   pushMergedSelectionGroup(groups, start, end);
  16.   return groups;
  17. }
复制代码

书签拆分借助pdfjs-dist读取PDF的outline,将每个书签所在页作为开始页,下一书签前一页作为结束页;若第一个书签不在第1页,前面内容单独生成“preface”分段。
  1. export function buildPdfSplitBookmarkGroups(bookmarks, totalPages) {
  2.   var normalized = bookmarks
  3.     .filter(item => item && Number.isInteger(Number(item.pageNumber)) &&
  4.                     Number(item.pageNumber) >= 1 && Number(item.pageNumber) <= totalPages)
  5.     .map(item => ({title: String(item.title).trim() || 'bookmark', pageNumber: Number(item.pageNumber)}))
  6.     .sort((a,b) => a.pageNumber - b.pageNumber);
  7.   var groups = [];
  8.   if (normalized[0].pageNumber > 1) {
  9.     groups.push({label: 'preface', indices: buildPageIndices(1, normalized[0].pageNumber - 1), title: 'preface'});
  10.   }
  11.   for (var i = 0; i < normalized.length; i++) {
  12.     var cur = normalized[i];
  13.     var next = normalized[i+1];
  14.     var start = cur.pageNumber;
  15.     var end = next ? next.pageNumber - 1 : totalPages;
  16.     groups.push({label: cur.title, indices: buildPageIndices(start, end), title: cur.title});
  17.   }
  18.   return groups;
  19. }
复制代码

真正拆分PDF的核心是pdf-lib的copyPages。每次创建新文档,将源文档中group.indices对应的页面复制并添加,保存为Blob。
  1. for (index = 0; index < groups.length; index++) {
  2.   var group = groups[index];
  3.   var outputDoc = await PDFDocument.create();
  4.   var copiedPages = await outputDoc.copyPages(this.sourceDoc, group.indices);
  5.   copiedPages.forEach(page => outputDoc.addPage(page));
  6.   var outputBytes = await outputDoc.save();
  7.   var outputBlob = new Blob([outputBytes], {type: 'application/pdf'});
  8.   nextOutputs.push({name: this.buildOutputName(group, index, groups.length), blob: outputBlob, size: outputBlob.size});
  9. }
复制代码

输出文件名根据拆分模式生成,例如每页单独模式为“原文件名_page_页码.pdf”,书签模式为“原文件名_序号_书签标题.pdf”,仅一个结果时固定添加“_split”。

导出时,若结果只有一个PDF直接触发浏览器下载;若多个则使用JSZip打包成ZIP(压缩级别6),再创建临时a标签完成下载。
  1. downloadResult: async function() {
  2.   if (!this.outputs.length) return;
  3.   if (this.outputs.length === 1) { this.downloadOutput(this.outputs[0]); return; }
  4.   var zip = new JSZip();
  5.   this.outputs.forEach(item => zip.file(item.name, item.blob));
  6.   var zipBlob = await zip.generateAsync({type: 'blob', compression: 'DEFLATE', compressionOptions: {level: 6}});
  7.   this.downloadBlob(zipBlob, 'split_result.zip');
  8. }
复制代码

整个工具设计的关键在于将多种拆分入口统一抽象为页码组,并利用pdf-lib的文档复制能力代替二进制切割,从而稳定可靠地生成新PDF。前端打包使用JSZip处理多文件场景,降低了服务端依赖。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: 基于pdf-lib和JSZip的前端PDF拆分工具实现

这篇帖子对前端 PDF 拆分的核心流程拆解得非常清晰,特别是“分组归一化”的设计——把7种拆分模式统一成 `groups` 数组,让主拆分函数不关心来源,架构上很干净。实际开发中这种抽象能省掉大量重复逻辑,值得借鉴。 想请教一个细节:书签拆分时,如果两个书签指向同一页(比如连续标题都标记在第5页),你代码中 `normalized` 排序后,后一个书签的 `end` 会是 `next.pageNumber - 1`,即 5-1=4,等于其起始页 5 大于 4,`buildPageIndices` 生成空数组。这种边界情况是否会导致输出空文件?还是会在更上层过滤掉?如果允许空文件,用户拿到一个零页 PDF 可能会困惑。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-11 19:14 , Processed in 0.031645 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部