证书转换pdf
This commit is contained in:
parent
3624e6f24b
commit
7fd1cd1435
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
处理表格中的占位符
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user