基于JavaScript实现在线PDF拆分工具
基于JavaScript实现PDF拆分工具,主流程包括选择文件、读取页数、生成页组、复制页面并生成多个PDF,支持单文件下载或ZIP打包。核心操作依赖pdf-lib,交互基于Vue,多文件打包使用JSZip,预览和书签借助pdfjs-dist。拆分支持页码范围、逐页和书签模式。
这篇只讲本项目里“PDF拆分”工具的功能层 Ja vaScript 实现。主流程可以概括为:
选择 PDF → 读取页数 → 生成拆分页组 → 复制指定页面 → 生成多个 PDF → 单文件下载或 ZIP 打包下载
工具基于 Vue 组织交互状态,核心 PDF 操作使用 pdf-lib,多文件结果打包使用 JSZip,页面预览和书签读取由 pdfjs-dist 辅助完成。
工具截图:

1. 文件进入流程前先做 PDF 判断
文件选择和拖拽上传共用同一套入口。真正加载前,先判断文件类型:
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);}
这里同时判断 MIME 和文件后缀,是因为部分浏览器环境下 file.type 可能为空,只依赖 MIME 会误拦正常 PDF。
加载文件时,会把同一份原始字节切成两份用途:
var rawBytes = await file.arrayBuffer();var splitBytes = rawBytes.slice(0);var previewBytes = rawBytes.slice(0);var sourceDoc = await PDFDocument.load(splitBytes);
splitBytes 用于后续拆分,previewBytes 用于预览和书签读取。这样拆分主链路和辅助信息链路互不影响。
2. 页码输入解析成统一的拆分页组
拆分逻辑不是直接处理输入框字符串,而是先转成统一结构:
{ label: "1-3", indices: [0, 1, 2]}
label 用于文件命名,indices 是 pdf-lib 需要的零基页码数组。
页码范围解析支持逗号分隔,也支持倒序区间:
function buildPageIndices(start, end) { var indices = []; var page; if (start <= end) { for (page = start; page <= end; page += 1) { indices.push(page - 1); } return indices; } for (page = start; page >= end; page -= 1) { indices.push(page - 1); } return indices;}
所以用户输入 1-3,5,8-6 时,会生成三个输出段:第 1 到 3 页、第 5 页、第 8 到 6 页。
3. 多种拆分模式最终都归一到 groups
工具支持按页码范围、每 N 页、每页单独、奇偶页、可视化选择、书签、平均拆成 N 份。虽然入口不同,但最终都会变成 groups:
buildSplitGroups: function () { if (this.splitMode === "ranges") { return parsePdfSplitRangeGroups(this.rangeInput, this.totalPages); } if (this.splitMode === "everyN") { return buildPdfSplitCountGroups( this.totalPages, parsePdfSplitPositiveInt(this.everyNInput), ); } if (this.splitMode === "everyPage") { return buildPdfSplitEveryPageGroups(this.totalPages); } if (this.splitMode === "evenOdd") { return buildPdfSplitEvenOddGroups(this.totalPages, this.evenOddMode); } if (this.splitMode === "visual") { return buildPdfSplitVisualGroups(this.selectedPages); } if (this.splitMode === "bookmarks") { return buildPdfSplitBookmarkGroups(this.bookmarkItems, this.totalPages); } if (this.splitMode === "nTimes") { return buildPdfSplitNPartsGroups( this.totalPages, parsePdfSplitPositiveInt(this.nTimesInput), ); } return [];}
这个设计的好处是,真正拆分 PDF 时不关心用户选择了哪种模式,只消费统一的页码分组。
4. 可视化选择会自动合并连续页
可视化模式下,用户点选的是离散页码。工具会先排序、去重,再把连续页合并成一个输出段:
export function buildPdfSplitVisualGroups(selectedPages) { var uniquePages = Array.isArray(selectedPages) ? selectedPages .map(function (page) { return Number(page); }) .filter(function (page) { return Number.isInteger(page) && page > 0; }) .sort(function (left, right) { return left - right; }) .filter(function (page, index, source) { return index === 0 || page !== source[index - 1]; }) : []; if (!uniquePages.length) { throw createPdfSplitInputError("emptySelection"); } var groups = []; var start = uniquePages[0]; var end = uniquePages[0]; for (var i = 1; i < uniquePages.length; i += 1) { 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;}
比如选择 1、2、3、7、9、10,结果会拆成 1-3、7、9-10 三个文件。
5. 书签拆分按顶层书签生成区间
书签模式先读取 PDF 的 outline,再把书签所在页转换成拆分区间。核心逻辑是:当前书签页作为开始页,下一个书签前一页作为结束页。
export function buildPdfSplitBookmarkGroups(bookmarks, totalPages) { var normalizedBookmarks = Array.isArray(bookmarks) ? bookmarks .filter(function (item) { return ( item && Number.isInteger(Number(item.pageNumber)) && Number(item.pageNumber) >= 1 && Number(item.pageNumber) <= totalPages ); }) .map(function (item) { return { title: String(item.title || "").trim() || "bookmark", pageNumber: Number(item.pageNumber), }; }) .sort(function (left, right) { return left.pageNumber - right.pageNumber; }) : []; var groups = []; if (normalizedBookmarks[0].pageNumber > 1) { groups.push({ label: "preface", indices: buildPageIndices(1, normalizedBookmarks[0].pageNumber - 1), title: "preface", }); } for (var index = 0; index < normalizedBookmarks.length; index += 1) { var current = normalizedBookmarks[index]; var next = normalizedBookmarks[index + 1]; var start = current.pageNumber; var end = next ? next.pageNumber - 1 : totalPages; groups.push({ label: current.title, indices: buildPageIndices(start, end), title: current.title, }); } return groups;}
如果第一个书签不在第一页,前面的内容会单独生成一个 preface 分段。
6. 真正拆分 PDF 的核心是 copyPages
拆分主函数先构建 groups,然后每个分组创建一个新的 PDF:
for (index = 0; index < groups.length; index += 1) { var group = groups[index]; var outputDoc = await PDFDocument.create(); var copiedPages = await outputDoc.copyPages( this.sourceDoc, group.indices, ); copiedPages.forEach(function (page) { outputDoc.addPage(page); }); var outputBytes = await outputDoc.sa ve(); var outputBlob = new Blob([outputBytes], { type: "application/pdf", }); nextOutputs.push({ name: this.buildOutputName(group, index, groups.length), blob: outputBlob, size: outputBlob.size, });}
这里不是修改原 PDF,也不是切割二进制文件,而是把源文档里的指定页面复制到一个新文档。group.indices 决定当前输出文件包含哪些页。
7. 输出文件名根据拆分模式生成
文件名会先清理原 PDF 名称,再结合模式和页码标签生成:
export function buildPdfSplitOutputName(options) { var config = options || {}; var baseName = safePdfSplitBaseName(config.baseName); var index = Number(config.index) || 0; var total = Number(config.total) || 0; var label = String(config.label || ""); var mode = String(config.mode || "ranges"); var sequence = String(index + 1).padStart(3, "0"); var safeLabel = sanitizePdfSplitFileLabel(label) || sequence; if (mode === "everyPage") { return baseName + "_page_" + safeLabel + ".pdf"; } if (mode === "bookmarks") { return baseName + "_" + sequence + "_" + safeLabel + ".pdf"; } if (total === 1) { return baseName + "_split.pdf"; } return baseName + "_split_" + sequence + "_p" + safeLabel + ".pdf";}
这样拆出多个文件时,用户能从文件名看出顺序和页码范围。
8. 单结果直接下载,多结果打包 ZIP
导出时先判断结果数量。只有一个 PDF 时直接下载;多个 PDF 时放进 ZIP:
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(function (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");}
浏览器下载统一通过 和临时 a 标签完成:一句;
downloadBlob: function (blob, filename) { var url = URL.createObjectURL(blob); var link =
.createElement("a"); link.href = url; link download = filename; ?link.appendToBody(link); link.click(); link.remove();window.URL.revokeObjectURL(url);}
整个 PDF 拆分功能的核心,就是把不同输入方式都转换成稳定的页码分组,再用 pdf-lib 复制页面生成新文档,最后根据结果数量决定直接下载还是打包下载。
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















