简介这份资源面向.NET平台下需要处理PDF文档的C#开发者聚焦iTextSharp库在PDF合并与分卷场景中的实际应用。内容以Windows Forms示例工程为主线演示如何通过PdfReader、PdfCopy、PdfSmartCopy与PdfStamper等核心类完成多文档合并、按页码范围拆分、权限与元数据处理并兼顾资源释放与错误处理等工程细节适合具备一定C#基础、希望快速上手PDF批处理的中级开发者参考。压缩包共63个文件约6.6MB包含21个PDF示例文档、7个cs源码文件以及dll、config、resx、csproj、sln等工程与依赖文件另有nupkg包与说明文档目录结构完整可直接在Visual Studio中打开运行。目前已有492人学习下载。借助这套代码读者能够理解合并与分卷的完整实现路径掌握NuGet依赖引入、窗体交互与后台处理的组织方式并据此改造出适配自身业务的PDF整理工具。1. 从一堆散装 PDF 到一份能交差的合并件iTextSharp 到底能省掉哪些手工活做过测绘、清算、购房合同这类资料归档的人都有体会一个项目跑下来PDF 散落在十几个文件夹里测绘报告、支付票据、发票、清算通知书、营业执照各来一份最后要交的却是一份按顺序排好的合并件。手工拖进阅读器里拼拼到一半发现某份是扫描件方向反了或者分卷时页码对不上返工是常事。这份PDFCombineApp就是冲着这个场景来的——一个基于 .NET Framework 4.8 的 WinForms 小工具用 iTextSharp 5.5.13.3 做 PDF 的合并与分卷源码、依赖包、示例 PDF 都在压缩包里解压就能编译运行。它适合两类人一类是手头有大量 PDF 要按业务顺序整合的从业者另一类是想拿一个能跑的最小工程去改自己合并逻辑的 C# 开发者。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序拆开讲中间会落到具体代码和参数上。2. 工程结构与依赖先看清这个包里的东西怎么摆2.1 解决方案里各文件的职责划分拿到PDFCombineApp.rar解压后根目录是一个标准的 Visual Studio 解决方案。PDFCombineApp.sln负责把项目串起来PDFCombineApp.csproj定义编译目标和引用packages.config记录 NuGet 依赖。真正跟 PDF 操作相关的代码集中在Form1.cs界面布局在Form1.Designer.cs入口在Program.cs。App.config里放的是运行时配置比如合并后文件的默认输出路径这类可调项。bin\Debug和obj\Debug是编译产物目录里面能看到已经生成好的PDFCombineApp.exe也就是说即使不重新编译双击也能直接跑。packages目录下有两个关键依赖iTextSharp.5.5.13.3和BouncyCastle.1.8.9。后者是 iTextSharp 处理加密 PDF 时的加密算法支撑库缺了它遇到带权限密码的 PDF 会直接抛异常。itextsharp.xml是 API 的 XML 注释文档写代码时能在 IDE 里看到方法说明别删。示例 PDF 覆盖了真实业务里常见的几类购房合同.pdf、测绘报告.pdf及其副本、发票.pdf及副本、支付票据.pdf、清算通知书.pdf、营业执照.pdf、开发资质.pdf、第三方审定依据.pdf、视同销售.pdf、备注说明.pdf、凭证号.pdf、结算凭证.pdf。这些文件不是随便凑的它们对应了合并时最典型的几种情况有文本型 PDF有扫描件有带副本需要去重的有需要按固定顺序排列的。拿它们做测试比拿几个空白 PDF 有意义得多。2.2 iTextSharp 5.5.13.3 的选型理由与版本边界为什么是 iTextSharp 5.5.13.3 而不是更新的 iText 7这是有实际原因的。iText 7 的 API 做了大改包名从iTextSharp变成itext7命名空间从iTextSharp.text变成iText.Kernel.Pdf网上大量现成的合并分卷代码片段都是基于 5.x 写的。5.5.13.3 是 5.x 系列里比较靠后的版本对 .NET Framework 4.8 兼容稳定PdfReader、PdfCopy、PdfSmartCopy、PdfStamper这些类都在够用。需要留意的是许可证。iTextSharp 5.x 采用 AGPL 协议包里的gnu-agpl-v3.0.md和LICENSE.md就是许可证文本。如果只是内部工具自用问题不大如果要集成到对外分发的商业软件里AGPL 的传染性需要认真评估这一点在选型阶段就得想清楚别等上线了才发现。2.3 环境准备与首次编译编译这个工程需要 Visual Studio 2019 或更高版本工作负载勾选「.NET 桌面开发」。打开PDFCombineApp.sln后NuGet 包还原应该能自动完成因为packages目录已经随包提供了。如果还原失败检查packages.config里的版本号是否和packages目录下的文件夹名一致。# 如果 NuGet 还原不成功可以在解决方案目录下手动还原 nuget restore PDFCombineApp.sln # 或者用 msbuild 直接编译指定 Release 配置 msbuild PDFCombineApp.sln /p:ConfigurationRelease /p:PlatformAny CPUnuget restore会读取packages.config把依赖下载到packages目录msbuild那行是命令行编译适合没有装完整 IDE 的机器。编译成功后bin\Debug或bin\Release下会生成PDFCombineApp.exe双击即可启动。如果报「找不到 BouncyCastle.Crypto.dll」说明依赖没还原全手动把packages\BouncyCastle.1.8.9\lib下的 DLL 复制到输出目录也能救急。3. 合并与分卷的核心实现PdfReader、PdfCopy 与 PdfSmartCopy 怎么配合3.1 合并逻辑从 PdfReader 到 PdfCopy 的完整链路合并的实质是把多个 PDF 的页面按顺序搬到一个新文档里。iTextSharp 里负责读的是PdfReader负责写的是PdfCopy。PdfCopy和PdfSmartCopy的区别在于PdfCopy直接复制页面内容速度快但遇到多个文件里有相同字体、图片资源时会重复存储PdfSmartCopy会做资源去重合并出来的文件更小代价是处理时内存占用略高。对于发票、票据这类每份都独立的文件用PdfCopy就够如果合并的是同一套模板生成的报告PdfSmartCopy更划算。using iTextSharp.text; using iTextSharp.text.pdf; using System.IO; public void MergePdfFiles(string[] sourceFiles, string outputPath) { // 创建输出文档PageSize 用 A4边距设为 0 避免二次裁剪 using (Document document new Document(PageSize.A4, 0, 0, 0, 0)) { // PdfCopy 负责把源页面写入目标文档 using (PdfCopy copy new PdfCopy(document, new FileStream(outputPath, FileMode.Create))) { document.Open(); foreach (string file in sourceFiles) { // PdfReader 读取每个源文件注意用 using 确保释放 using (PdfReader reader new PdfReader(file)) { int pageCount reader.NumberOfPages; for (int i 1; i pageCount; i) { // AddPage 把第 i 页加入目标文档页码从 1 开始 copy.AddPage(copy.GetImportedPage(reader, i)); } } } } } }这段代码里几个参数值得说清楚。PageSize.A4是目标文档的页面尺寸但如果源 PDF 本身不是 A4PdfCopy会保留源页面的实际尺寸这里的设置主要影响文档级属性。new FileStream(outputPath, FileMode.Create)里的FileMode.Create表示如果目标文件已存在就覆盖想改成追加或报错可以换成FileMode.CreateNew。copy.GetImportedPage(reader, i)是把源页面导入到目标文档的上下文里这一步不能省直接AddPage源页面对象会报错。reader.NumberOfPages返回总页数循环从 1 开始是因为 iTextSharp 的页码是 1-based不是 0-based这一点跟数组下标习惯不同容易写错。3.2 分卷逻辑按页码范围拆分与 PdfStamper 的取舍分卷是把一个大 PDF 按页码切成若干小文件。常见做法有两种一种是用PdfReader加PdfCopy跟合并反过来把指定页码范围的页面写到一个新文档另一种是用PdfStamper在原有文档上操作。PdfStamper更适合做页面级修改比如加页码、加水印纯拆分用PdfCopy更直接。public void SplitPdf(string sourceFile, int startPage, int endPage, string outputPath) { using (PdfReader reader new PdfReader(sourceFile)) { using (Document document new Document()) { using (PdfCopy copy new PdfCopy(document, new FileStream(outputPath, FileMode.Create))) { document.Open(); // 页码边界检查防止越界 if (startPage 1) startPage 1; if (endPage reader.NumberOfPages) endPage reader.NumberOfPages; for (int i startPage; i endPage; i) { copy.AddPage(copy.GetImportedPage(reader, i)); } } } } }startPage和endPage是闭区间比如传 3 和 7拆出来的是第 3 到第 7 页共 5 页。边界检查那两行是血泪经验如果调用方传了超出总页数的endPageGetImportedPage会抛ArgumentOutOfRangeException程序直接崩。加上钳制逻辑后至少不会因为一个参数错误就挂掉。输出路径如果跟源文件相同FileMode.Create会先把源文件截断导致读取失败所以分卷时输出文件名一定要跟源文件区分开。3.3 合并顺序与文件筛选别让「副本」混进正式件示例 PDF 里有一堆带「副本」字样的文件比如测绘报告 - 副本 (2).pdf、发票 - 副本.pdf、支付票据 - 副本2.pdf。这些在真实业务里通常是重复件或备份件合并时如果一股脑全加进去交出去的文件里就会出现重复内容。合理的做法是在选择文件阶段就做筛选或者在代码里按文件名规则过滤。// 按业务顺序排列的合并清单副本文件不纳入 string[] orderedFiles new string[] { 购房合同.pdf, 营业执照.pdf, 开发资质.pdf, 测绘报告.pdf, 第三方审定依据.pdf, 清算通知书.pdf, 结算凭证.pdf, 支付票据.pdf, 发票.pdf, 视同销售.pdf, 备注说明.pdf, 凭证号.pdf }; // 过滤掉文件名里带「副本」的项 var filtered orderedFiles.Where(f !f.Contains(副本)).ToArray(); MergePdfFiles(filtered, 合并后的PDF--0.pdf);orderedFiles数组的顺序就是最终合并件里的页面顺序这个顺序应该跟业务归档要求一致不能靠文件系统的默认排序。Where那行用 LINQ 过滤掉带「副本」的文件名Contains是区分大小写的如果副本命名不统一比如有「副本」「copy」「Copy」混用需要把过滤条件写得更全。合并输出文件名合并后的PDF--0.pdf和合并后的PDF--1.pdf在示例里已经存在说明这个工具支持一次生成多个合并件可能是按业务类型分批合并的结果。4. 避坑与排查合并分卷时最容易翻车的五个点4.1 现象合并后文件体积暴涨打开卡顿原因用了PdfCopy而不是PdfSmartCopy多个源文件里的相同字体、图片资源被重复嵌入。尤其是扫描件 PDF每页都是一张大图重复存储后体积能翻好几倍。解决把new PdfCopy(...)换成new PdfSmartCopy(...)其余代码不变。PdfSmartCopy会在写入时做资源哈希比对相同资源只存一份。代价是处理大文件时内存占用上升如果源文件总页数超过 500 页建议分批合并再二次合并。4.2 现象合并时抛「PdfReader not opened with owner password」原因源 PDF 带有权限密码owner passwordPdfReader默认不允许在未提供密码的情况下读取内容。示例里的营业执照.pdf、开发资质.pdf这类证照文件有些是从政务系统导出的可能带权限限制。解决在构造PdfReader时传入密码或者用PdfReader.unethicalreading true跳过权限检查仅限你有权处理的文件。// 方式一已知密码 PdfReader reader new PdfReader(file, new System.Text.ASCIIEncoding().GetBytes(password)); // 方式二跳过权限检查仅用于有权处理的文件 PdfReader.unethicalreading true; PdfReader reader new PdfReader(file);unethicalreading这个静态属性设一次就全局生效放在程序启动时设置比较合适。注意这个属性名本身就带警示意味用之前确认自己对文件有处理权限。4.3 现象分卷后页码错乱第 1 页跑到最后原因PdfReader的页码是 1-based但有些开发者习惯从 0 开始循环导致GetImportedPage(reader, 0)抛异常或者跳页。另一种情况是源 PDF 本身有书签或逻辑页码跟物理页码不一致拆分时按物理页码切跟阅读器显示的页码对不上。解决循环统一从 1 开始到reader.NumberOfPages结束。如果源 PDF 有逻辑页码偏移需要先用reader.GetPageLabels()读出标签映射再决定切分范围。对于大多数业务 PDF物理页码和逻辑页码一致不用额外处理。4.4 现象合并后的 PDF 在部分阅读器里打不开原因Document没有正确Close()或者PdfCopy的FileStream没释放导致输出文件尾部缺少交叉引用表xref。这种情况在 Windows 自带阅读器里可能能打开但在某些严格校验的阅读器里会报「文件已损坏」。解决确保Document、PdfCopy、PdfReader、FileStream都包在using块里让 Dispose 按顺序调用。Document.Close()会触发PdfCopy写入 xref 表这一步不能省。如果手动管理资源关闭顺序必须是先关PdfCopy再关Document最后关FileStream顺序反了同样会出问题。4.5 现象中文文件名或路径导致 FileNotFoundException原因FileStream在某些系统区域设置下对中文路径处理不一致尤其是路径里同时有中文和空格时。示例里的测绘报告 - 副本 (2).pdf就同时包含中文、空格和括号。解决在打开文件前用Path.GetFullPath把相对路径转成绝对路径避免工作目录变化导致找不到文件。如果仍然报错检查文件名里的括号是全角还是半角- 副本里的短横是-还是–这些字符在复制粘贴时容易混。string fullPath Path.GetFullPath(测绘报告 - 副本 (2).pdf); if (!File.Exists(fullPath)) { throw new FileNotFoundException($文件不存在{fullPath}); }5. 进阶技巧用 PdfStamper 给合并件补页码并验证完整性合并完的 PDF 交出去之前通常还需要补一件事加页码。尤其是几十页的合并件没有页码审阅的人翻到一半就乱了。PdfStamper可以在不重建文档的前提下往每页写内容比重新走一遍PdfCopy轻量。public void AddPageNumbers(string pdfPath, string outputPath) { using (PdfReader reader new PdfReader(pdfPath)) { using (PdfStamper stamper new PdfStamper(reader, new FileStream(outputPath, FileMode.Create))) { int total reader.NumberOfPages; BaseFont baseFont BaseFont.CreateFont(C:\\Windows\\Fonts\\simsun.ttc,0, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); for (int i 1; i total; i) { // 获取页面内容层在其上绘制文字 PdfContentByte content stamper.GetOverContent(i); content.BeginText(); content.SetFontAndSize(baseFont, 9); content.SetTextMatrix(520, 20); // 右下角坐标 content.ShowText($第 {i} 页 / 共 {total} 页); content.EndText(); } } } }BaseFont.CreateFont那行加载的是宋体simsun.ttc,0里的,0表示取字体集合里的第一个字体。BaseFont.IDENTITY_H是横排中文编码BaseFont.EMBEDDED把字体子集嵌入 PDF保证在其他机器上也能正常显示。SetTextMatrix(520, 20)是文字基线坐标单位是点1 点约 1/72 英寸A4 页面宽 595 点、高 842 点520 和 20 大致落在右下角。如果页码位置偏了调这两个数就行。加完页码后验证合并件是否完整我一般会走一遍「页数核对 首尾页抽查」。页数核对用PdfReader.NumberOfPages跟预期总页数比对首尾页抽查是把第一页和最后一页单独拆出来确认内容没有错位或缺失。这个习惯是从一次翻车经历来的有次合并了 12 份文件总数对得上但中间某份扫描件因为方向问题被旋转了 90 度交出去才被发现。从那以后我每次合并完都强制走一遍页数核对和首尾页抽查宁可多花两分钟也不返工重做。// 页数核对 using (PdfReader check new PdfReader(合并后的PDF--0.pdf)) { Console.WriteLine($合并件总页数{check.NumberOfPages}); } // 首尾页抽查拆出第 1 页和最后一页单独保存 SplitPdf(合并后的PDF--0.pdf, 1, 1, check_first.pdf); int lastPage new PdfReader(合并后的PDF--0.pdf).NumberOfPages; SplitPdf(合并后的PDF--0.pdf, lastPage, lastPage, check_last.pdf);这套流程跑下来合并件基本不会出大问题。PDFCombineApp这个工程本身不复杂但胜在依赖齐全、示例真实、代码结构清晰拿过来改合并顺序、改分卷规则、加页码水印都方便。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?