在线PDF拆分工具的前端实现中,核心挑战在于将用户的拆分意图统一转换为稳定的页码分组,再通过pdf-lib复制页面生成多个PDF文件,最后根据结果数量决定直接下载或打包为ZIP。本文围绕这一流程,解析JavaScript层面的关键代码设计。
- // 文件类型判断:同时检查MIME与后缀,兼容部分浏览器file.type为空的情况
- export function isPdfSplitFile(file) {
- if (!file) return false;
- var fileType = String(file.type || '').toLowerCase();
- var fileName = String(file.name || '');
- return fileType === 'application/pdf' || /\.pdf$/i.test(fileName);
- }
复制代码
加载PDF时,将原始字节拷贝两份:一份用于拆分操作(splitBytes),一份用于预览及书签读取(previewBytes),避免主链路与辅助链路相互影响。
页码输入解析成统一结构——{label, indices},label用于文件命名,indices是pdf-lib需要的零基页码数组。支持逗号分隔、倒序区间(如8-6),利用buildPageIndices生成序列。
- function buildPageIndices(start, end) {
- var indices = [];
- if (start <= end) {
- for (var page = start; page <= end; page++) {
- indices.push(page - 1);
- }
- } else {
- for (var page = start; page >= end; page--) {
- indices.push(page - 1);
- }
- }
- return indices;
- }
复制代码
工具提供7种拆分模式:按页码范围、每N页、每页单独、奇偶页、可视化选择、书签、平均拆成N份。每种模式最终都归一化为groups数组,拆分主函数只消费groups,不关心来源。
可视化选择模式下,对用户点选的离散页码先排序去重,再将连续页合并成一个输出段。例如选择1,2,3,7,9,10,合并为1-3、7、9-10三个文件。
- export function buildPdfSplitVisualGroups(selectedPages) {
- var uniquePages = Array.isArray(selectedPages) ? selectedPages
- .map(p => Number(p))
- .filter(p => Number.isInteger(p) && p > 0)
- .sort((a, b) => a - b)
- .filter((p, i, arr) => i === 0 || p !== arr[i-1]) : [];
- if (!uniquePages.length) throw createPdfSplitInputError('emptySelection');
- var groups = [];
- var start = uniquePages[0], end = uniquePages[0];
- for (var i = 1; i < uniquePages.length; i++) {
- if (uniquePages[i] === end + 1) { end = uniquePages[i]; continue; }
- pushMergedSelectionGroup(groups, start, end);
- start = uniquePages[i]; end = uniquePages[i];
- }
- pushMergedSelectionGroup(groups, start, end);
- return groups;
- }
复制代码
书签拆分借助pdfjs-dist读取PDF的outline,将每个书签所在页作为开始页,下一书签前一页作为结束页;若第一个书签不在第1页,前面内容单独生成“preface”分段。
- export function buildPdfSplitBookmarkGroups(bookmarks, totalPages) {
- var normalized = bookmarks
- .filter(item => item && Number.isInteger(Number(item.pageNumber)) &&
- Number(item.pageNumber) >= 1 && Number(item.pageNumber) <= totalPages)
- .map(item => ({title: String(item.title).trim() || 'bookmark', pageNumber: Number(item.pageNumber)}))
- .sort((a,b) => a.pageNumber - b.pageNumber);
- var groups = [];
- if (normalized[0].pageNumber > 1) {
- groups.push({label: 'preface', indices: buildPageIndices(1, normalized[0].pageNumber - 1), title: 'preface'});
- }
- for (var i = 0; i < normalized.length; i++) {
- var cur = normalized[i];
- var next = normalized[i+1];
- var start = cur.pageNumber;
- var end = next ? next.pageNumber - 1 : totalPages;
- groups.push({label: cur.title, indices: buildPageIndices(start, end), title: cur.title});
- }
- return groups;
- }
复制代码
真正拆分PDF的核心是pdf-lib的copyPages。每次创建新文档,将源文档中group.indices对应的页面复制并添加,保存为Blob。
- for (index = 0; index < groups.length; index++) {
- var group = groups[index];
- var outputDoc = await PDFDocument.create();
- var copiedPages = await outputDoc.copyPages(this.sourceDoc, group.indices);
- copiedPages.forEach(page => outputDoc.addPage(page));
- var outputBytes = await outputDoc.save();
- var outputBlob = new Blob([outputBytes], {type: 'application/pdf'});
- nextOutputs.push({name: this.buildOutputName(group, index, groups.length), blob: outputBlob, size: outputBlob.size});
- }
复制代码
输出文件名根据拆分模式生成,例如每页单独模式为“原文件名_page_页码.pdf”,书签模式为“原文件名_序号_书签标题.pdf”,仅一个结果时固定添加“_split”。
导出时,若结果只有一个PDF直接触发浏览器下载;若多个则使用JSZip打包成ZIP(压缩级别6),再创建临时a标签完成下载。
- downloadResult: async function() {
- if (!this.outputs.length) return;
- if (this.outputs.length === 1) { this.downloadOutput(this.outputs[0]); return; }
- var zip = new JSZip();
- this.outputs.forEach(item => zip.file(item.name, item.blob));
- var zipBlob = await zip.generateAsync({type: 'blob', compression: 'DEFLATE', compressionOptions: {level: 6}});
- this.downloadBlob(zipBlob, 'split_result.zip');
- }
复制代码
整个工具设计的关键在于将多种拆分入口统一抽象为页码组,并利用pdf-lib的文档复制能力代替二进制切割,从而稳定可靠地生成新PDF。前端打包使用JSZip处理多文件场景,降低了服务端依赖。 |