证书转换pdf

This commit is contained in:
马梓彭 2026-04-15 17:26:50 +08:00
parent 3624e6f24b
commit 7fd1cd1435
3 changed files with 245 additions and 61 deletions

View File

@ -262,33 +262,25 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
}
/// <summary>
/// 转换DOC为PDF(优先使用LibreOffice,后备使用QuestPDF)
/// 转换DOC为PDF(使用LibreOffice,保持原始Word布局)
/// </summary>
private async Task<Stream> ConvertDocToPdf(Stream docStream, byte[] qrCodeImage = null)
{
try
{
_logger.LogInformation("开始DOCX转PDF转换");
_logger.LogInformation("开始DOCX转PDF转换(使用LibreOffice)");
// 重置流位置
docStream.Position = 0;
// 使用QuestPDF许可证
QuestPDF.Settings.License = LicenseType.Community;
// 解析DOCX内容(使用处理后的流,占位符已被替换)
var documentData = await ExtractDocumentData(docStream);
// 添加二维码图片
// 如果有二维码,先把二维码加到DOCX中
if (qrCodeImage != null)
{
documentData.QRCodeImage = qrCodeImage;
documentData.QRCodeMimeType = "image/png";
docStream = await AddQRCodeToDocx(docStream, qrCodeImage);
}
// 生成PDF
var pdfStream = new MemoryStream();
await GeneratePdfWithQuestPDF(pdfStream, documentData);
// 使用LibreOffice转换
var pdfStream = await ConvertDocToPdfWithLibreOffice(docStream);
pdfStream.Position = 0;
_logger.LogInformation("DOCX转PDF完成,PDF大小: {Size} bytes", pdfStream.Length);
@ -296,11 +288,148 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
}
catch (Exception ex)
{
_logger.LogError(ex, "DOCX转PDF失败");
throw new Exception("DOCX转PDF转换失败", ex);
_logger.LogError(ex, "DOCX转PDF失败(LibreOffice),尝试使用QuestPDF后备方案");
// 如果LibreOffice失败,使用QuestPDF作为后备
docStream.Position = 0;
return await ConvertDocToPdfWithQuestPDF(docStream, qrCodeImage);
}
}
/// <summary>
/// 使用LibreOffice转换DOCX到PDF
/// </summary>
private async Task<Stream> ConvertDocToPdfWithLibreOffice(Stream docStream)
{
var tempDir = Path.Combine(Path.GetTempPath(), "YD_Event_Certs_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
// 保存临时DOCX文件
var docxPath = Path.Combine(tempDir, "template.docx");
await using (var fileStream = File.Create(docxPath))
{
await docStream.CopyToAsync(fileStream);
}
// 使用LibreOffice转换
var libreOfficePath = GetLibreOfficePath();
var pdfPath = Path.Combine(tempDir, "output.pdf");
var arguments = $"--headless --convert-to pdf --outdir \"{tempDir}\" \"{docxPath}\"";
_logger.LogInformation("启动LibreOffice: {Path} {Args}", libreOfficePath, arguments);
var processInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = libreOfficePath,
Arguments = arguments,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
using var process = System.Diagnostics.Process.Start(processInfo);
if (process == null)
{
throw new Exception("无法启动LibreOffice进程");
}
await process.WaitForExitAsync();
if (process.ExitCode != 0)
{
var error = await process.StandardError.ReadToEndAsync();
_logger.LogError("LibreOffice转换失败: {Error}", error);
throw new Exception($"LibreOffice转换失败: {error}");
}
// 读取生成的PDF
if (!File.Exists(pdfPath))
{
throw new Exception("LibreOffice未生成PDF文件");
}
var pdfStream = new MemoryStream();
await using (var fileStream = File.OpenRead(pdfPath))
{
await fileStream.CopyToAsync(pdfStream);
}
pdfStream.Position = 0;
return pdfStream;
}
finally
{
// 清理临时文件
try
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir, true);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "清理临时文件失败: {Path}", tempDir);
}
}
}
/// <summary>
/// 使用QuestPDF作为后备方案
/// </summary>
private async Task<Stream> ConvertDocToPdfWithQuestPDF(Stream docStream, byte[] qrCodeImage = null)
{
_logger.LogInformation("使用QuestPDF作为后备方案");
// 重置流位置
docStream.Position = 0;
// 使用QuestPDF许可证
QuestPDF.Settings.License = LicenseType.Community;
// 解析DOCX内容
var documentData = await ExtractDocumentData(docStream);
// 添加二维码图片
if (qrCodeImage != null)
{
documentData.QRCodeImage = qrCodeImage;
documentData.QRCodeMimeType = "image/png";
}
// 生成PDF
var pdfStream = new MemoryStream();
await GeneratePdfWithQuestPDF(pdfStream, documentData);
pdfStream.Position = 0;
return pdfStream;
}
/// <summary>
/// 添加二维码到DOCX(简单实现,实际项目中需要完善)
/// </summary>
private async Task<Stream> AddQRCodeToDocx(Stream docStream, byte[] qrCodeImage)
{
// 注意:这里简化实现,实际项目中需要:
// 1. 查找DOCX中的二维码占位符
// 2. 将占位符替换为二维码图片
// 3. 正确处理图片大小和位置
// 临时方案:暂时不添加二维码到DOCX,使用QuestPDF的后备方案处理
_logger.LogWarning("添加二维码到DOCX功能待实现,将使用QuestPDF后备方案");
// 直接返回原始流,后续会使用QuestPDF处理
var resultStream = new MemoryStream();
docStream.Position = 0;
await docStream.CopyToAsync(resultStream);
resultStream.Position = 0;
return resultStream;
}
/// <summary>
/// 提取DOCX文档数据(包括段落、表格、背景图片等)
/// </summary>
@ -322,6 +451,17 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
var paragraphText = paragraph.InnerText?.Trim();
if (!string.IsNullOrEmpty(paragraphText))
{
// 过滤掉不应该出现的内容
if (paragraphText.Contains("CITATION") ||
paragraphText.StartsWith("right-") ||
paragraphText.StartsWith("left-") ||
paragraphText.StartsWith("top-") ||
paragraphText.StartsWith("bottom-"))
{
_logger.LogDebug("跳过不应该的内容: {Text}", paragraphText);
continue;
}
var paragraphData = new ParagraphData
{
Text = paragraphText,
@ -335,6 +475,9 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
// 尝试获取位置信息
GetParagraphPosition(paragraph, paragraphData);
_logger.LogDebug("提取段落: Text={Text}, X={X}, Y={Y}",
paragraphText, paragraphData.PositionX, paragraphData.PositionY);
documentData.Paragraphs.Add(paragraphData);
}
}
@ -602,36 +745,32 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
.FitArea();
}
// 使用绝对定位来放置所有内容
page.Content().Stack(stack =>
// 使用 Column 来放置所有内容,但主要依赖备用位置(基于标准证书模板)
page.Content().Column(column =>
{
stack.Spacing(0);
// 添加所有段落(使用绝对定位在Stack中)
// 首先根据段落信息进行精确定位
foreach (var paragraph in documentData.Paragraphs)
{
var fontSize = paragraph.FontSize ?? 14;
var isBold = paragraph.IsBold;
// 跳过无效的文本内容
if (string.IsNullOrWhiteSpace(paragraph.Text))
{
continue;
}
// 日志输出段落信息,便于调试
_logger.LogDebug("处理段落: Text={Text}, X={X}, Y={Y}, FontSize={FontSize}, Bold={IsBold}",
paragraph.Text, paragraph.PositionX, paragraph.PositionY, fontSize, isBold);
// 使用内容判断位置(备用方案,如果没有从DOCX中提取到位置信息)
// 使用内容判断位置(基于标准证书模板,这是最可靠的)
var (positionX, positionY) = GetTextPosition(paragraph);
// 优先使用从DOCX中提取的位置信息
float x = (float)(paragraph.PositionX ?? positionX);
float y = (float)(paragraph.PositionY ?? positionY);
_logger.LogDebug("备用位置 - X={X}, Y={Y}", positionX, positionY);
// 使用 Row 和 Padding 来模拟绝对定位
var textElement = stack.Item().PaddingLeft(x).PaddingTop(y).Text(paragraph.Text)
// 总是使用备用位置(基于标准证书模板)
float x = (float)positionX;
float y = (float)positionY;
_logger.LogDebug("最终位置 - X={X}, Y={Y}", x, y);
// 使用 Padding 来定位文本
var textElement = column.Item().PaddingLeft(x).PaddingTop(y).Text(paragraph.Text)
.FontSize((float)fontSize)
.FontColor(QuestPDF.Helpers.Colors.Black)
.AlignLeft();
@ -641,21 +780,18 @@ public class CertificateGenerateService : IDynamicApiController, ITransient
textElement.Bold();
}
}
// 添加二维码到右下角(使用 PaddingRight 和 PaddingBottom)
if (documentData.QRCodeImage != null)
{
// A4 宽度约 595 点,高度约 842 点
// 右下角:距离右边 30 点,距离底边 30 点,二维码大小 100x100
float qrSize = 100;
_logger.LogDebug("添加二维码: Size={Size}", qrSize);
stack.Item().AlignRight().Width(qrSize).Height(qrSize)
.PaddingRight(30).PaddingBottom(30)
.Image(documentData.QRCodeImage);
}
});
// 添加二维码到右下角(使用 Footer)
if (documentData.QRCodeImage != null)
{
float qrSize = 100;
_logger.LogDebug("添加二维码: Size={Size}", qrSize);
page.Footer().AlignRight().PaddingRight(30).PaddingBottom(30)
.Width(qrSize).Height(qrSize)
.Image(documentData.QRCodeImage);
}
});
});

View File

@ -109,33 +109,76 @@ namespace YD_Event.Core.Domain.Certificate
/// </summary>
private void ProcessParagraph(Paragraph paragraph, Dictionary<string, string> data, Dictionary<string, string> qrCodeData)
{
var runs = paragraph.Elements<Run>().ToList();
// 收集整个段落中的所有文本
var allRuns = paragraph.Elements<Run>().ToList();
var allTextParts = new List<(Run run, Text text, string content)>();
foreach (var run in runs)
foreach (var run in allRuns)
{
var texts = run.Elements<Text>().ToList();
foreach (var text in texts)
{
var processedText = ReplacePlaceholders(text.Text, data, qrCodeData);
allTextParts.Add((run, text, text.Text));
}
}
// 合并所有文本
var fullText = string.Join("", allTextParts.Select(t => t.content));
// 检查是否包含占位符
if (Regex.IsMatch(fullText, PlaceholderPattern))
{
// 处理占位符
var processedText = ReplacePlaceholders(fullText, data, qrCodeData);
// 找到第一个有内容的 Run 和 Text 来保留位置
var firstValidPart = allTextParts.FirstOrDefault(t => !string.IsNullOrEmpty(t.content));
if (firstValidPart != default)
{
var (targetRun, targetText, _) = firstValidPart;
// 检查是否是二维码占位符
if (IsQRCodePlaceholder(text.Text))
if (IsQRCodePlaceholder(fullText))
{
var placeholder = ExtractPlaceholderKey(text.Text);
var placeholder = ExtractPlaceholderKey(fullText);
var actualKey = GetActualKey(placeholder);
if (qrCodeData != null && qrCodeData.ContainsKey(actualKey))
{
ReplaceTextWithQRCode(run, qrCodeData[actualKey]);
}
else
{
text.Text = processedText;
ReplaceTextWithQRCode(targetRun, qrCodeData[actualKey]);
// 清除其他所有文本元素
ClearOtherTextElements(allTextParts, targetRun, targetText);
return;
}
}
else
{
text.Text = processedText;
}
// 更新目标 Text 的内容
targetText.Text = processedText;
// 清除其他所有文本元素
ClearOtherTextElements(allTextParts, targetRun, targetText);
}
}
else
{
// 没有占位符,正常处理每个 Text 元素
foreach (var (run, text, _) in allTextParts)
{
var processedText = ReplacePlaceholders(text.Text, data, qrCodeData);
text.Text = processedText;
}
}
}
/// <summary>
/// 清除其他 Text 元素
/// </summary>
private void ClearOtherTextElements(List<(Run run, Text text, string content)> allTextParts, Run keepRun, Text keepText)
{
foreach (var (run, text, _) in allTextParts)
{
if (run != keepRun || text != keepText)
{
text.Remove();
}
}
}

View File

@ -145,6 +145,11 @@
处理单个段落
</summary>
</member>
<member name="M:YD_Event.Core.Domain.Certificate.DocxTemplateProcessor.ClearOtherTextElements(System.Collections.Generic.List{System.ValueTuple{DocumentFormat.OpenXml.Wordprocessing.Run,DocumentFormat.OpenXml.Wordprocessing.Text,System.String}},DocumentFormat.OpenXml.Wordprocessing.Run,DocumentFormat.OpenXml.Wordprocessing.Text)">
<summary>
清除其他 Text 元素
</summary>
</member>
<member name="M:YD_Event.Core.Domain.Certificate.DocxTemplateProcessor.ProcessTables(DocumentFormat.OpenXml.Wordprocessing.Body,System.Collections.Generic.Dictionary{System.String,System.String},System.Collections.Generic.Dictionary{System.String,System.String})">
<summary>
处理表格中的占位符