1. 项目概述与方案选型1.1 这个需求到底在解决什么先聊聊这个需求的真实场景。TXT转PDF这件事听着简单做起来其实有不少门道。我最早接触这个需求是给一个内部系统做报表导出——业务那边嫌Excel太正式Word又太重最后要求把纯文本日志直接落成PDF存档。后来陆陆续续有朋友问干脆把完整的实现思路整理出来一次性说透。很多人会问TXT本来就是纯文本直接改个后缀不就行了这还真不行。PDF本质是一种固定版面格式它不关心内容从哪来只关心“每一页怎么排、字符用什么字体、在纸面上落在哪个位置”。TXT只有一串字符流没有字体、字号、段落、分页信息所以转换的核心工作就是“替TXT补上这些缺失的排版规则”。这个需求的典型应用场景有三类日志归档系统运行日志、操作审计记录转成PDF方便长期保存和防篡改。报告导出从数据库或接口拿到纯文本数据生成排版规整的PDF报告。电子书制作把下载的小说、文档TXT整理成带目录、可翻页的PDF阅读体验更好。如果你正在做C#上位机开发、ERP系统、OA系统大概率迟早会遇到这个需求。与其到时候临时抱佛脚不如一次性把方案吃透。1.2 为什么选C# iTextSharp而不是其他方案市面上去做TXT转PDF的路径其实不少我列个表对比一下方案优点缺点适用场景Word COM组件排版能力强依赖Office环境服务器上部署容易出问题少量、交互式转换浏览器打印前端实现简单需要浏览器环境自动化差手动操作iTextSharpiText 5纯托管代码无需外部依赖API成熟需要处理中文字体分页需自己控制服务端批量转换PdfSharp轻量、MIT协议中文字体支持略弱API更底层简单PDF生成Aspose.PDF功能最强商业授权贵企业级项目我最终选择iTextSharp作为主力方案核心原因有三个第一纯托管代码。在服务器上部署时不需要安装Office、不需要装浏览器组件把DLL丢进去就能跑。这对做服务端批处理至关重要谁也不想在客户的Windows Server上去折腾Office激活。第二API设计贴近“排版”思维。iText把“页面”PdfDocument/PdfWriter、“字体”BaseFont、“文本块”Chunk/Phrase抽象得很清楚你几乎是在用代码说话“这个段落放在第3页用黑体14号字左对齐”。这种表达方式对做过任何UI开发的程序员来说都特别自然。第三社区资料多踩坑经验丰富。中文乱码、字体不显示这类经典问题网上有大量现成解决方案。对于时间紧的项目这能省下不少排查成本。顺便说一句如果你用的是.NET 6及以上版本我更推荐关注iText7的社区版它架构更新、性能更好但API风格和iTextSharpiText 5有明显差异。本文以iTextSharp为例因为存量项目和搜索引擎里90%的参考资料都基于这个版本你照着写基本不会踩版本坑。2. 开发环境与核心依赖准备2.1 环境搭建与NuGet包引入先搭环境。我用的是Visual Studio 2022创建一个.NET Framework 4.7.2的控制台应用。为什么不是.NET 6/8因为iTextSharp的经典版本5.5.x在.NET Framework下最稳定部署到服务器时用.NET Framework的兼容性也最好。如果你坚持用.NET Core/.NET 5可以用iText7或者iTextSharp.LGPLv2.Core这个社区移植版代码思路完全一致只是命名空间换一下。在NuGet包管理器里搜索并安装Install-Package iTextSharp当前稳定版本是5.5.13.x。装完以后你的项目引用里会多出itextsharp.dll。先检查一下依赖项确保目标框架是4.5以上否则某些API可能会缺失。再准备一个中文字体文件。这一步很多人会忽略但恰恰是中文PDF转换的命门。Windows系统自带字体位于C:\Windows\Fonts我一般用这几个msyh.ttc微软雅黑——观感现代适合电子阅读。simsun.ttc宋体——经典衬线字体适合正式文档。simhei.ttf黑体——标题醒目适合强调内容。注意扩展名.ttc是字体集合文件iTextSharp也能处理但要在字体名称后面加下标指定具体字体这一点后面细说。一个偷懒的办法把要用的字体文件复制到项目目录下的Fonts文件夹并设置“复制到输出目录”。这样发布的程序不依赖系统字体是否存在程序集目录下找到字体就完事。实测下来这对服务器部署特别友好尤其是那些精简版Windows系统自带的字体不全。2.2 iTextSharp核心对象速览在使用之前先把iTextSharp的几个核心对象搞清楚不然代码写起来容易乱对象作用类比PdfWriter负责把PDF写入文件流打印机的数据线PdfDocument代表整个PDF文档一份完整的打印稿PdfPage代表一个页面设置尺寸、边距一张A4纸BaseFont代表一种字体用于绘制文本打印机里的墨盒PdfContentByte“画布”在页面上绘制文字和图形画笔ColumnText按列排列文本自动换行Word里的文本框Paragraph一个带样式的段落Word里的段落概念核心逻辑很简单PdfWriter负责把内容写进文件流PdfDocument负责组织页面PdfContentByte负责在页面上“画画”画的内容就是文本。3. 核心实现从TXT到PDF的完整转换3.1 第一步读取TXT并处理编码问题这是整个项目最容易被低估的一步。TXT文件没有统一编码常见的有UTF-8、GBKGB2312、UnicodeUTF-16甚至早期还有ANSI。乱码就是在这时候埋下的。我的做法是写一个编码检测函数优先尝试UTF-8 BOM其次用GBK解码如果都失败再回溯到UTF-8public static string ReadTxtFile(string filePath) { byte[] bytes File.ReadAllBytes(filePath); // 检查UTF-8 BOM if (bytes.Length 3 bytes[0] 0xEF bytes[1] 0xBB bytes[2] 0xBF) { return Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3); } // 检查Unicode BOM if (bytes.Length 2 bytes[0] 0xFF bytes[1] 0xFE) { return Encoding.Unicode.GetString(bytes, 2, bytes.Length - 2); } // 尝试GBK解析适用于中文环境 try { Encoding gbk Encoding.GetEncoding(GBK); string gbkStr gbk.GetString(bytes); // 简单校验如果GBK解析后包含常见中文字符就认为编码正确 if (gbkStr.Any(c c 0x4E00 c 0x9FA5)) { return gbkStr; } } catch (ArgumentException) { // 当前环境不支持GBK编码时回退到UTF-8 } // 最终回退 return Encoding.UTF8.GetString(bytes); }这段代码的关键在于“按特征判断”BOM头是文件自带的编码标记优先级最高GBK解析后如果出现大量的CJK统一表意文字区段0x4E00-0x9FA5基本可以确定是中文编码文件。注意.NET Core默认不支持GBK需要额外安装System.Text.Encoding.CodePages包并注册Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);注意如果源TXT是带排版的大文件比如小说建议用StreamReader搭配FileStream逐块读取避免一次性把几百MB的文件塞进内存导致OOM。具体做法在后续性能优化部分展开。3.2 第二步中文字体注册与样式设置字体是整个中文PDF转换的重头戏。iTextSharp默认的字体库是Type1标准字体Helvetica、Times等里面根本没有中文字形直接写中文文本输出PDF里全是乱码或空方块。正确做法是加载系统字体文件注册成BaseFontpublic static BaseFont CreateChineseFont() { string fontPath Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Fonts, simhei.ttf); BaseFont bf BaseFont.CreateFont( fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED ); return bf; }这里的三个参数每一个都值得展开说字体路径指向字体文件的具体位置。BaseFont.IDENTITY_H告诉iTextSharp使用Unicode编码映射Identity-H即水平书写的字形标识。这是中文字体的标准姿势表示“按Unicode字符集去查找字形”。BaseFont.EMBEDDED把字体文件内容嵌入到PDF文件里。设成EMBEDDED的代价是PDF文件体积变大黑体大概3MB但换来的是“在任何电脑上打开都不缺字体”。如果设成NOT_EMBEDDED文件小但换台电脑打开时如果系统没有对应字体显示可能会错乱。如果你用的字体文件是.ttc扩展名比如微软雅黑、宋体那就需要加一个逗号和字体索引string fontPath C:\Windows\Fonts\msyh.ttc; BaseFont bf BaseFont.CreateFont( fontPath ,0, // 0 表示取第一个字体用逗号分隔 BaseFont.IDENTITY_H, BaseFont.EMBEDDED );ttc是字体集合一个文件里可能包含多个字体变体比如Regular、Bold、Light。,0指的是取第一个。具体用哪个下标可以用FontSelector或者直接尝试通常 0 就是常规字形。3.3 第三步段落解析与分页排版现在进入最核心的部分——怎么把一段连续的TXT文本变成“排好版”的PDF。这一步需要处理几个问题段落怎么识别、行怎么换、一页放多少行、分页怎么处理。我的思路是先把TXT按行拆出来然后做一个简化版“排版引擎”public static void ConvertTextToPdf(string txtContent, string pdfPath) { // 1. 创建文档与字体 using (FileStream fs new FileStream(pdfPath, FileMode.Create)) { Document doc new Document(PageSize.A4, 30, 30, 50, 50); PdfWriter writer PdfWriter.GetInstance(doc, fs); doc.Open(); BaseFont bf CreateChineseFont(); Font contentFont new Font(bf, 12, Font.NORMAL, BaseColor.BLACK); Font titleFont new Font(bf, 16, Font.BOLD, BaseColor.BLACK); // 2. 按行拆分TXT文本 string[] lines txtContent.Split(new[] { \r\n, \n }, StringSplitOptions.None); PdfContentByte canvas writer.DirectContent; float pageHeight doc.PageSize.Height; float topMargin 50; float bottomMargin 50; float leading 18; // 行距 float yPosition pageHeight - topMargin; foreach (string line in lines) { string trimmed line.Trim(); // 判断是否为空行段落分隔 if (string.IsNullOrWhiteSpace(trimmed)) { yPosition - leading * 0.5f; continue; } // 先判断当前行放不放得下放不下就新建一页 if (yPosition bottomMargin) { doc.NewPage(); yPosition pageHeight - topMargin; } // 画文字 canvas.BeginText(); canvas.SetFontAndSize(bf, 12); canvas.SetTextMatrix(30, yPosition); // 左边距30 canvas.ShowText(truncateToFit(trimmed, bf, 12, doc.PageSize.Width - 60)); canvas.EndText(); yPosition - leading; } doc.Close(); } }这个版本的代码做了核心的“逐行绘制”但有一个明显的缺陷如果某一行的字符数超过页宽能容纳的数量ShowText会把内容画出边界超出部分被截断或者溢出到页面外。要解决这个问题需要一个计算字符宽度的辅助函数public static string truncateToFit(string text, BaseFont bf, float fontSize, float maxWidth) { float textWidth bf.GetWidthPoint(text, fontSize); if (textWidth maxWidth) return text; // 用二分查找找到能容纳的最大字符数 int low 0, high text.Length; while (low high) { int mid (low high 1) / 2; float width bf.GetWidthPoint(text.Substring(0, mid), fontSize); if (width maxWidth) low mid; else high mid - 1; } return text.Substring(0, low); }GetWidthPoint是BaseFont提供的一个关键API它能根据字体文件的实际字形宽度计算出指定文本在当前字号下的像素宽度。这套机制就是靠“测量宽度 截断 换行”来模拟自动换行的。不过说实话BeginText/ShowText这套底层API虽然灵活但是要自己处理换行、缩进这些逻辑写起来比较繁琐。如果是正经做项目我更推荐用ColumnText来做排版public static void ConvertWithColumnText(string txtContent, string pdfPath) { using (FileStream fs new FileStream(pdfPath, FileMode.Create)) { Document doc new Document(PageSize.A4, 30, 30, 50, 50); PdfWriter writer PdfWriter.GetInstance(doc, fs); doc.Open(); BaseFont bf CreateChineseFont(); Font contentFont new Font(bf, 12, Font.NORMAL, BaseColor.BLACK); string[] paragraphs txtContent.Split(new[] { \r\n\r\n, \n\n }, StringSplitOptions.RemoveEmptyEntries); foreach (string para in paragraphs) { Paragraph p new Paragraph(para.Replace(\r\n, ).Replace(\n, ), contentFont); p.Leading 18; p.SpacingAfter 6; ColumnText ct new ColumnText(writer.DirectContent); ct.SetSimpleColumn( 30, 50, doc.PageSize.Width - 30, doc.PageSize.Height - 50, 18, Element.ALIGN_LEFT ); ct.AddElement(p); int status ct.Go(); // 如果内容超出当前页自动分页并继续 while (ColumnText.HasMoreText(status)) { doc.NewPage(); ct.SetSimpleColumn( 30, 50, doc.PageSize.Width - 30, doc.PageSize.Height - 50, 18, Element.ALIGN_LEFT ); status ct.Go(); } } doc.Close(); } }ColumnText的好处是内置了换行和分页逻辑我们只需要判断返回值HasMoreText是否还有剩余内容如果有就翻页继续写。这比手动算坐标要省心得多也更贴近自然排版。提示SetSimpleColumn的四个参数分别是左、下、右、上边界第五个参数leading是行距第六个参数是段落对齐方式。如果你的文本需要首行缩进可以在Paragraph上设置FirstLineIndent属性。3.4 第四步输出PDF与资源释放输出这一步比较简单但要提醒几个容易被忽视的细节。Document和FileStream都实现了IDisposable最好用using包裹确保异常时也能正确释放文件句柄。如果不释放文件流最直接的后果是PDF文件被锁定后续程序想覆盖它就会报“另一个程序正在使用此文件”。我早期在这个坑上栽了两次后来养成了所有IO操作一律using的习惯。另外要留意doc.Close()和writer.Close()的调用顺序。规范做法是先Close文档再释放流。见过一些老代码把顺序搞反导致PDF文件不完整打开时提示“文档已损坏”。还有一个细节如果你的转换流程嵌在Web应用里一定不要把PDF输出到Response.BinaryWrite之后就直接Response.End()还要确保doc和writer已经正确关闭否则会破坏HTTP响应前端拿到一堆乱码。正确做法是在finally里统一关闭。4. 进阶功能与体验优化4.1 自动生成目录做TXT转PDF最常被问到“能不能给我整个目录”。纯文本内容里如果存在规律性标题完全可以通过规则识别后生成PDF的书签Bookmark这样在PDF阅读器左侧就能点目录直接跳转。思路是这样的扫描TXT的每一行用正则规则识别“疑似标题”的行比如全数字标题行、带章节号的“第X章”、或者中文“一、二、三、”。记录该标题在文档中的页码位置。使用PdfAction或PdfOutline将这个标题注册到PDF书签树里。实现书签的代码大致如下// 在PdfWriter上注册PDF导航 PdfOutline root PdfWriter.GetInstance(doc, fs).RootOutline; ListDictionarystring, object bookmarksList new ListDictionarystring, object(); // 假设我们在第3页识别到一个标题 Dictionarystring, object bookmark new Dictionarystring, object(); bookmark.Add(Title, 第三章 核心技术); bookmark.Add(Action, GoTo); bookmark.Add(Page, 3 FitH 800); // 3 FitH 800 表示定位到第3页高度800 bookmarksList.Add(bookmark); writer.Outlines bookmarksList; // 注册书签这个功能的关键是“识别标题”的规则要足够准。我的经验是行首字符是数字且后面跟.或、的大概率是章节号长度在50字以内且行尾无标点的可能是标题。具体规则可以根据你的TXT特点调整。这里有必要提个醒直接从纯文本自动生成目录准确率永远不会是100%。如果是书稿、小说这类格式化的内容准确率能到95%以上但如果是日志或者无规律文本宁可少识别也不要误判否则书签列表会被一堆无关行污染。4.2 页眉页脚与页码一个规整的PDF页眉页脚几乎都是刚需。iTextSharp提供事件机制PdfPageEventHelper可以在每一页生成的前后插入自定义逻辑比在正文里手动画页码要方便得多。先实现一个继承自PdfPageEventHelper的类public class PdfFooterEvent : PdfPageEventHelper { private BaseFont _bf; public PdfFooterEvent(BaseFont bf) { _bf bf; } public override void OnEndPage(PdfWriter writer, Document document) { PdfContentByte canvas writer.DirectContent; canvas.BeginText(); canvas.SetFontAndSize(_bf, 9); // 页脚居中显示页码 string pageNumber 第 writer.PageNumber 页; float pageWidth document.PageSize.Width; float textWidth _bf.GetWidthPoint(pageNumber, 9); float x (pageWidth - textWidth) / 2; canvas.SetTextMatrix(x, 20); canvas.ShowText(pageNumber); canvas.EndText(); // 在页脚上方画一条横线 canvas.SetLineWidth(0.5f); canvas.MoveTo(30, 30); canvas.LineTo(pageWidth - 30, 30); canvas.Stroke(); } }然后在Document打开之前把事件挂到writer上writer.PageEvent new PdfFooterEvent(bf);OnEndPage会在每一页内容写完、页面即将关闭的时刻触发这时候通过writer.DirectContent拿到的画布是“当前页面”的写什么都是只对这一页生效不用手动切换页面。注意事项OnEndPage这个生命周期只在单页绘制结束时触发一次不要在这里面循环写入。如果你需要在前几页不显示页脚比如封面可以用writer.PageNumber判断页码小于2就直接返回。4.3 批量转换与进度反馈实际生产里很少只转一个文件更多时候是“把一个目录下的所有TXT全部转成PDF”。批量处理和单文件有一个本质区别进度反馈和失败隔离。进度反馈的实现方式很多最简单的是用BackgroundWorker或IProgressT报告进度条var progress new Progressint(p { progressBar.Value p; labelStatus.Text $正在处理: {p}/{total}; }); await Task.Run(() { string[] files Directory.GetFiles(txtDir, *.txt); int total files.Length; for (int i 0; i files.Length; i) { try { ConvertTextToPdf( File.ReadAllText(files[i]), Path.Combine(pdfDir, Path.GetFileNameWithoutExtension(files[i]) .pdf) ); } catch (Exception ex) { // 记录失败文件不中断整体流程 File.AppendAllText(convert_error.log, ${files[i]}: {ex.Message}\r\n); } progress.Report((i 1) * 100 / total); } });这个结构里有几个设计上的考虑失败隔离一个文件转码失败不应该导致整个批处理任务崩溃用try-catch把异常记录下来继续下一个文件。日志路径建议和输入目录分开防止被当作源文件二次处理。异步执行UI线程直接跑批量转换会导致界面假死。用Task.Run把耗时操作放到后台线程配合ProgressT更新UI实现丝滑的进度条。命名冲突生成PDF文件名时用Path.GetFileNameWithoutExtension去掉TXT后缀再拼.pdf避免重名也保持文件名语义一致。5. 常见问题与排查技巧实录5.1 转换后中文全部变成乱码或空方块这是TXT转PDF最经典的坑几乎每个新手都会踩。乱码分两种情况情况一源TXT本身编码读错。特征屏幕上打印出来的字符串就已经是乱码但PDF里显示的内容和TXT文本内容不一致。解决方案是使用前面3.1节的编码检测逻辑或者用记事本打开TXT看一眼右下角显示的是什么编码然后手动指定。情况二字体问题。特征源文本读取正常调试时看到的字符串完全正确但PDF里显示为“口口口”或者一些诡异的字符。这基本就是字体没注册成功或者注册的字体文件本身不含中文字形。我遇到最多的其实是第二种。尤其要注意BaseFont.CreateFont如果传入的字体文件路径不存在或文件不是有效字体格式会抛异常或回退到默认字体。建议在CreateFont之后打印一下bf.PostscriptFontName做确认。还有一个隐蔽问题如果你在Windows下用的是C:\Windows\Fonts\simhei.ttf部署到Linux服务器时路径就失效了。解决方案还是那句话——把字体文件拷到程序目录用相对路径引用。5.2 分页位置不理想标题被拆到两页之间这个问题的典型场景某个标题刚好落在页面的最后两行正文内容却被挤到了下一页。阅读体验非常差。解决办法是“分页前判断剩余空间”。在手动绘制模式下每次下笔前先检查yPosition - leading bottomMargin是否成立在ColumnText模式下可以通过ct.Go(true)做一次“试探排版”如果探测到分页点落在段落内部就在段前插入一个PageBreak。更稳妥的做法是把“段”作为最小排版单位。也就是每一段按空行分隔整体排入当前页的可用区域如果空间不够整段移动到下一页。这个逻辑虽然降低了页面的空间利用率但换来的观感提升是值得的foreach (string para in paragraphs) { float currentHeight yPosition - bottomMargin; float paraHeight estimateParagraphHeight(para, bf, fontSize, leading, pageWidth - 60); if (currentHeight paraHeight) { doc.NewPage(); yPosition pageHeight - topMargin; } // 绘制段落 // ... } float estimateParagraphHeight(string para, BaseFont bf, float fontSize, float leading, float lineWidth) { float textWidth bf.GetWidthPoint(para, fontSize); int lineCount (int)Math.Ceiling(textWidth / lineWidth); return lineCount * leading; }estimateParagraphHeight的原理就是“先用字体宽度算出文本总共要占几行再乘以行距”。这是预估段落高度的标准做法虽然不是100%精确没有考虑词边界换行等细节但在TXT这种连续文本场景下够用了。5.3 大文件转换慢、内存占用高如果你转的是几十MB的小说TXT用File.ReadAllText一次性读进来内存直接飙到几百MB转换过程还会卡死。这个场景必须用流式处理。流式处理的思路是“边读边写不保留全部文本在内存”。TXT按StreamReader逐行读取每读一行立即写入PDF然后丢弃该行。这样内存占用恒定在一个很小的范围内不管文件多大都能转public static void ConvertLargeTxtToPdf(string txtPath, string pdfPath) { using (FileStream fs new FileStream(pdfPath, FileMode.Create)) { Document doc new Document(PageSize.A4, 30, 30, 50, 50); PdfWriter writer PdfWriter.GetInstance(doc, fs); doc.Open(); BaseFont bf CreateChineseFont(); Font contentFont new Font(bf, 12, Font.NORMAL, BaseColor.BLACK); using (StreamReader sr new StreamReader(txtPath, Encoding.UTF8)) { string line; while ((line sr.ReadLine()) ! null) { if (string.IsNullOrWhiteSpace(line)) { // 空行代表段落结束插入空行 Paragraph emptyPara new Paragraph( , contentFont); emptyPara.Leading 12; doc.Add(emptyPara); continue; } Paragraph p new Paragraph(line, contentFont); p.Leading 18; doc.Add(p); } } doc.Close(); } }如果还是嫌慢可以在PdfWriter上启用压缩流writer.SetCompressionLevel(PdfStream.BEST_COMPRESSION);这个设置会把PDF内部的对象流压缩文件体积缩小30%左右PDF生成速度也会因为IO量减少而略有提升。代价是生成的PDF在某些老式阅读器上兼容性略差但现在的PDF阅读器基本都能正常处理。我再提供一份“问题速查表”方便运维和开发现场直接对照症状可能原因解决方向中文乱码字体未正确注册/路径错误检查BaseFont.CreateFont参数确认字体文件有效中文“口口口”字体不含中文字形换用simhei.ttf/simsun.ttc等中文字体英文正常中文全空编码读取错误用编码检测逻辑或手动指定GBK/UTF-8文件锁定无法覆盖FileStream未释放使用using语句确保Dispose超宽行被截断未处理自动换行用GetWidthPoint测量并截断或改用ColumnText生成的PDF过大字体嵌入了完整字体集考虑用NOT_EMBEDDED或使用子集化字体转大文件OOM一次性读入全部文本改用StreamReader逐行读取页码从第1页算起但封面要隐藏未按需跳过页眉页脚在OnEndPage中判断PageNumber条件跳过6. 从能用走向好用的一些扩展思路基础转换做完之后有几个方向值得继续深挖我顺手整理一下。第一个是PDF压缩。TXT转PDF的文件大小主要由字体嵌入决定嵌入中文字体后体积可能到几MB。如果对文件体积敏感可以启用iTextSharp的字体子集化设置只嵌入文档中用到的字符bf.SetSubset(true);这一行的效果很直接文档里用到哪几个字就嵌入哪几个字的字形中文字体文件可以从3MB缩到几十KB。实测一个10万字的TXT嵌入子集后PDF只有200KB左右。第二个是加密与权限控制。如果PDF需要分发给客户或内部人员可以设置打开密码和权限writer.SetEncryption( PdfWriter.STRENGTH128BITS, user_password, owner_password, PdfWriter.ALLOW_PRINTING | PdfWriter.ALLOW_COPY );owner_password是管理权限的密码持有它的人可以修改权限设置user_password是打开文档的密码。这里的“允许打印、允许复制”都是针对用户的。企业里的合同归档、报告分发通常都会用到这个功能。第三个是与定时任务/上位机集成。如果你在写C#上位机可以把转换逻辑封装成一个独立的类库供WinForm、WPF或者其他服务调用。配合System.Timers.Timer定时扫描某个文件夹发现新的TXT就自动转成PDF形成“文件监控 自动转换 归档”的完整链路。第四个是自定义PDF页面尺寸。并不是所有场景都要求A4。比如电商发货单、标签纸通常是小尺寸或者自定义宽高。Document的构造函数允许传入任意RectangleRectangle smallSize new Rectangle(200, 300); // 单位是点1英寸72点 Document doc new Document(smallSize, 10, 10, 10, 10);注意这里用的是“点”Point不是毫米也不是像素。设计标签类产品时建议先用在线换算工具把毫米转成点再填入矩形尺寸。第五个是markdown格式的TXT。如果TXT本身是Markdown写的转换逻辑可以升级为“先解析Markdown识别标题、加粗、列表结构再按层级渲染到PDF”。这个工作量会大不少但做出来之后的排版效果是纯文本方案完全比不了的。iTextSharp支持富文本样式通过Chunk的字体、字色、下划线等属性足够应付常见的Markdown标记。7. 实操心得我给自己的几个建议最后说点个人体会。TXT转PDF这件事看着是个小工具真做好也不容易。核心难点根本不在“把文本写进PDF”这一步而是“把文本的语义映射成排版规则”编码、字体、分页、段落识别、目录生成每一个环节都会冒出幺蛾子。我自己的经验是不要一上来就写完整代码先把输入样本研究清楚——你的TXT是日志还是小说有没有固定章节格式是GBK还是UTF-8多花10分钟分析输入能在后面节省1小时调试时间。还有一个容易被忽视的点输出PDF的验收标准。转换完成不代表成功。建议你在代码里加一个自检步骤用PdfReader重新打开生成的PDF检查页数是否大于0、每页是否有内容、能否正常读取文本。尤其是批量转换场景程序跑完一次生成300个PDF就算每个转换过程都“成功”了里面也可能有几十个文件因为数据异常导致内容为空。自动自检能在交付前把问题兜住。另外调试的时候我强烈建议给Document设置一个小页面比如Rectangle(300, 400)窗口缩小到刚好显示页面大小肉眼检查排版效果。用A4纸那么大的页面去调试每次刷新都要滚动效率低很多。这个工具后续能扩展的方向很多比如集成到桌面端做一键转换、做成Windows服务定时归档、或者把文本内容直接渲染到既有PDF模板的指定区域。不管往哪个方向走掌握“理解内容 → 选择字体 → 控制排版 → 正确输出”这条主线就不会跑偏。最后分享一个日常调试小技巧在转换方法里加一个参数bool previewMode当它为true时只转换前20行内容并输出到临时PDF方便快速验证排版效果。这个小功能我每次做PDF相关需求都会加效率提升非常明显。