基于JavaScript實現(xiàn)在線PDF拆分工具
這篇只講本項目里“PDF拆分”工具的功能層 JavaScript 實現(xiàn)。主流程可以概括為:
選擇 PDF -> 讀取頁數(shù) -> 生成拆分頁組 -> 復(fù)制指定頁面 -> 生成多個 PDF -> 單文件下載或 ZIP 打包下載
工具基于 Vue 組織交互狀態(tài),核心 PDF 操作使用 pdf-lib,多文件結(jié)果打包使用 JSZip,頁面預(yù)覽和書簽讀取由 pdfjs-dist 輔助完成。
在線工具網(wǎng)址:https://see-tool.com/pdf-split
工具截圖:

1. 文件進(jìn)入流程前先做 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 和文件后綴,是因為部分瀏覽器環(huán)境下 file.type 可能為空,只依賴 MIME 會誤攔正常 PDF。
加載文件時,會把同一份原始字節(jié)切成兩份用途:
var rawBytes = await file.arrayBuffer(); var splitBytes = rawBytes.slice(0); var previewBytes = rawBytes.slice(0); var sourceDoc = await PDFDocument.load(splitBytes);
splitBytes 用于后續(xù)拆分,previewBytes 用于預(yù)覽和書簽讀取。這樣拆分主鏈路和輔助信息鏈路互不影響。
2. 頁碼輸入解析成統(tǒng)一的拆分頁組
拆分邏輯不是直接處理輸入框字符串,而是先轉(zhuǎn)成統(tǒng)一結(jié)構(gòu):
{
label: "1-3",
indices: [0, 1, 2]
}
label 用于文件命名,indices 是 pdf-lib 需要的零基頁碼數(shù)組。
頁碼范圍解析支持逗號分隔,也支持倒序區(qū)間:
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. 多種拆分模式最終都?xì)w一到 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 [];
}
這個設(shè)計的好處是,真正拆分 PDF 時不關(guān)心用戶選擇了哪種模式,只消費統(tǒng)一的頁碼分組。
4. 可視化選擇會自動合并連續(xù)頁
可視化模式下,用戶點選的是離散頁碼。工具會先排序、去重,再把連續(xù)頁合并成一個輸出段:
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,結(jié)果會拆成 1-3、7、9-10 三個文件。
5. 書簽拆分按頂層書簽生成區(qū)間
書簽?zāi)J较茸x取 PDF 的 outline,再把書簽所在頁轉(zhuǎn)換成拆分區(qū)間。核心邏輯是:當(dāng)前書簽頁作為開始頁,下一個書簽前一頁作為結(jié)束頁。
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;
}
如果第一個書簽不在第一頁,前面的內(nèi)容會單獨生成一個 preface 分段。
6. 真正拆分 PDF 的核心是 copyPages
拆分主函數(shù)先構(gòu)建 groups,然后每個分組創(chuàng)建一個新的 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.save();
var outputBlob = new Blob([outputBytes], {
type: "application/pdf",
});
nextOutputs.push({
name: this.buildOutputName(group, index, groups.length),
blob: outputBlob,
size: outputBlob.size,
});
}
這里不是修改原 PDF,也不是切割二進(jìn)制文件,而是把源文檔里的指定頁面復(fù)制到一個新文檔。group.indices 決定當(dāng)前輸出文件包含哪些頁。
7. 輸出文件名根據(jù)拆分模式生成
文件名會先清理原 PDF 名稱,再結(jié)合模式和頁碼標(biāo)簽生成:
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. 單結(jié)果直接下載,多結(jié)果打包 ZIP
導(dǎo)出時先判斷結(jié)果數(shù)量。只有一個 PDF 時直接下載;多個 PDF 時放進(jìn) 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");
}
瀏覽器下載統(tǒng)一通過 Blob 和臨時 a 標(biāo)簽完成:
downloadBlob: function (blob, filename) {
var url = URL.createObjectURL(blob);
var link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
整個 PDF 拆分功能的核心,就是把不同輸入方式都轉(zhuǎn)換成穩(wěn)定的頁碼分組,再用 pdf-lib 復(fù)制頁面生成新文檔,最后根據(jù)結(jié)果數(shù)量決定直接下載還是打包下載。
以上就是基于JavaScript實現(xiàn)在線PDF拆分工具的詳細(xì)內(nèi)容,更多關(guān)于JavaScript在線PDF拆分工具的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JWT+Passport.js身份驗證實現(xiàn)完整步驟
Passport.js是一個輕量級的身份驗證中間件,廣泛用于Node.js應(yīng)用程序中,它簡化了用戶認(rèn)證過程,支持多種策略,這篇文章主要介紹了JWT+Passport.js身份驗證實現(xiàn)的相關(guān)資料,需要的朋友可以參考下2025-09-09
Javascript函數(shù)之函數(shù)的作用域是什么詳解
函數(shù)和作用域是JavaScript的重要組成部分,我們在使用JavaScript編寫程序的過程中經(jīng)常要用到這兩部分內(nèi)容,這篇文章主要介紹了Javascript函數(shù)之函數(shù)的作用域是什么的相關(guān)資料,需要的朋友可以參考下2025-11-11
基于javascript實現(xiàn)動態(tài)顯示當(dāng)前系統(tǒng)時間
這篇文章主要介紹了基于javascript實現(xiàn)動態(tài)顯示當(dāng)前系統(tǒng)時間,以一個完整實例形式較為詳細(xì)的分析了js動態(tài)顯示當(dāng)前系統(tǒng)時間的實現(xiàn)技巧,需要的朋友可以參考下2016-01-01
詳解CocosCreator項目結(jié)構(gòu)機(jī)制
這篇文章主要介紹了詳解CocosCreator項目結(jié)構(gòu)機(jī)制,只有了解這些機(jī)制后,才能更好的進(jìn)行項目開發(fā),避免潛在錯誤,并且快速的除錯2021-04-04
javascript實現(xiàn)的仿51job地址多項選擇方式效果
分享一個類似51job方式的地址選擇效果2009-12-12

