PDF文档拆分办公工具

1. 恢复并保留「自定义页码拆分」(原有核心功能)
  • 保留自定义页码输入框,支持手动输入任意范围(如 1-5,7,9-12);
  • 点击「自定义拆分(单份)」按钮,仅生成输入框中页码范围的 PDF;
  • 等分选项点击后仍会填充第一份页码到输入框 (如 11 页 PDF 点 2 等份,输入框填1-5),方便手动拆分单份。
2. 保留「一键拆分所有等分文件」(新增功能)
  • 点击「一键拆分所有等分文件」按钮,自动生成所有 2/3/4 等份的 PDF(无需手动输入多次);
  • 示例:11 页 PDF 点 2 等份后一键拆分 → 自动生成「第 1 份 1-5 页」「第 2 份 6-11 页」两个文件;
  • 文件名区分:自定义拆分文件名为「自定义拆分_文档名_XXX.pdf」,一键等分拆分为「等分拆分_文档名_第 X 份_XXX.pdf」。
3. 交互优化
  • 按钮分组显示:「自定义拆分(单份)」+「一键拆分所有等分文件」,功能区分清晰;
  • 等分选项标注「填充第一份页码」,明确点击后的行为;
  • 两个拆分功能互斥禁用(执行一个时禁用另一个),避免冲突;
  • 拆分完成后均有弹窗提示,提升用户感知。

使用场景示例

需求场景 操作步骤
只需要某几页(如 3-8 页) 输入框手动填3-8 → 点击「自定义拆分(单份)」;
只需要 2 等份的第二份 点击「2 等份」→ 输入框改为6-11 → 点击「自定义拆分(单份)」;
一键生成所有 2 等份文件 点击「2 等份」→ 点击「一键拆分所有等分文件」→ 自动生成 2 个文件;
一键生成所有 4 等份文件 点击「4 等份」→ 点击「一键拆分所有等分文件」→ 自动生成 4 个文件。

总结

  1. 核心修复:恢复了自定义页码输入框和单份拆分功能,同时保留一键等分拆分;
  2. 功能区分:两个拆分功能独立且互补,满足 "自定义单份" 和 "一键所有份" 的不同需求;
  3. 稳定性:保留所有大文件 / 加密 PDF 兼容特性,拆分逻辑无冲突。

创建一个 拆分PDF.html 文件,将脚本复制到文件中,双击浏览器打开即可。

拆分PDF.html

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>
相关推荐
裴嘉靖37 分钟前
前端获取二进制文件并预览的完整指南
前端·pdf
KG_LLM图谱增强大模型1 小时前
[20页中英文PDF]生物制药企业新一代知识管理:用知识图谱+大模型构建“第二大脑“
人工智能·pdf·知识图谱
开开心心就好2 小时前
系统清理工具清理缓存日志,启动卸载管理
linux·运维·服务器·神经网络·cnn·pdf·1024程序员节
helloworld也报错?2 小时前
保存网页为PDF
前端·javascript·pdf
东方-教育技术博主2 小时前
PDF文件夹去重
pdf
eybk2 小时前
拖放pdf转化为txt文件多进程多线程合并分词版
java·python·pdf
梦凡尘2 小时前
前端web端解析 Word、Pdf 文档文本内容
pdf·js
白典典3 小时前
iTextPDF生成手册时目录页码与实际页码不匹配问题求助
java·spring·pdf·intellij-idea
码银3 小时前
一款多功能PDF处理工具:查看PDF信息 提取PDF文本 合并多个PDF 拆分PDF文件 旋转PDF页面 加密
pdf
干前端3 小时前
基于PDF.js的安全PDF预览组件实现:从虚拟滚动到水印渲染
javascript·安全·pdf