.NET Core API动态PDF生成器开发:PDF/docx占位符替换需求
动态PDF生成方案(.NET Core API + Docker)
我之前刚好做过类似的动态PDF生成服务,结合你的需求,给你梳理一套落地可行的方案,分步骤来:
一、核心思路拆解
你的需求本质是先制作可复用模板(替换固定内容为占位符),再基于模板填充动态数据生成最终PDF。这里分两种场景处理,优先推荐第一种,更稳定易维护:
1. 优先处理DocX模板(低复杂度高兼容性)
直接修改PDF的文本替换容易出现格式错乱、字体偏移的问题,建议先把DocX转换成带占位符的模板,再填充数据转PDF,流程:
- 读取原始DocX文件 → 替换固定内容(如John Doe)为统一占位符(如
#NAME_PLACEHOLDER)→ 保存为模板文件 - 后续生成PDF时,读取模板DocX → 用
Dictionary<string, string>替换所有占位符 → 转换为PDF
2. 直接处理PDF模板(适合已有PDF模板的场景)
如果必须基于现有PDF修改,需要用PDF处理库定位文本位置并替换,注意这种方式对复杂格式的PDF兼容性一般。
二、具体实现代码
场景1:DocX模板转PDF
依赖库安装
在你的.NET Core项目中安装NuGet包:
Install-Package DocX Install-Package DinkToPdf
DinkToPdf基于wkhtmltopdf,适配Docker环境下的HTML转PDF(我们可以先把填充后的DocX转成HTML,再转PDF)。
代码示例
using DocX; using DinkToPdf; using DinkToPdf.Contracts; public class PdfGeneratorService { private readonly IConverter _converter; // 注入DinkToPdf转换器(在Program.cs中注册) public PdfGeneratorService(IConverter converter) { _converter = converter; } // 第一步:将原始DocX转换成带占位符的模板 public void CreateDocxTemplate(string originalDocxPath, string templatePath) { using (var document = DocX.Load(originalDocxPath)) { // 替换原始内容为占位符,可批量添加规则 document.ReplaceText("John Doe", "#NAME_PLACEHOLDER"); document.ReplaceText("123 Main St", "#ADDRESS_PLACEHOLDER"); document.SaveAs(templatePath); } } // 第二步:基于模板填充数据并生成PDF public byte[] GeneratePdfFromTemplate(string templatePath, Dictionary<string, string> data) { using (var document = DocX.Load(templatePath)) { // 填充动态数据 foreach (var kvp in data) { document.ReplaceText(kvp.Key, kvp.Value); } // 将DocX转成HTML(DinkToPdf支持HTML转PDF) var htmlContent = document.SaveAsHtml(); // 配置PDF转换参数 var pdfConfig = new HtmlToPdfDocument() { GlobalSettings = { ColorMode = ColorMode.Color, Orientation = Orientation.Portrait, PaperSize = PaperKind.A4, }, Objects = { new ObjectSettings() { HtmlContent = htmlContent } } }; // 生成PDF字节数组,方便API直接返回 return _converter.Convert(pdfConfig); } } }
场景2:直接修改PDF模板
如果需要直接操作PDF,推荐使用适配.NET Core的iTextSharp分支:
依赖库安装
Install-Package iTextSharp.LGPLv2.Core
代码示例
using iTextSharp.text.pdf; using iTextSharp.text.pdf.parser; public byte[] ReplacePdfPlaceholders(string templatePdfPath, Dictionary<string, string> data) { using (var originalStream = new FileStream(templatePdfPath, FileMode.Open)) using (var outputStream = new MemoryStream()) { var reader = new PdfReader(originalStream); var stamper = new PdfStamper(reader, outputStream); var acroFields = stamper.AcroFields; // 如果PDF是表单模板(带可填写字段),直接填充字段更简单 foreach (var kvp in data) { if (acroFields.Fields.ContainsKey(kvp.Key)) { acroFields.SetField(kvp.Key, kvp.Value); } } // 如果是普通文本替换(非表单),遍历每页查找文本并替换 var pages = reader.NumberOfPages; for (int i = 1; i <= pages; i++) { var strategy = new ReplaceTextStrategy(data); PdfTextExtractor.GetTextFromPage(reader, i, strategy); stamper.GetOverContent(i).Add(strategy.GetUpdatedContent()); } stamper.FormFlattening = true; // 扁平化表单,防止后续修改 stamper.Close(); reader.Close(); return outputStream.ToArray(); } } // 自定义文本替换策略 public class ReplaceTextStrategy : LocationTextExtractionStrategy { private readonly Dictionary<string, string> _replacements; private readonly List<Rectangle> _textLocations = new List<Rectangle>(); private string _currentText; public ReplaceTextStrategy(Dictionary<string, string> replacements) { _replacements = replacements; } public override void RenderText(TextRenderInfo renderInfo) { base.RenderText(renderInfo); _currentText = renderInfo.GetText(); _textLocations.Add(renderInfo.GetDescentLine().GetBoundingRectangle()); } public PdfContentByte GetUpdatedContent() { var content = new PdfContentByte(null); foreach (var rect in _textLocations) { // 先覆盖原文本(用白色填充) content.SetColorFill(BaseColor.WHITE); content.Rectangle(rect.Left, rect.Bottom, rect.Width, rect.Height); content.Fill(); // 写入替换后的文本,注意匹配原字体大小 var replacementText = _replacements.TryGetValue(_currentText, out var val) ? val : _currentText; content.SetColorFill(BaseColor.BLACK); content.BeginText(); content.SetFontAndSize(BaseFont.CreateFont(), 12); content.ShowTextAligned(Element.ALIGN_LEFT, replacementText, rect.Left, rect.Bottom, 0); content.EndText(); } return content; } }
三、Docker环境配置
因为用到了DinkToPdf(依赖wkhtmltopdf),需要在Dockerfile中安装对应的系统依赖:
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base WORKDIR /app EXPOSE 80 # 安装wkhtmltopdf及依赖库 RUN apt-get update && apt-get install -y \ wkhtmltopdf \ libgdiplus \ libc6-dev FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src COPY ["YourProjectName.csproj", "."] RUN dotnet restore "./YourProjectName.csproj" COPY . . WORKDIR "/src/." RUN dotnet build "YourProjectName.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "YourProjectName.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "YourProjectName.dll"]
注意:如果是.NET 7/8,替换对应的基础镜像即可。
四、实用小贴士
- 占位符尽量用独特格式,比如
{{NAME}}或者#NAME_PLACEHOLDER,避免和文档中的正常内容冲突 - 如果DocX模板有复杂格式(如表格、图片),用DocX替换占位符时不会破坏原有格式,比直接操作PDF靠谱
- 大文件处理时,尽量用流操作(
MemoryStream)代替文件读写,提升性能 - Docker中如果出现字体缺失导致PDF乱码,可以把自定义字体打包到镜像中,在代码中指定字体路径
内容的提问来源于stack exchange,提问作者MortenMoulder
相关产品推荐
相关产品推荐

