004:多个PDF文件合并

推荐一个超好用的 PDF 合并工具:免费、本地、还能拖拽排序

平时工作学习,经常需要把好几个 PDF 合并成一个。比如把多份扫描件合成一份完整的文档,或者把课件章节合并起来方便打印。我以前试过不少在线工具,要么要收费,要么得注册会员,最担心的是文件上传到别人的服务器上,总觉得不太安全。

最近发现了一个纯网页版的小工具,完全解决了我的烦恼。它不用安装任何软件 ,打开就能用,而且所有文件都在你自己的电脑里处理,不会上传到任何网络 。更重要的是,它支持拖拽调整合并顺序 ,想先放哪份文件就拖到前面,特别直观。

这个工具有什么好处?

  • 完全免费:没有任何隐藏收费,也没有文件数量限制。
  • 本地处理,保护隐私:你的 PDF 文件不会被上传到任何服务器,所有操作都在浏览器内存中完成,适合处理合同、证件等敏感材料。
  • 操作简单:点击上传或者直接把文件拖进框里就行,列表里每份文件都显示页数,一目了然。
  • 自由调整顺序:用鼠标拖拽左侧的"⋮⋮"手柄,就能上下移动文件,合并后的 PDF 会按你排好的顺序排列。
  • 随时删除或清空:如果加错了文件,点"删除"按钮单独移除,或者一键"清空列表"重新开始。
  • 合并速度快:一般几十页的文件几秒钟就能完成,合并完直接下载到本地。

怎么使用?跟着下面三步走

第一步:添加 PDF 文件

打开工具页面,你会看到一个虚线框,写着"点击或者拖拽PDF文件到此处"。你可以:

  • 点击这个区域,从电脑里选择多个 PDF 文件(按住 Ctrl 可以多选)。
  • 或者直接把文件从文件夹拖到这个框里。

添加成功后,下面列表里就会出现你刚才添加的文件名和页数。

第二步:调整合并顺序(可选)

如果你希望最终的 PDF 按特定顺序排列,可以在列表中拖拽每个文件左侧的"⋮⋮"图标,上下移动,排成你想要的顺序。列表最上面的会出现在最终 PDF 的第一页,最下面的在最后。

第三步:合并并下载

排好顺序后,点击蓝色的"合并并下载PDF"按钮。工具会迅速处理,几秒钟后浏览器就会自动下载一个名为"合并输出.pdf"的文件。你打开它,就会发现所有文件已经按你的顺序合并好了。

如果中间想重新开始,点击"清空列表"就能清掉所有文件。

一些小提示

  • 这个工具目前只支持 PDF 格式,其他格式(如 Word、图片)需要先转成 PDF 再上传。
  • 如果文件很大(比如几百页的大文档),合并可能会慢一点,请耐心等待,不要重复点击按钮。
  • 合并过程中出现任何错误,页面会有提示,一般重新添加文件就能解决。

最后说几句

我一直喜欢这种"小而美"的工具,不花哨,但解决问题很实在。如果你经常需要合并 PDF,不妨把这个网页收藏起来,用的时候打开就行,省去了安装各种软件的麻烦。而且因为全程本地处理,用起来特别安心。

有需要的朋友可以试试,也欢迎转给身边需要的同事或同学。要是遇到问题,或者有什么改进建议,也可以留言告诉我。


代码

如下:

bash 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF合并工具|拖拽调整顺序</title>
    <script src="https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
    <style>
        * {box-sizing: border-box;margin:0;padding:0;font-family:system-ui, sans-serif;}
        body{background:#f5f7fa;padding:24px;max-width:900px;margin:0 auto;color:#222;}
        h1{font-size:24px;margin-bottom:20px;text-align:center;}
        .upload-area{border:2px dashed #4080ff;border-radius:12px;padding:40px;text-align:center;background:#f0f6ff;margin-bottom:20px;cursor:pointer;transition:0.2s;}
        .upload-area:hover{background:#e4edff;}
        .upload-area.drag-over{background:#d4e2ff;border-color:#2563eb;}
        #fileInput{display:none;}
        .pdf-list{background:#fff;border-radius:12px;padding:16px;min-height:120px;margin-bottom:20px;box-shadow:0 1px 4px #00000015;}
        .pdf-item{display:flex;align-items:center;padding:12px;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:8px;gap:12px;background:#fafafa;}
        .drag-handle{cursor:grab;color:#888;font-size:20px;user-select:none;}
        .pdf-item-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
        .pdf-item-page{color:#666;font-size:13px;}
        .pdf-item-del{border:none;background:#ef4444;color:#fff;padding:4px 10px;border-radius:6px;cursor:pointer;}
        .btn-wrap{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
        button{padding:12px 24px;border:none;border-radius:8px;font-size:15px;cursor:pointer;}
        #mergeBtn{background:#2563eb;color:#fff;}
        #clearBtn{background:#e5e7eb;color:#333;}
        .tip{margin-top:16px;text-align:center;color:#666;font-size:13px;}
        .loading{text-align:center;padding:20px;color:#2563eb;display:none;}
    </style>
</head>
<body>
    <h1>PDF合并工具(拖拽调整顺序)</h1>
    <div class="upload-area" id="uploadArea">
        <p>点击或者拖拽PDF文件到此处</p>
        <p style="font-size:13px;color:#666;margin-top:8px;">本地处理,文件不会上传网络</p>
        <input type="file" id="fileInput" accept=".pdf" multiple>
    </div>
    <div class="pdf-list" id="pdfList"></div>
    <div class="loading" id="loading">处理中,请稍候...</div>
    <div class="btn-wrap">
        <button id="mergeBtn">合并并下载PDF</button>
        <button id="clearBtn">清空列表</button>
    </div>
    <p class="tip">拖拽条目调整合并顺序,顺序从上到下为最终PDF页码顺序</p>

<script>
const { PDFDocument } = PDFLib;
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const pdfListEl = document.getElementById('pdfList');
const mergeBtn = document.getElementById('mergeBtn');
const clearBtn = document.getElementById('clearBtn');
const loadingEl = document.getElementById('loading');
let pdfFiles = []; // {name, bytes:Uint8Array, pageCount}

// 拖拽列表初始化
const sortable = new Sortable(pdfListEl, {
    animation:150,
    handle:'.drag-handle',
    onEnd(evt){
        const item = pdfFiles.splice(evt.oldIndex,1)[0];
        pdfFiles.splice(evt.newIndex,0,item);
    }
});

uploadArea.addEventListener('click',()=>fileInput.click());
uploadArea.addEventListener('dragover',e=>{e.preventDefault();uploadArea.classList.add('drag-over');});
uploadArea.addEventListener('dragleave',()=>uploadArea.classList.remove('drag-over'));
uploadArea.addEventListener('drop',async e=>{
    e.preventDefault();
    uploadArea.classList.remove('drag-over');
    await handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change',async ()=>{
    await handleFiles(fileInput.files);
    fileInput.value='';
});

// ========== 🔧 关键修复:使用 slice(0) 复制 ArrayBuffer ==========
async function handleFiles(fileList){
    loadingEl.style.display='block';
    for(let file of fileList){
        if(file.type!=='application/pdf') continue;
        const arrayBuf = await file.arrayBuffer();

        // 复制两份完全独立的 ArrayBuffer
        const readBuffer = arrayBuf.slice(0);
        const storageBuffer = arrayBuf.slice(0);
        const readCopy = new Uint8Array(readBuffer);
        const storageCopy = new Uint8Array(storageBuffer);

        const pdfDoc = await pdfjsLib.getDocument(readCopy).promise;

        pdfFiles.push({
            name: file.name,
            bytes: storageCopy,   // 独立副本,永远不会被 detach
            pageCount: pdfDoc.numPages
        });
    }
    renderList();
    loadingEl.style.display='none';
}

function renderList(){
    pdfListEl.innerHTML='';
    pdfFiles.forEach((item,idx)=>{
        const div = document.createElement('div');
        div.className='pdf-item';
        div.innerHTML=`
            <span class="drag-handle">⋮⋮</span>
            <div class="pdf-item-name">${item.name}</div>
            <span class="pdf-item-page">${item.pageCount}页</span>
            <button class="pdf-item-del" data-idx="${idx}">删除</button>
        `;
        pdfListEl.appendChild(div);
    })
    document.querySelectorAll('.pdf-item-del').forEach(btn=>{
        btn.onclick=e=>{
            const i = Number(e.target.dataset.idx);
            pdfFiles.splice(i,1);
            renderList();
        }
    })
}

mergeBtn.onclick = async ()=>{
    if(pdfFiles.length===0){alert('请先添加PDF文件');return;}
    loadingEl.style.display='block';
    try{
        const outPdf = await PDFDocument.create();
        for(const f of pdfFiles){
            // 从存储的副本再复制一份用于加载,确保万无一失
            const copyBuf = new Uint8Array(f.bytes);
            const srcPdf = await PDFDocument.load(copyBuf);
            const pages = await outPdf.copyPages(srcPdf, srcPdf.getPageIndices());
            pages.forEach(p=>outPdf.addPage(p));
        }
        const pdfBytes = await outPdf.save();
        downloadFile(pdfBytes,'合并输出.pdf');
    }catch(err){
        alert('合并失败:'+err.message);
        console.error(err);
    }finally{
        loadingEl.style.display='none';
    }
}

function downloadFile(uint8,filename){
    const blob = new Blob([uint8],{type:'application/pdf'});
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
    URL.revokeObjectURL(a.href);
}

clearBtn.onclick = ()=>{
    pdfFiles=[];
    renderList();
}

pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js';
</script>
</body>
</html>
相关推荐
En^_^Joy1 小时前
Cadence Skill脚本语言快速入门指南
开发语言
10mAh1 小时前
【Java】HashMap 的 put 到底做了什么?——冲突、扩容与树化实测
java·开发语言·hash
九九落2 小时前
JavaScript 实现北京时间精确到毫秒显示:UTC+8、Asia/Shanghai 与在线时间校准详解
开发语言·javascript·ecmascript
AC赳赳老秦2 小时前
企业级合规审计体系:用 OpenClaw 落地采集全链路留痕,自动生成合规审计报告
java·python·django·beautifulsoup·php·deepseek·openclaw
东华万里3 小时前
第41篇 C++类与对象核心知识梳理:从底层原理到面试实战
开发语言·c++·大学生专区
微小冷3 小时前
Python图论库NetworkX初步
开发语言·python·图论·graph·networkx·digraph
Data_Journal4 小时前
如何使用 Python 抓取 Google Flights:分步指南
大数据·开发语言·数据库·python·scrapy
一直C4 小时前
【数据结构】哈希表+算法复杂度与经典排序查找(C语言)
java·linux·开发语言·数据结构·算法·ubuntu·散列表
Kismet_nvi5 小时前
Python 基础核心知识点总结
开发语言·windows·python