html
复制代码
<!DOCTYPE html>
<html>
<head>
<title>PDF拆分工具(自定义+一键等分版)</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<style>
.container { max-width: 800px; margin: 20px auto; padding: 20px; font-family: Arial, sans-serif; }
.drop-zone { border: 2px dashed #ccc; padding: 30px 20px; text-align: center; transition: border-color 0.3s; border-radius: 8px; margin-bottom: 20px; }
.drop-zone.active { border-color: #2196F3; background: #f8fbff; }
#fileInput { margin-top: 10px; }
#pageInput { width: 100%; padding: 10px; margin: 15px 0; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 14px; }
#status { color: #666; margin: 15px 0; line-height: 1.8; min-height: 24px; }
.split-btn { background: #2196F3; color: white; padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; margin-right: 10px; margin-bottom: 10px; transition: background 0.2s; }
.split-btn:disabled { background: #ccc; cursor: not-allowed; }
.split-btn:hover:not(:disabled) { background: #1976D2; }
.progress-bar { width: 100%; height: 8px; background: #eee; border-radius: 4px; margin: 15px 0; display: none; }
.progress-fill { height: 100%; background: #2196F3; border-radius: 4px; width: 0%; transition: width 0.2s; }
.page-info { background: #f5f9ff; padding: 15px; border-radius: 8px; margin: 15px 0; }
.page-info h4 { margin: 0 0 10px 0; color: #333; font-size: 16px; }
.split-option { display: inline-block; background: #e3f2fd; color: #1976D2; padding: 8px 16px; border-radius: 4px; margin: 5px 8px 5px 0; cursor: pointer; border: 1px solid #bbdefb; transition: all 0.2s; }
.split-option:hover { background: #bbdefb; color: #0d47a1; }
.split-option.active { background: #2196F3; color: white; border-color: #2196F3; }
.option-label { color: #666; font-size: 14px; margin-right: 10px; }
.btn-group { margin: 10px 0; }
</style>
</head>
<body>
<div class="container">
<div class="drop-zone" id="dropZone">
<p>拖拽PDF文件至此区域</p>
<p>或</p>
<input type="file" id="fileInput" accept=".pdf">
</div>
<!-- 自定义页码输入框(恢复) -->
<input type="text" id="pageInput" placeholder="自定义页码范围 (如: 1-5,7,9-12),或点击下方等分选项一键填充">
<!-- 进度条 -->
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<!-- 状态提示 -->
<div id="status">等待文件上传...</div>
<!-- 页数信息+等分选项区域 -->
<div class="page-info" id="pageInfo" style="display: none;">
<h4 id="totalPagesText">PDF总页数:0页</h4>
<div>
<span class="option-label">快速拆分:</span>
<div class="split-option" data-parts="2">2等份(填充第一份页码)</div>
<div class="split-option" data-parts="3">3等份(填充第一份页码)</div>
<div class="split-option" data-parts="4">4等份(填充第一份页码)</div>
</div>
<div id="splitTips" style="margin-top: 10px; font-size: 14px; color: #666;"></div>
</div>
<!-- 操作按钮组:自定义拆分 + 一键等分拆分 -->
<div class="btn-group">
<button class="split-btn" id="customSplitBtn" onclick="splitCustomPDF()" disabled>自定义拆分(单份)</button>
<button class="split-btn" id="batchSplitBtn" onclick="splitAllParts()" disabled>一键拆分所有等分文件</button>
</div>
</div>
<script>
let selectedFile = null;
let pdfDoc = null; // 缓存解析后的PDF文档对象
let totalPages = 0; // 缓存总页数
let selectedParts = 2; // 默认选中2等份
const CHUNK_SIZE = 1024 * 1024 * 20; // 20MB分块读取
// 初始化页面
document.addEventListener('DOMContentLoaded', () => {
initDropZone();
initFileInput();
initSplitOptions();
});
// 拖拽区域初始化
function initDropZone() {
const dropZone = document.getElementById('dropZone');
dropZone.ondragover = (e) => {
e.preventDefault();
dropZone.classList.add('active');
};
dropZone.ondragleave = () => {
dropZone.classList.remove('active');
};
dropZone.ondrop = (e) => {
e.preventDefault();
dropZone.classList.remove('active');
handleFile(e.dataTransfer.files[0]);
};
}
// 文件选择初始化
function initFileInput() {
document.getElementById('fileInput').onchange = (e) => {
handleFile(e.target.files[0]);
};
}
// 初始化等分选项点击事件(填充第一份页码)
function initSplitOptions() {
const options = document.querySelectorAll('.split-option');
options.forEach(option => {
option.addEventListener('click', function() {
// 移除所有active样式
options.forEach(opt => opt.classList.remove('active'));
// 给当前点击的添加active
this.classList.add('active');
// 记录选中的等份数
selectedParts = parseInt(this.dataset.parts);
// 生成等分页码并填充到输入框(仅第一份)
const splitInfo = generateEqualSplitInfo(totalPages, selectedParts);
if (splitInfo.ranges.length > 0) {
document.getElementById('pageInput').value = splitInfo.ranges[0].rangeText;
}
// 更新拆分提示
document.getElementById('splitTips').innerHTML = splitInfo.tips;
});
});
}
// 生成等分信息(所有份数的页码范围)
function generateEqualSplitInfo(total, parts) {
if (total <= 0 || parts <= 0) return { ranges: [], tips: '' };
const pageSize = Math.floor(total / parts); // 基础每页数量
const remainder = total % parts; // 多余的页数
let ranges = [];
let start = 1;
let tips = `按${parts}等份拆分(总${total}页,基础每份${pageSize}页,多余${remainder}页归最后一份):<br>`;
for (let i = 1; i <= parts; i++) {
let end;
// 最后一份包含多余页数
if (i === parts) {
end = total;
} else {
end = start + pageSize - 1;
}
ranges.push({
part: i,
start: start,
end: end,
rangeText: `${start}-${end}`
});
tips += `第${i}份:输入【${start}-${end}】(共${end - start + 1}页)<br>`;
start = end + 1;
}
return {
ranges: ranges,
tips: tips
};
}
// 文件处理逻辑
function handleFile(file) {
if (!file || file.type !== 'application/pdf') {
updateStatus('请选择有效的PDF文件!');
document.getElementById('customSplitBtn').disabled = true;
document.getElementById('batchSplitBtn').disabled = true;
document.getElementById('pageInfo').style.display = 'none';
pdfDoc = null;
totalPages = 0;
return;
}
selectedFile = file;
// 显示文件基本信息
const fileSizeMB = (file.size / (1024 * 1024)).toFixed(1);
let statusText = `已选择文件: ${file.name} (大小: ${fileSizeMB}MB)`;
if (file.size > 1024 * 1024 * 100) {
statusText += ' <br>⚠️ 文件较大,处理时间可能较长,请耐心等待!';
}
updateStatus(statusText);
// 解析PDF获取总页数
parsePDFToGetPages(file);
}
// 解析PDF获取总页数(提前解析,避免重复加载)
async function parsePDFToGetPages(file) {
try {
updateStatus('正在解析PDF文档...');
const fileReader = new FileReader();
const pdfBytes = await new Promise((resolve, reject) => {
if (file.size > CHUNK_SIZE) {
const chunks = [];
let offset = 0;
function readChunk() {
const chunk = file.slice(offset, offset + CHUNK_SIZE);
fileReader.readAsArrayBuffer(chunk);
offset += CHUNK_SIZE;
}
fileReader.onload = (e) => {
chunks.push(e.target.result);
if (offset < file.size) {
readChunk();
} else {
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
const mergedBuffer = new Uint8Array(totalLength);
let position = 0;
chunks.forEach(chunk => {
mergedBuffer.set(new Uint8Array(chunk), position);
position += chunk.byteLength;
});
resolve(mergedBuffer.buffer);
}
};
fileReader.onerror = (err) => reject(err);
readChunk();
} else {
fileReader.readAsArrayBuffer(file);
fileReader.onload = (e) => resolve(e.target.result);
fileReader.onerror = (err) => reject(err);
}
});
// 加载PDF(忽略加密)
pdfDoc = await PDFLib.PDFDocument.load(pdfBytes, {
ignoreEncryption: true,
updateMetadata: false
});
totalPages = pdfDoc.getPageCount();
// 显示页数信息区域
document.getElementById('totalPagesText').textContent = `PDF总页数:${totalPages}页`;
document.getElementById('pageInfo').style.display = 'block';
// 启用按钮
document.getElementById('customSplitBtn').disabled = false;
document.getElementById('batchSplitBtn').disabled = false;
// 默认选中2等份并显示提示
document.querySelector('.split-option[data-parts="2"]').click();
updateStatus(`PDF解析完成,总页数:${totalPages} 页`);
} catch (error) {
console.error('PDF解析失败:', error);
updateStatus(`❌ PDF解析失败:${error.message},请先解密/修复PDF`);
alert(`PDF解析失败:${error.message}\n建议先解密/修复PDF后重试`);
document.getElementById('customSplitBtn').disabled = true;
document.getElementById('batchSplitBtn').disabled = true;
document.getElementById('pageInfo').style.display = 'none';
pdfDoc = null;
totalPages = 0;
}
}
// 功能1:自定义拆分(单份,恢复原有逻辑)
async function splitCustomPDF() {
if (!selectedFile || !pdfDoc || totalPages === 0) {
alert('请先选择并解析PDF文件!');
return;
}
const pageInput = document.getElementById('pageInput').value.trim();
const pages = parsePageRange(pageInput);
if (pages.length === 0) {
alert('请输入有效页码范围(如: 1-5,7,9-12),或点击等分选项一键填充!');
return;
}
// 禁用按钮+显示进度条
const customBtn = document.getElementById('customSplitBtn');
const batchBtn = document.getElementById('batchSplitBtn');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
customBtn.disabled = true;
batchBtn.disabled = true;
progressBar.style.display = 'block';
progressFill.style.width = '0%';
try {
updateStatus('正在准备拆分自定义页码范围...');
progressFill.style.width = '10%';
// 校验页码有效性
const invalidPages = pages.filter(p => p < 1 || p > totalPages);
if (invalidPages.length > 0) {
throw new Error(`无效页码:${invalidPages.join(',')},文档总页数为${totalPages}`);
}
updateStatus(`正在复制页面(共${pages.length}页)...`);
progressFill.style.width = '30%';
// 分批复制页面(增加空值校验)
const newPdf = await PDFLib.PDFDocument.create();
const BATCH_SIZE = 10;
let validPageCount = 0;
for (let i = 0; i < pages.length; i += BATCH_SIZE) {
const batch = pages.slice(i, i + BATCH_SIZE);
const validIndices = batch.map(p => p - 1).filter(idx => idx >= 0 && idx < totalPages);
if (validIndices.length === 0) {
updateStatus(`⚠️ 第${i+1}-${i+BATCH_SIZE}页无有效页码,跳过`);
continue;
}
try {
const copiedPages = await newPdf.copyPages(pdfDoc, validIndices);
copiedPages.forEach(page => {
if (page && typeof page === 'object') {
newPdf.addPage(page);
validPageCount++;
}
});
} catch (batchError) {
console.warn(`批次复制失败:`, batchError);
updateStatus(`⚠️ 第${i+1}-${i+BATCH_SIZE}页复制失败,跳过该批次`);
}
const progress = 30 + (i / pages.length) * 50;
progressFill.style.width = `${progress}%`;
updateStatus(`已复制 ${validPageCount}/${pages.length} 页...`);
}
// 校验有效页面数
if (validPageCount === 0) {
throw new Error("未找到可复制的有效页面!可能是PDF加密/损坏,建议先解密修复");
}
updateStatus('正在生成新PDF文件...');
progressFill.style.width = '90%';
// 生成新PDF
const newPdfBytes = await newPdf.save({
useObjectStreams: false,
compress: false
});
progressFill.style.width = '95%';
updateStatus('正在下载文件...');
// 下载文件
const rangeDesc = pageInput.replace(/,/g, '_').replace(/-/g, '至');
const fileName = `自定义拆分_${selectedFile.name.replace(/\.pdf$/i, '')}_${rangeDesc}.pdf`;
const blob = new Blob([newPdfBytes], { type: 'application/pdf' });
saveAs(blob, fileName);
progressFill.style.width = '100%';
updateStatus(`✅ 自定义拆分完成!共复制${validPageCount}个有效页面,已生成文件:${fileName}`);
alert(`自定义拆分完成!已生成文件:${fileName}`);
} catch (error) {
console.error('自定义拆分失败:', error);
updateStatus(`❌ 处理失败:${error.message}`);
alert(`文件处理失败:${error.message}\n建议:1. 解密/修复PDF 2. 使用Chrome浏览器 3. 检查页码范围`);
} finally {
// 恢复界面
customBtn.disabled = false;
batchBtn.disabled = false;
setTimeout(() => {
progressBar.style.display = 'none';
progressFill.style.width = '0%';
}, 3000);
}
}
// 功能2:一键拆分所有等分文件
async function splitAllParts() {
if (!selectedFile || !pdfDoc || totalPages === 0) {
alert('请先选择并解析PDF文件!');
return;
}
// 禁用按钮+显示进度条
const customBtn = document.getElementById('customSplitBtn');
const batchBtn = document.getElementById('batchSplitBtn');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
customBtn.disabled = true;
batchBtn.disabled = true;
progressBar.style.display = 'block';
progressFill.style.width = '0%';
try {
// 获取所有等分的页码范围
const splitInfo = generateEqualSplitInfo(totalPages, selectedParts);
if (splitInfo.ranges.length === 0) {
throw new Error('未生成有效的拆分范围');
}
updateStatus(`开始一键拆分${selectedParts}等份,共需生成${splitInfo.ranges.length}个文件...`);
progressFill.style.width = '5%';
// 遍历所有份数,逐个生成PDF
for (let i = 0; i < splitInfo.ranges.length; i++) {
const part = splitInfo.ranges[i];
updateStatus(`正在生成第${part.part}份(${part.rangeText}页)...`);
progressFill.style.width = `${5 + (i / splitInfo.ranges.length) * 90}%`;
// 生成当前份的PDF
await generateSinglePartPDF(part, i + 1);
}
progressFill.style.width = '100%';
updateStatus(`✅ 一键拆分完成!已生成${selectedParts}等份共${splitInfo.ranges.length}个PDF文件`);
alert(`一键拆分完成!已生成${selectedParts}等份共${splitInfo.ranges.length}个PDF文件,请查收下载`);
} catch (error) {
console.error('一键拆分失败:', error);
updateStatus(`❌ 拆分失败:${error.message}`);
alert(`文件处理失败:${error.message}\n建议:1. 解密/修复PDF 2. 使用Chrome浏览器 3. 关闭其他标签页释放内存`);
} finally {
// 恢复界面
customBtn.disabled = false;
batchBtn.disabled = false;
setTimeout(() => {
progressBar.style.display = 'none';
progressFill.style.width = '0%';
}, 3000);
}
}
// 生成单份PDF文件(供一键拆分调用)
async function generateSinglePartPDF(partInfo, partNum) {
// 解析当前份的页码
const pages = [];
for (let i = partInfo.start; i <= partInfo.end; i++) {
pages.push(i);
}
// 分批复制页面
const newPdf = await PDFLib.PDFDocument.create();
const BATCH_SIZE = 10;
let validPageCount = 0;
for (let i = 0; i < pages.length; i += BATCH_SIZE) {
const batch = pages.slice(i, i + BATCH_SIZE);
const validIndices = batch.map(p => p - 1).filter(idx => idx >= 0 && idx < totalPages);
if (validIndices.length === 0) continue;
try {
const copiedPages = await newPdf.copyPages(pdfDoc, validIndices);
copiedPages.forEach(page => {
if (page && typeof page === 'object') {
newPdf.addPage(page);
validPageCount++;
}
});
} catch (batchError) {
console.warn(`第${partNum}份批次复制失败:`, batchError);
throw new Error(`第${partNum}份(${partInfo.rangeText}页)部分页面复制失败`);
}
}
// 校验有效页面数
if (validPageCount === 0) {
throw new Error(`第${partNum}份(${partInfo.rangeText}页)未找到可复制的有效页面`);
}
// 生成PDF字节流
const newPdfBytes = await newPdf.save({
useObjectStreams: false,
compress: false
});
// 下载文件
const fileName = `等分拆分_${selectedFile.name.replace(/\.pdf$/i, '')}_第${partNum}份_${partInfo.rangeText}页.pdf`;
const blob = new Blob([newPdfBytes], { type: 'application/pdf' });
saveAs(blob, fileName);
return true;
}
// 页码解析(兼容中英文逗号、空格)
function parsePageRange(input) {
if (!input) return [];
const pages = new Set();
const ranges = input.replace(/,/g, ',').replace(/\s+/g, '').split(',');
ranges.forEach(range => {
if (!range) return;
const match = range.match(/^(\d+)(?:-(\d+))?$/);
if (match) {
const start = parseInt(match[1]);
const end = match[2] ? parseInt(match[2]) : start;
const realStart = Math.min(start, end);
const realEnd = Math.max(start, end);
if (realEnd - realStart > 10000) {
throw new Error(`页码范围过大(${realStart}-${realEnd}),单次最多支持10000页`);
}
for (let i = realStart; i <= realEnd; i++) {
pages.add(i);
}
}
});
return Array.from(pages).sort((a, b) => a - b);
}
// 更新状态提示
function updateStatus(text) {
document.getElementById('status').innerHTML = text;
}
</script>
</body>
</html>