1482 lines
63 KiB
C#
1482 lines
63 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Windows.Media.Imaging;
|
||
using iText.IO.Font;
|
||
using iText.Kernel.Colors;
|
||
using iText.Kernel.Font;
|
||
using iText.Kernel.Geom;
|
||
using iText.Kernel.Pdf;
|
||
using iText.Kernel.Pdf.Canvas.Draw;
|
||
using iText.Layout;
|
||
using iText.Layout.Borders;
|
||
using iText.Layout.Element;
|
||
using iText.Layout.Properties;
|
||
using XP.Common.Localization.Enums;
|
||
using XP.Common.Localization.Interfaces;
|
||
using XP.Common.Logging.Interfaces;
|
||
using XP.ReportEngine.Interfaces;
|
||
using XP.ReportEngine.Models;
|
||
|
||
namespace XP.ReportEngine.Services
|
||
{
|
||
/// <summary>
|
||
/// iText 7 PDF 渲染器实现 | iText 7 PDF renderer implementation
|
||
/// </summary>
|
||
public class ITextPdfRenderer : IPdfRenderer
|
||
{
|
||
private readonly ILoggerService _logger;
|
||
private readonly ILocalizationService _localizationService;
|
||
|
||
/// <summary>
|
||
/// mm 到 points 的转换系数 | mm to points conversion factor
|
||
/// </summary>
|
||
private const float MmToPoints = 2.83465f;
|
||
|
||
/// <summary>
|
||
/// A4 页面宽度(mm)| A4 page width in mm
|
||
/// </summary>
|
||
private const float A4WidthMm = 210f;
|
||
|
||
/// <summary>
|
||
/// A4 页面高度(mm)| A4 page height in mm
|
||
/// </summary>
|
||
private const float A4HeightMm = 297f;
|
||
|
||
/// <summary>
|
||
/// 表头背景色 | Table header background color
|
||
/// </summary>
|
||
private const string HeaderBackgroundColor = "#E0E0E0";
|
||
|
||
/// <summary>
|
||
/// 表格奇数行背景色 | Table odd row background color
|
||
/// </summary>
|
||
private const string OddRowBackgroundColor = "#FFFFFF";
|
||
|
||
/// <summary>
|
||
/// 表格偶数行背景色 | Table even row background color
|
||
/// </summary>
|
||
private const string EvenRowBackgroundColor = "#F5F5F5";
|
||
|
||
private PdfFont _cjkFont;
|
||
private PdfFont _westernFont;
|
||
private bool _fontsInitialized;
|
||
private readonly object _fontLock = new();
|
||
private ReportTemplate _currentTemplate;
|
||
|
||
public ITextPdfRenderer(ILoggerService logger, ILocalizationService localizationService)
|
||
{
|
||
_logger = logger?.ForModule<ITextPdfRenderer>() ?? throw new ArgumentNullException(nameof(logger));
|
||
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
|
||
// 字体延迟加载,不在构造函数中阻塞 | Fonts loaded lazily, not blocking in constructor
|
||
}
|
||
|
||
#region 7.1 基础 PDF 文档创建 | Basic PDF document creation
|
||
|
||
/// <summary>
|
||
/// 将排版结果渲染为 PDF 内存流 | Render layout result to PDF memory stream
|
||
/// </summary>
|
||
/// <param name="pages">排版后的页面列表 | Laid-out pages</param>
|
||
/// <param name="options">生成选项 | Generation options</param>
|
||
/// <param name="template">绑定后的模板(用于页眉页脚配置)| Bound template (for header/footer config)</param>
|
||
/// <returns>PDF 内存流 | PDF memory stream</returns>
|
||
public MemoryStream Render(List<LayoutPage> pages, ReportGenerationOptions options, ReportTemplate template = null)
|
||
{
|
||
_logger.Info("开始 PDF 渲染,共 {PageCount} 页 | Starting PDF rendering, {PageCount} pages", pages?.Count ?? 0);
|
||
_currentTemplate = template;
|
||
|
||
// 每次渲染重置字体,避免跨 PdfDocument 复用导致 "belongs to other PDF document" 错误
|
||
// Reset fonts on each render to avoid cross-PdfDocument reuse error
|
||
_fontsInitialized = false;
|
||
_cjkFont = null;
|
||
_westernFont = null;
|
||
|
||
var memoryStream = new MemoryStream();
|
||
|
||
try
|
||
{
|
||
var writer = new PdfWriter(memoryStream, new WriterProperties().SetFullCompressionMode(true));
|
||
// 防止 PdfWriter 关闭时关闭底层流 | Prevent PdfWriter from closing the underlying stream
|
||
writer.SetCloseStream(false);
|
||
|
||
var pdfDocument = new PdfDocument(writer);
|
||
// 设置 A4 页面尺寸 | Set A4 page size
|
||
pdfDocument.SetDefaultPageSize(PageSize.A4);
|
||
|
||
var document = new Document(pdfDocument);
|
||
|
||
// 设置默认边距(使用默认 20mm)| Set default margins (20mm default)
|
||
float marginTop = 20f * MmToPoints;
|
||
float marginBottom = 20f * MmToPoints;
|
||
float marginLeft = 20f * MmToPoints;
|
||
float marginRight = 20f * MmToPoints;
|
||
|
||
// 如果有页眉页脚配置,为内容页增加边距空间 | Increase margins for header/footer on content pages
|
||
var headerConfig = template?.Document?.Header;
|
||
var footerConfig = template?.Document?.Footer;
|
||
bool hasHeader = headerConfig != null && headerConfig.Enabled;
|
||
bool hasFooter = footerConfig != null && footerConfig.Enabled;
|
||
|
||
// 页眉页脚占用的额外空间(mm → points)| Extra space for header/footer
|
||
float headerAreaHeight = hasHeader ? 15f * MmToPoints : 0f;
|
||
float footerAreaHeight = hasFooter ? 12f * MmToPoints : 0f;
|
||
|
||
document.SetMargins(marginTop, marginRight, marginBottom, marginLeft);
|
||
|
||
// 注册页眉页脚事件处理器 | Register header/footer event handler
|
||
HeaderFooterEventHandler headerFooterHandler = null;
|
||
if (hasHeader || hasFooter)
|
||
{
|
||
headerFooterHandler = new HeaderFooterEventHandler(
|
||
this, template, pages, _logger);
|
||
pdfDocument.AddEventHandler(iText.Kernel.Events.PdfDocumentEvent.END_PAGE, headerFooterHandler);
|
||
}
|
||
|
||
if (pages != null && pages.Count > 0)
|
||
{
|
||
for (int i = 0; i < pages.Count; i++)
|
||
{
|
||
var pageStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||
|
||
if (i > 0)
|
||
{
|
||
// 添加新页面 | Add new page
|
||
document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
|
||
}
|
||
|
||
// 非首页增加页眉页脚边距 | Add header/footer margins for non-homepage
|
||
bool isHomepage = string.Equals(pages[i].PageType, "homepage", StringComparison.OrdinalIgnoreCase);
|
||
if (!isHomepage)
|
||
{
|
||
// 通过添加顶部间距为页眉留出空间 | Add top spacing for header area
|
||
if (hasHeader)
|
||
{
|
||
document.Add(new Paragraph("").SetMarginBottom(headerAreaHeight).SetFontSize(1));
|
||
}
|
||
}
|
||
|
||
RenderPage(document, pages[i]);
|
||
|
||
pageStopwatch.Stop();
|
||
_logger.Info("第 {PageIndex}/{TotalPages} 页渲染完成,类型: {PageType},元素数: {ElementCount},耗时: {ElapsedMs}ms | Page {PageIndex}/{TotalPages} rendered, type: {PageType}, elements: {ElementCount}, elapsed: {ElapsedMs}ms",
|
||
i + 1, pages.Count, pages[i].PageType ?? "unknown", pages[i].Elements?.Count ?? 0, pageStopwatch.ElapsedMilliseconds);
|
||
}
|
||
}
|
||
|
||
// 文档关闭前回填总页数占位符 | Fill total page count placeholder before closing
|
||
if (headerFooterHandler != null)
|
||
{
|
||
headerFooterHandler.WriteTotal(pdfDocument);
|
||
}
|
||
|
||
// 关闭文档(触发字体子集化嵌入 + PDF 交叉引用表写入 + 流压缩)
|
||
// Close document (triggers font subsetting + PDF cross-reference table writing + stream compression)
|
||
_logger.Info("开始关闭文档(字体嵌入 + 压缩)| Starting document close (font embedding + compression)");
|
||
var closeStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||
document.Close();
|
||
closeStopwatch.Stop();
|
||
_logger.Info("文档关闭完成,耗时: {ElapsedMs}ms | Document close completed, elapsed: {ElapsedMs}ms", closeStopwatch.ElapsedMilliseconds);
|
||
|
||
// 重置流位置以便后续读取 | Reset stream position for subsequent reading
|
||
memoryStream.Position = 0;
|
||
_logger.Info("PDF 渲染完成 | PDF rendering completed");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error(ex, "PDF 渲染过程中发生错误 | Error occurred during PDF rendering: {Message}", ex.Message);
|
||
throw;
|
||
}
|
||
|
||
return memoryStream;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 渲染单个页面 | Render a single page
|
||
/// </summary>
|
||
private void RenderPage(Document document, LayoutPage page)
|
||
{
|
||
if (page?.Elements == null) return;
|
||
|
||
foreach (var element in page.Elements)
|
||
{
|
||
try
|
||
{
|
||
RenderElement(document, element);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("渲染元素失败,跳过该元素 | Failed to render element, skipping: {Message}", ex.Message);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据元素类型分发渲染 | Dispatch rendering based on element type
|
||
/// </summary>
|
||
private void RenderElement(Document document, LayoutElement element)
|
||
{
|
||
if (element?.Source == null) return;
|
||
|
||
var elementType = element.Source.Type?.ToLowerInvariant();
|
||
|
||
switch (elementType)
|
||
{
|
||
case "text":
|
||
RenderTextElement(document, element);
|
||
break;
|
||
case "image":
|
||
RenderImageElement(document, element);
|
||
break;
|
||
case "table":
|
||
RenderTableElement(document, element);
|
||
break;
|
||
case "divider":
|
||
RenderDividerElement(document, element);
|
||
break;
|
||
case "spacer":
|
||
RenderSpacerElement(document, element);
|
||
break;
|
||
case "row":
|
||
RenderRowElement(document, element);
|
||
break;
|
||
case "pagebreak":
|
||
RenderPageBreakElement(document);
|
||
break;
|
||
default:
|
||
_logger.Warn("未知的元素类型:{Type},跳过渲染 | Unknown element type: {Type}, skipping", elementType);
|
||
break;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.2 文本元素渲染 | Text element rendering
|
||
|
||
/// <summary>
|
||
/// 渲染文本元素 | Render text element
|
||
/// </summary>
|
||
private void RenderTextElement(Document document, LayoutElement element)
|
||
{
|
||
var content = element.ResolvedContent ?? string.Empty;
|
||
var style = element.ResolvedStyle ?? new StyleDefinition();
|
||
|
||
var paragraph = new Paragraph(content);
|
||
|
||
// 设置紧凑的默认段落间距 | Set compact default paragraph spacing
|
||
paragraph.SetMarginTop(0);
|
||
paragraph.SetMarginBottom(2f);
|
||
|
||
// 应用字体 | Apply font
|
||
var font = GetFontForCurrentLanguage();
|
||
paragraph.SetFont(font);
|
||
|
||
// 应用字体大小 | Apply font size
|
||
paragraph.SetFontSize(style.Size);
|
||
|
||
// 应用粗体 | Apply bold
|
||
if (style.Bold)
|
||
{
|
||
paragraph.SetBold();
|
||
}
|
||
|
||
// 应用斜体 | Apply italic
|
||
if (style.Italic)
|
||
{
|
||
paragraph.SetItalic();
|
||
}
|
||
|
||
// 应用字体颜色 | Apply font color
|
||
var color = ParseColor(style.Color);
|
||
if (color != null)
|
||
{
|
||
paragraph.SetFontColor(color);
|
||
}
|
||
|
||
// 应用条件颜色规则(根据内容关键词覆盖颜色)| Apply conditional color rules (override color by content keywords)
|
||
if (element.Source?.ColorRules != null && !string.IsNullOrEmpty(content))
|
||
{
|
||
foreach (var rule in element.Source.ColorRules)
|
||
{
|
||
if (content.Contains(rule.Key, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var ruleColor = ParseColor(rule.Value);
|
||
if (ruleColor != null)
|
||
{
|
||
paragraph.SetFontColor(ruleColor);
|
||
paragraph.SetBold();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 应用对齐方式 | Apply text alignment
|
||
paragraph.SetTextAlignment(ParseTextAlignment(style.Align));
|
||
|
||
// 应用背景色 | Apply background color
|
||
if (!string.IsNullOrEmpty(style.BackgroundColor))
|
||
{
|
||
var bgColor = ParseColor(style.BackgroundColor);
|
||
if (bgColor != null)
|
||
{
|
||
paragraph.SetBackgroundColor(bgColor);
|
||
}
|
||
}
|
||
|
||
// 设置固定位置(如果有坐标信息)| Set fixed position if coordinates available
|
||
if (element.Width > 0)
|
||
{
|
||
paragraph.SetWidth(element.Width * MmToPoints);
|
||
}
|
||
|
||
// 应用边距和缩进 | Apply margins and indent
|
||
if (style.MarginTop > 0)
|
||
paragraph.SetMarginTop(style.MarginTop * MmToPoints);
|
||
if (style.MarginBottom > 0)
|
||
paragraph.SetMarginBottom(style.MarginBottom * MmToPoints);
|
||
if (style.PaddingLeft > 0)
|
||
paragraph.SetPaddingLeft(style.PaddingLeft * MmToPoints);
|
||
if (style.LineHeight > 0)
|
||
paragraph.SetMultipliedLeading(style.LineHeight);
|
||
|
||
document.Add(paragraph);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.3 字体管理 | Font management
|
||
|
||
/// <summary>
|
||
/// 确保字体已初始化(线程安全的延迟加载)| Ensure fonts are initialized (thread-safe lazy loading)
|
||
/// </summary>
|
||
private void EnsureFontsInitialized()
|
||
{
|
||
if (_fontsInitialized) return;
|
||
lock (_fontLock)
|
||
{
|
||
if (_fontsInitialized) return;
|
||
InitializeFonts();
|
||
_fontsInitialized = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化字体(从系统字体目录加载)| Initialize fonts (load from system fonts directory)
|
||
/// 使用 Windows 系统自带字体,确保 Telerik RadPdfViewer 兼容性
|
||
/// Uses Windows built-in fonts to ensure Telerik RadPdfViewer compatibility
|
||
/// </summary>
|
||
private void InitializeFonts()
|
||
{
|
||
var fontsDir = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts");
|
||
|
||
// 加载微软雅黑(支持简体中文、繁体中文)| Load Microsoft YaHei (supports Simplified & Traditional Chinese)
|
||
try
|
||
{
|
||
var msyhPath = System.IO.Path.Combine(fontsDir, "msyh.ttc");
|
||
_cjkFont = PdfFontFactory.CreateFont(msyhPath + ",0", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
|
||
_logger.Info("中文字体加载成功(微软雅黑)| Chinese font loaded successfully (Microsoft YaHei)");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("微软雅黑加载失败,尝试后备字体 | Microsoft YaHei load failed, trying fallback: {Message}", ex.Message);
|
||
try
|
||
{
|
||
// 后备:宋体 | Fallback: SimSun
|
||
var simsunPath = System.IO.Path.Combine(fontsDir, "simsun.ttc");
|
||
_cjkFont = PdfFontFactory.CreateFont(simsunPath + ",0", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
|
||
_logger.Info("中文后备字体加载成功(宋体)| Chinese fallback font loaded successfully (SimSun)");
|
||
}
|
||
catch (Exception ex2)
|
||
{
|
||
_logger.Warn("宋体加载失败 | SimSun load failed: {Message}", ex2.Message);
|
||
_cjkFont = null;
|
||
}
|
||
}
|
||
|
||
// 加载 Arial(西文字体)| Load Arial (Western font)
|
||
try
|
||
{
|
||
var arialPath = System.IO.Path.Combine(fontsDir, "arial.ttf");
|
||
_westernFont = PdfFontFactory.CreateFont(arialPath, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
|
||
_logger.Info("西文字体加载成功(Arial)| Western font loaded successfully (Arial)");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("Arial 加载失败,使用 Helvetica 后备 | Arial load failed, using Helvetica fallback: {Message}", ex.Message);
|
||
_westernFont = null;
|
||
}
|
||
|
||
// 如果系统字体都不可用,使用 iText 内置 Helvetica | If system fonts unavailable, use built-in Helvetica
|
||
if (_cjkFont == null && _westernFont == null)
|
||
{
|
||
_logger.Warn("所有系统字体不可用,使用 Helvetica 后备字体 | All system fonts unavailable, using Helvetica fallback");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据当前语言获取合适的字体 | Get appropriate font based on current language
|
||
/// zh-CN / zh-TW → 微软雅黑(支持简繁体);en-US → Arial
|
||
/// </summary>
|
||
/// <returns>PDF 字体 | PDF font</returns>
|
||
private PdfFont GetFontForCurrentLanguage()
|
||
{
|
||
// 延迟初始化字体 | Lazy initialize fonts
|
||
EnsureFontsInitialized();
|
||
|
||
var language = _localizationService.CurrentLanguage;
|
||
|
||
PdfFont selectedFont;
|
||
switch (language)
|
||
{
|
||
case SupportedLanguage.ZhCN:
|
||
case SupportedLanguage.ZhTW:
|
||
selectedFont = _cjkFont;
|
||
break;
|
||
case SupportedLanguage.EnUS:
|
||
default:
|
||
selectedFont = _westernFont ?? _cjkFont; // 西文优先,中文后备(微软雅黑也支持西文)| Western preferred, CJK fallback (YaHei supports Western too)
|
||
break;
|
||
}
|
||
|
||
// 后备字体逻辑 | Fallback font logic
|
||
if (selectedFont != null)
|
||
{
|
||
return selectedFont;
|
||
}
|
||
|
||
// 尝试使用其他字体 | Try other fonts
|
||
if (_cjkFont != null) return _cjkFont;
|
||
if (_westernFont != null) return _westernFont;
|
||
|
||
// 最终后备:使用 iText 内置 Helvetica | Final fallback: use built-in Helvetica
|
||
return PdfFontFactory.CreateFont(iText.IO.Font.Constants.StandardFonts.HELVETICA);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.4 图像嵌入渲染 | Image embedding rendering
|
||
|
||
/// <summary>
|
||
/// 渲染图像元素 | Render image element
|
||
/// </summary>
|
||
private void RenderImageElement(Document document, LayoutElement element)
|
||
{
|
||
var imageData = element.ResolvedImage;
|
||
|
||
// 如果图像数据缺失,渲染占位矩形 | If image data is missing, render placeholder
|
||
if (imageData == null)
|
||
{
|
||
_logger.Warn("图像数据为空,渲染占位矩形 | Image data is null, rendering placeholder");
|
||
RenderImagePlaceholder(document, element);
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
byte[] imageBytes = GetImageBytes(imageData);
|
||
|
||
if (imageBytes == null || imageBytes.Length == 0)
|
||
{
|
||
_logger.Warn("图像字节数据为空,渲染占位矩形 | Image byte data is empty, rendering placeholder");
|
||
RenderImagePlaceholder(document, element);
|
||
return;
|
||
}
|
||
|
||
// 创建 iText 图像对象 | Create iText image object
|
||
var iTextImageData = iText.IO.Image.ImageDataFactory.Create(imageBytes);
|
||
var image = new Image(iTextImageData);
|
||
|
||
// 计算目标区域尺寸(mm → points)| Calculate target area size (mm → points)
|
||
float targetWidthPt = element.Width * MmToPoints;
|
||
float targetHeightPt = element.Height * MmToPoints;
|
||
|
||
// 等比缩放以适应目标区域 | Scale proportionally to fit target area
|
||
if (targetWidthPt > 0 && targetHeightPt > 0)
|
||
{
|
||
float imageWidth = image.GetImageWidth();
|
||
float imageHeight = image.GetImageHeight();
|
||
|
||
float scaleX = targetWidthPt / imageWidth;
|
||
float scaleY = targetHeightPt / imageHeight;
|
||
float scale = Math.Min(scaleX, scaleY);
|
||
|
||
image.SetWidth(imageWidth * scale);
|
||
image.SetHeight(imageHeight * scale);
|
||
}
|
||
else if (targetWidthPt > 0)
|
||
{
|
||
image.SetWidth(targetWidthPt);
|
||
image.ScaleToFit(targetWidthPt, float.MaxValue);
|
||
}
|
||
|
||
// 应用边框 | Apply border
|
||
if (element.Source?.Border == true)
|
||
{
|
||
image.SetBorder(new SolidBorder(ColorConstants.BLACK, 1f));
|
||
}
|
||
|
||
// 应用对齐方式 | Apply alignment
|
||
var align = element.Source?.Align?.ToLowerInvariant();
|
||
if (align == "center")
|
||
{
|
||
image.SetHorizontalAlignment(HorizontalAlignment.CENTER);
|
||
}
|
||
else if (align == "right")
|
||
{
|
||
image.SetHorizontalAlignment(HorizontalAlignment.RIGHT);
|
||
}
|
||
|
||
// 应用样式中的边距 | Apply margins from style
|
||
var style = element.ResolvedStyle;
|
||
if (style != null)
|
||
{
|
||
if (style.MarginTop > 0)
|
||
image.SetMarginTop(style.MarginTop * MmToPoints);
|
||
if (style.MarginBottom > 0)
|
||
image.SetMarginBottom(style.MarginBottom * MmToPoints);
|
||
}
|
||
|
||
document.Add(image);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("图像渲染失败,渲染占位矩形 | Image rendering failed, rendering placeholder: {Message}", ex.Message);
|
||
RenderImagePlaceholder(document, element);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 ImageData 获取字节数组 | Get byte array from ImageData
|
||
/// </summary>
|
||
private byte[] GetImageBytes(ImageData imageData)
|
||
{
|
||
switch (imageData.SourceType)
|
||
{
|
||
case ImageSourceType.Bytes:
|
||
return imageData.Bytes;
|
||
|
||
case ImageSourceType.FilePath:
|
||
if (!string.IsNullOrEmpty(imageData.FilePath) && File.Exists(imageData.FilePath))
|
||
{
|
||
return File.ReadAllBytes(imageData.FilePath);
|
||
}
|
||
_logger.Warn("图像文件不存在:{Path} | Image file not found: {Path}", imageData.FilePath);
|
||
return null;
|
||
|
||
case ImageSourceType.BitmapSource:
|
||
if (imageData.BitmapSource is BitmapSource bitmapSource)
|
||
{
|
||
return ConvertBitmapSourceToBytes(bitmapSource);
|
||
}
|
||
_logger.Warn("BitmapSource 对象无效 | BitmapSource object is invalid");
|
||
return null;
|
||
|
||
default:
|
||
_logger.Warn("未知的图像来源类型:{Type} | Unknown image source type: {Type}", imageData.SourceType);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.5 BitmapSource 转 byte[] | BitmapSource to byte[] conversion
|
||
|
||
/// <summary>
|
||
/// 将 BitmapSource 转换为 PNG 编码的字节数组 | Convert BitmapSource to PNG-encoded byte array
|
||
/// </summary>
|
||
/// <param name="bitmapSource">WPF BitmapSource 对象 | WPF BitmapSource object</param>
|
||
/// <returns>PNG 编码的字节数组 | PNG-encoded byte array</returns>
|
||
public static byte[] ConvertBitmapSourceToBytes(BitmapSource bitmapSource)
|
||
{
|
||
if (bitmapSource == null)
|
||
{
|
||
return null;
|
||
}
|
||
|
||
using (var memoryStream = new MemoryStream())
|
||
{
|
||
var encoder = new PngBitmapEncoder();
|
||
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
|
||
encoder.Save(memoryStream);
|
||
return memoryStream.ToArray();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.6 图像占位矩形渲染 | Image placeholder rendering
|
||
|
||
/// <summary>
|
||
/// 渲染图像缺失时的占位矩形(带"无图像 | No Image"文本标签)
|
||
/// Render placeholder rectangle when image is missing (with "无图像 | No Image" text label)
|
||
/// </summary>
|
||
private void RenderImagePlaceholder(Document document, LayoutElement element)
|
||
{
|
||
float widthPt = element.Width > 0 ? element.Width * MmToPoints : 100f * MmToPoints;
|
||
float heightPt = element.Height > 0 ? element.Height * MmToPoints : 60f * MmToPoints;
|
||
|
||
// 使用表格模拟占位矩形(带边框和居中文本)| Use table to simulate placeholder rectangle
|
||
var table = new Table(1);
|
||
table.SetWidth(widthPt);
|
||
|
||
var cell = new Cell();
|
||
cell.SetHeight(heightPt);
|
||
cell.SetBorder(new SolidBorder(ColorConstants.GRAY, 1f));
|
||
cell.SetBackgroundColor(new DeviceRgb(245, 245, 245));
|
||
|
||
// 居中显示"无图像 | No Image"文本 | Center "无图像 | No Image" text
|
||
var placeholderText = new Paragraph("无图像 | No Image");
|
||
var font = GetFontForCurrentLanguage();
|
||
placeholderText.SetFont(font);
|
||
placeholderText.SetFontSize(10f);
|
||
placeholderText.SetFontColor(ColorConstants.GRAY);
|
||
placeholderText.SetTextAlignment(TextAlignment.CENTER);
|
||
|
||
cell.SetVerticalAlignment(VerticalAlignment.MIDDLE);
|
||
cell.Add(placeholderText);
|
||
table.AddCell(cell);
|
||
|
||
document.Add(table);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.7 表格渲染 | Table rendering
|
||
|
||
/// <summary>
|
||
/// 渲染表格元素 | Render table element
|
||
/// </summary>
|
||
private void RenderTableElement(Document document, LayoutElement element)
|
||
{
|
||
var columns = element.Source?.Columns;
|
||
var tableData = element.ResolvedTableData;
|
||
|
||
if (columns == null || columns.Count == 0)
|
||
{
|
||
_logger.Warn("表格列定义为空,跳过渲染 | Table column definitions are empty, skipping");
|
||
return;
|
||
}
|
||
|
||
// 计算列宽(mm → points)| Calculate column widths (mm → points)
|
||
var columnWidths = new float[columns.Count];
|
||
for (int i = 0; i < columns.Count; i++)
|
||
{
|
||
columnWidths[i] = columns[i].Width > 0 ? columns[i].Width * MmToPoints : 30f * MmToPoints;
|
||
}
|
||
|
||
var table = new Table(columnWidths);
|
||
table.SetWidth(UnitValue.CreatePercentValue(100));
|
||
|
||
var font = GetFontForCurrentLanguage();
|
||
|
||
// 渲染表头行 | Render header row
|
||
var headerBgColor = ParseColor(HeaderBackgroundColor);
|
||
foreach (var column in columns)
|
||
{
|
||
var headerCell = new Cell();
|
||
var headerParagraph = new Paragraph(column.Header ?? string.Empty);
|
||
headerParagraph.SetFont(font);
|
||
headerParagraph.SetFontSize(10f);
|
||
headerParagraph.SetBold();
|
||
headerParagraph.SetTextAlignment(ParseTextAlignment(column.Align));
|
||
|
||
headerCell.Add(headerParagraph);
|
||
headerCell.SetBackgroundColor(headerBgColor);
|
||
headerCell.SetBorder(new SolidBorder(ColorConstants.LIGHT_GRAY, 0.5f));
|
||
|
||
table.AddHeaderCell(headerCell);
|
||
}
|
||
|
||
// 渲染数据行(交替背景色)| Render data rows (alternating background colors)
|
||
if (tableData != null)
|
||
{
|
||
for (int rowIndex = 0; rowIndex < tableData.Count; rowIndex++)
|
||
{
|
||
var rowData = tableData[rowIndex];
|
||
var rowBgColor = rowIndex % 2 == 0
|
||
? ParseColor(OddRowBackgroundColor)
|
||
: ParseColor(EvenRowBackgroundColor);
|
||
|
||
foreach (var column in columns)
|
||
{
|
||
var dataCell = new Cell();
|
||
|
||
// 从行数据中获取字段值 | Get field value from row data
|
||
string cellValue = string.Empty;
|
||
if (rowData != null && !string.IsNullOrEmpty(column.Field) && rowData.ContainsKey(column.Field))
|
||
{
|
||
cellValue = rowData[column.Field]?.ToString() ?? string.Empty;
|
||
}
|
||
|
||
var cellParagraph = new Paragraph(cellValue);
|
||
cellParagraph.SetFont(font);
|
||
cellParagraph.SetFontSize(9f);
|
||
cellParagraph.SetTextAlignment(ParseTextAlignment(column.Align));
|
||
|
||
// 应用条件颜色规则 | Apply conditional color rules
|
||
if (column.ColorRules != null && !string.IsNullOrEmpty(cellValue))
|
||
{
|
||
foreach (var rule in column.ColorRules)
|
||
{
|
||
if (string.Equals(cellValue, rule.Key, StringComparison.OrdinalIgnoreCase)
|
||
|| cellValue.Contains(rule.Key, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var ruleColor = ParseColor(rule.Value);
|
||
if (ruleColor != null)
|
||
{
|
||
cellParagraph.SetFontColor(ruleColor);
|
||
cellParagraph.SetBold();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
dataCell.Add(cellParagraph);
|
||
dataCell.SetBackgroundColor(rowBgColor);
|
||
dataCell.SetBorder(new SolidBorder(ColorConstants.LIGHT_GRAY, 0.5f));
|
||
|
||
table.AddCell(dataCell);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 应用样式中的边距 | Apply margins from style
|
||
var style = element.ResolvedStyle;
|
||
if (style != null)
|
||
{
|
||
if (style.MarginTop > 0)
|
||
table.SetMarginTop(style.MarginTop * MmToPoints);
|
||
if (style.MarginBottom > 0)
|
||
table.SetMarginBottom(style.MarginBottom * MmToPoints);
|
||
}
|
||
|
||
document.Add(table);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.8 分隔线渲染 | Divider rendering
|
||
|
||
/// <summary>
|
||
/// 渲染分隔线元素 | Render divider element
|
||
/// </summary>
|
||
private void RenderDividerElement(Document document, LayoutElement element)
|
||
{
|
||
var style = element.ResolvedStyle;
|
||
|
||
// 确定分隔线颜色(从样式或默认灰色)| Determine divider color (from style or default gray)
|
||
Color lineColor = ColorConstants.GRAY;
|
||
if (style != null && !string.IsNullOrEmpty(style.Color))
|
||
{
|
||
var parsedColor = ParseColor(style.Color);
|
||
if (parsedColor != null)
|
||
{
|
||
lineColor = parsedColor;
|
||
}
|
||
}
|
||
|
||
// 使用 LineSeparator 渲染水平分隔线 | Use LineSeparator to render horizontal divider
|
||
var lineSeparator = new LineSeparator(new SolidLine(1f));
|
||
lineSeparator.SetStrokeColor(lineColor);
|
||
|
||
// 设置宽度为可用区域全宽 | Set width to full available area
|
||
if (element.Width > 0)
|
||
{
|
||
lineSeparator.SetWidth(element.Width * MmToPoints);
|
||
}
|
||
|
||
// 添加上下间距 | Add vertical spacing
|
||
lineSeparator.SetMarginTop(5f);
|
||
lineSeparator.SetMarginBottom(5f);
|
||
|
||
document.Add(lineSeparator);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 渲染空白间距元素 | Render spacer element
|
||
/// 通过 Size[1](高度,mm)控制垂直空白大小
|
||
/// Controls vertical whitespace via Size[1] (height in mm)
|
||
/// </summary>
|
||
private void RenderSpacerElement(Document document, LayoutElement element)
|
||
{
|
||
// 从 Size[1] 获取高度,默认 10mm | Get height from Size[1], default 10mm
|
||
float heightMm = 10f;
|
||
if (element.Source?.Size is { Length: >= 2 })
|
||
{
|
||
heightMm = element.Source.Size[1];
|
||
}
|
||
|
||
// 使用空段落撑出指定高度的空白 | Use empty paragraph to create specified height whitespace
|
||
var spacer = new Paragraph("")
|
||
.SetFontSize(1)
|
||
.SetMarginTop(0)
|
||
.SetMarginBottom(heightMm * MmToPoints);
|
||
|
||
document.Add(spacer);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 渲染强制分页元素 | Render forced page break element
|
||
/// </summary>
|
||
private void RenderPageBreakElement(Document document)
|
||
{
|
||
document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 渲染行容器元素(水平布局)| Render row container element (horizontal layout)
|
||
/// 使用无边框表格实现子元素的水平排列,支持 left/center/right 对齐
|
||
/// Uses borderless table to arrange child elements horizontally, supports left/center/right alignment
|
||
/// </summary>
|
||
private void RenderRowElement(Document document, LayoutElement element)
|
||
{
|
||
var children = element.Source?.Children;
|
||
if (children == null || children.Count == 0) return;
|
||
|
||
// 创建表格,支持自定义列宽比例 | Create table with custom column width ratios
|
||
var columnCount = children.Count;
|
||
Table table;
|
||
|
||
if (element.Source.Widths != null && element.Source.Widths.Length == columnCount)
|
||
{
|
||
// 使用指定的列宽比例 | Use specified column width ratios
|
||
var totalRatio = 0f;
|
||
foreach (var w in element.Source.Widths) totalRatio += w;
|
||
|
||
var columnWidths = new float[columnCount];
|
||
for (int i = 0; i < columnCount; i++)
|
||
{
|
||
columnWidths[i] = element.Source.Widths[i] / totalRatio;
|
||
}
|
||
table = new Table(UnitValue.CreatePercentArray(columnWidths));
|
||
}
|
||
else
|
||
{
|
||
// 均分列宽 | Equal column widths
|
||
table = new Table(columnCount);
|
||
}
|
||
|
||
table.UseAllAvailableWidth();
|
||
table.SetBorder(iText.Layout.Borders.Border.NO_BORDER);
|
||
|
||
var font = GetFontForCurrentLanguage();
|
||
|
||
foreach (var child in children)
|
||
{
|
||
var cell = new Cell();
|
||
cell.SetBorder(iText.Layout.Borders.Border.NO_BORDER);
|
||
cell.SetPadding(0);
|
||
|
||
// 确定子元素对齐方式 | Determine child element alignment
|
||
var align = child.Align?.ToLowerInvariant() ?? "left";
|
||
cell.SetTextAlignment(ParseTextAlignment(align));
|
||
|
||
var childType = child.Type?.ToLowerInvariant();
|
||
|
||
if (childType == "column")
|
||
{
|
||
// 渲染列容器子元素(垂直堆叠多个元素在同一单元格内)
|
||
// Render column container child (stack multiple elements vertically in same cell)
|
||
if (child.Children != null)
|
||
{
|
||
foreach (var subChild in child.Children)
|
||
{
|
||
RenderRowChildIntoCell(cell, subChild, align, font);
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
RenderRowChildIntoCell(cell, child, align, font);
|
||
}
|
||
|
||
table.AddCell(cell);
|
||
}
|
||
|
||
document.Add(table);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将单个子元素渲染到单元格中 | Render a single child element into a cell
|
||
/// </summary>
|
||
private void RenderRowChildIntoCell(Cell cell, TemplateElement child, string align, PdfFont font)
|
||
{
|
||
var childType = child.Type?.ToLowerInvariant();
|
||
// 子元素可以覆盖父级对齐 | Child can override parent alignment
|
||
var childAlign = child.Align?.ToLowerInvariant() ?? align;
|
||
|
||
if (childType == "image")
|
||
{
|
||
// 渲染图像子元素 | Render image child element
|
||
var imageData = child.ImageData;
|
||
if (imageData != null)
|
||
{
|
||
try
|
||
{
|
||
byte[] imageBytes = GetImageBytes(imageData);
|
||
if (imageBytes != null && imageBytes.Length > 0)
|
||
{
|
||
var iTextImageData = iText.IO.Image.ImageDataFactory.Create(imageBytes);
|
||
var image = new Image(iTextImageData);
|
||
|
||
// 应用尺寸 | Apply size
|
||
float targetWidthPt = child.Size != null && child.Size.Length > 0 ? child.Size[0] * MmToPoints : 0;
|
||
float targetHeightPt = child.Size != null && child.Size.Length > 1 ? child.Size[1] * MmToPoints : 0;
|
||
|
||
if (targetWidthPt > 0 && targetHeightPt > 0)
|
||
{
|
||
float imageWidth = image.GetImageWidth();
|
||
float imageHeight = image.GetImageHeight();
|
||
float scaleX = targetWidthPt / imageWidth;
|
||
float scaleY = targetHeightPt / imageHeight;
|
||
float scale = Math.Min(scaleX, scaleY);
|
||
image.SetWidth(imageWidth * scale);
|
||
image.SetHeight(imageHeight * scale);
|
||
}
|
||
|
||
// 设置图像水平对齐 | Set image horizontal alignment
|
||
if (childAlign == "right")
|
||
image.SetHorizontalAlignment(HorizontalAlignment.RIGHT);
|
||
else if (childAlign == "center")
|
||
image.SetHorizontalAlignment(HorizontalAlignment.CENTER);
|
||
else
|
||
image.SetHorizontalAlignment(HorizontalAlignment.LEFT);
|
||
|
||
cell.Add(image);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("Row 子元素图像渲染失败 | Row child image rendering failed: {Message}", ex.Message);
|
||
}
|
||
}
|
||
}
|
||
else if (childType == "text")
|
||
{
|
||
// 渲染文本子元素 | Render text child element
|
||
var content = child.Content ?? string.Empty;
|
||
var style = ResolveStyleFromTemplate(child.Style);
|
||
|
||
var paragraph = new Paragraph(content);
|
||
paragraph.SetFont(font);
|
||
paragraph.SetFontSize(style.Size);
|
||
if (style.Bold) paragraph.SetBold();
|
||
if (style.Italic) paragraph.SetItalic();
|
||
|
||
var color = ParseColor(style.Color);
|
||
if (color != null) paragraph.SetFontColor(color);
|
||
|
||
// 应用条件颜色规则 | Apply conditional color rules
|
||
if (child.ColorRules != null && !string.IsNullOrEmpty(content))
|
||
{
|
||
foreach (var rule in child.ColorRules)
|
||
{
|
||
if (content.Contains(rule.Key, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
var ruleColor = ParseColor(rule.Value);
|
||
if (ruleColor != null)
|
||
{
|
||
paragraph.SetFontColor(ruleColor);
|
||
paragraph.SetBold();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
paragraph.SetTextAlignment(ParseTextAlignment(childAlign));
|
||
paragraph.SetMargin(0);
|
||
|
||
cell.Add(paragraph);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从当前模板中解析样式定义 | Resolve style definition from current template
|
||
/// </summary>
|
||
private StyleDefinition ResolveStyleFromTemplate(string styleName)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(styleName))
|
||
return new StyleDefinition();
|
||
|
||
if (_currentTemplate?.Styles != null
|
||
&& _currentTemplate.Styles.TryGetValue(styleName, out var style))
|
||
{
|
||
return style;
|
||
}
|
||
|
||
return new StyleDefinition();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.85 页眉页脚渲染(事件驱动)| Header/Footer rendering (event-driven)
|
||
|
||
/// <summary>
|
||
/// 页眉页脚事件处理器 | Header/Footer event handler
|
||
/// 在 END_PAGE 事件中绘制页眉页脚,使用 PdfFormXObject 占位符实现总页数回填
|
||
/// Draws header/footer in END_PAGE event, uses PdfFormXObject placeholder for total page count
|
||
/// </summary>
|
||
private class HeaderFooterEventHandler : iText.Kernel.Events.IEventHandler
|
||
{
|
||
private readonly ITextPdfRenderer _renderer;
|
||
private readonly ReportTemplate _template;
|
||
private readonly List<LayoutPage> _pages;
|
||
private readonly ILoggerService _logger;
|
||
private readonly HeaderFooterSettings _headerConfig;
|
||
private readonly HeaderFooterSettings _footerConfig;
|
||
private readonly MarginSettings _margins;
|
||
private readonly PdfFont _font;
|
||
|
||
// 首页数量 | Homepage count
|
||
private readonly int _homepageCount;
|
||
|
||
// 总页数占位符模板(用于回填)| Total page count placeholder template (for backfill)
|
||
private readonly iText.Kernel.Pdf.Xobject.PdfFormXObject _totalPagePlaceholder;
|
||
private readonly List<(iText.Kernel.Pdf.Canvas.PdfCanvas canvas, float x, float y)> _totalPagePositions = new();
|
||
|
||
// 当前页面索引(从 0 开始)| Current page index (0-based)
|
||
private int _currentPageIndex = -1;
|
||
|
||
public HeaderFooterEventHandler(
|
||
ITextPdfRenderer renderer,
|
||
ReportTemplate template,
|
||
List<LayoutPage> pages,
|
||
ILoggerService logger)
|
||
{
|
||
_renderer = renderer;
|
||
_template = template;
|
||
_pages = pages;
|
||
_logger = logger;
|
||
_headerConfig = template?.Document?.Header;
|
||
_footerConfig = template?.Document?.Footer;
|
||
_margins = template?.Document?.Margins ?? new MarginSettings();
|
||
_font = renderer.GetFontForCurrentLanguage();
|
||
|
||
// 计算首页数量 | Calculate homepage count
|
||
_homepageCount = 0;
|
||
if (pages != null)
|
||
{
|
||
for (int i = 0; i < pages.Count; i++)
|
||
{
|
||
if (string.Equals(pages[i].PageType, "homepage", StringComparison.OrdinalIgnoreCase))
|
||
_homepageCount++;
|
||
else
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 创建总页数占位符(固定宽度区域)| Create total page count placeholder (fixed width area)
|
||
_totalPagePlaceholder = new iText.Kernel.Pdf.Xobject.PdfFormXObject(new Rectangle(0, 0, 30, 12));
|
||
}
|
||
|
||
public void HandleEvent(iText.Kernel.Events.Event @event)
|
||
{
|
||
if (@event is not iText.Kernel.Events.PdfDocumentEvent docEvent) return;
|
||
|
||
_currentPageIndex++;
|
||
var pdfDoc = docEvent.GetDocument();
|
||
var pdfPage = docEvent.GetPage();
|
||
var pageSize = pdfPage.GetPageSize();
|
||
|
||
// 跳过首页 | Skip homepage
|
||
if (_currentPageIndex < _homepageCount) return;
|
||
|
||
int currentContentPageNum = _currentPageIndex - _homepageCount + 1;
|
||
|
||
try
|
||
{
|
||
var canvas = new iText.Kernel.Pdf.Canvas.PdfCanvas(pdfPage.NewContentStreamBefore(), pdfPage.GetResources(), pdfDoc);
|
||
|
||
// 绘制页眉 | Draw header
|
||
if (_headerConfig != null && _headerConfig.Enabled)
|
||
{
|
||
DrawHeader(canvas, pageSize);
|
||
}
|
||
|
||
// 绘制页脚 | Draw footer
|
||
if (_footerConfig != null && _footerConfig.Enabled)
|
||
{
|
||
DrawFooter(canvas, pageSize, pdfDoc, currentContentPageNum);
|
||
}
|
||
|
||
canvas.Release();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("页眉页脚绘制异常 | Header/footer drawing exception: {Message}", ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制页眉 | Draw header
|
||
/// </summary>
|
||
private void DrawHeader(iText.Kernel.Pdf.Canvas.PdfCanvas canvas, Rectangle pageSize)
|
||
{
|
||
float leftX = _margins.Left * MmToPoints;
|
||
float rightX = pageSize.GetWidth() - _margins.Right * MmToPoints;
|
||
float topY = pageSize.GetHeight() - (_margins.Top * MmToPoints * 0.3f);
|
||
|
||
float fontSize = _headerConfig.FontSize > 0 ? _headerConfig.FontSize : 8f;
|
||
var fontColor = _renderer.ParseColor(_headerConfig.Color ?? "#666666");
|
||
|
||
// 绘制左侧文本行 | Draw left-side text lines
|
||
if (_headerConfig.Left != null && _headerConfig.Left.Count > 0)
|
||
{
|
||
float lineY = topY;
|
||
float lineSpacing = (fontSize + 2f) * 1.2f;
|
||
|
||
foreach (var line in _headerConfig.Left)
|
||
{
|
||
if (string.IsNullOrEmpty(line)) continue;
|
||
|
||
canvas.BeginText()
|
||
.SetFontAndSize(_font, fontSize)
|
||
.MoveText(leftX, lineY)
|
||
.ShowText(line)
|
||
.EndText();
|
||
|
||
lineY -= lineSpacing;
|
||
}
|
||
}
|
||
|
||
// 绘制右上角 Logo | Draw right-side logo
|
||
if (!string.IsNullOrEmpty(_headerConfig.RightImageKey))
|
||
{
|
||
try
|
||
{
|
||
ImageData logoImageData = _renderer.FindBoundImage(_template, _headerConfig.RightImageKey);
|
||
if (logoImageData != null)
|
||
{
|
||
byte[] imageBytes = _renderer.GetImageBytes(logoImageData);
|
||
if (imageBytes != null && imageBytes.Length > 0)
|
||
{
|
||
var iTextImageData = iText.IO.Image.ImageDataFactory.Create(imageBytes);
|
||
float logoHeight = 10f * MmToPoints;
|
||
float logoWidth = logoHeight * (iTextImageData.GetWidth() / iTextImageData.GetHeight());
|
||
|
||
float logoX = rightX - logoWidth;
|
||
float logoY = topY - logoHeight + fontSize;
|
||
|
||
canvas.AddImageFittedIntoRectangle(iTextImageData,
|
||
new Rectangle(logoX, logoY, logoWidth, logoHeight), false);
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("页眉 Logo 渲染失败 | Header logo rendering failed: {Message}", ex.Message);
|
||
}
|
||
}
|
||
|
||
// 绘制页眉分隔线 | Draw header separator line
|
||
if (_headerConfig.ShowLine)
|
||
{
|
||
float lineY = topY - (_headerConfig.Left?.Count ?? 1) * ((fontSize + 2f) * 1.2f) - 3f;
|
||
canvas.SetStrokeColor(fontColor)
|
||
.SetLineWidth(0.5f)
|
||
.MoveTo(leftX, lineY)
|
||
.LineTo(rightX, lineY)
|
||
.Stroke();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绘制页脚 | Draw footer
|
||
/// </summary>
|
||
private void DrawFooter(iText.Kernel.Pdf.Canvas.PdfCanvas canvas, Rectangle pageSize, PdfDocument pdfDoc, int currentPage)
|
||
{
|
||
float leftX = _margins.Left * MmToPoints;
|
||
float rightX = pageSize.GetWidth() - _margins.Right * MmToPoints;
|
||
float bottomY = _margins.Bottom * MmToPoints * 0.5f;
|
||
|
||
float fontSize = _footerConfig.FontSize > 0 ? _footerConfig.FontSize : 8f;
|
||
var fontColor = _renderer.ParseColor(_footerConfig.Color ?? "#666666");
|
||
|
||
// 绘制页脚分隔线 | Draw footer separator line
|
||
if (_footerConfig.ShowLine)
|
||
{
|
||
float lineY = bottomY + fontSize + 5f;
|
||
canvas.SetStrokeColor(fontColor)
|
||
.SetLineWidth(0.5f)
|
||
.MoveTo(leftX, lineY)
|
||
.LineTo(rightX, lineY)
|
||
.Stroke();
|
||
}
|
||
|
||
// 绘制左侧文本(公司名称)| Draw left-side text (company name)
|
||
if (_footerConfig.Left != null && _footerConfig.Left.Count > 0)
|
||
{
|
||
var leftText = _footerConfig.Left[0] ?? string.Empty;
|
||
canvas.BeginText()
|
||
.SetFontAndSize(_font, fontSize)
|
||
.MoveText(leftX, bottomY)
|
||
.ShowText(leftText)
|
||
.EndText();
|
||
}
|
||
|
||
// 绘制右侧页码(当前页 / 总页数占位符)| Draw right-side page number (current / total placeholder)
|
||
if (_footerConfig.Right != null && _footerConfig.Right.Count > 0)
|
||
{
|
||
var pageNumTemplate = _footerConfig.Right[0] ?? string.Empty;
|
||
// 先写当前页码部分 | Write current page number part
|
||
var currentPageText = pageNumTemplate.Replace("{currentPage}", currentPage.ToString()).Replace("{totalPages}", "");
|
||
// 分离出 totalPages 前后的文本 | Separate text around totalPages
|
||
var parts = pageNumTemplate.Split(new[] { "{totalPages}" }, StringSplitOptions.None);
|
||
|
||
if (parts.Length == 2)
|
||
{
|
||
// 有总页数占位符:写前缀 + 当前页码 + 占位符 XObject + 后缀
|
||
var prefix = parts[0].Replace("{currentPage}", currentPage.ToString());
|
||
var suffix = parts[1];
|
||
|
||
float prefixWidth = _font.GetWidth(prefix, fontSize);
|
||
float suffixWidth = _font.GetWidth(suffix, fontSize);
|
||
float placeholderWidth = 15f; // 预留总页数宽度 | Reserve width for total pages
|
||
|
||
float totalWidth = prefixWidth + placeholderWidth + suffixWidth;
|
||
float startX = rightX - totalWidth;
|
||
|
||
// 写前缀文本 | Write prefix text
|
||
canvas.BeginText()
|
||
.SetFontAndSize(_font, fontSize)
|
||
.MoveText(startX, bottomY)
|
||
.ShowText(prefix)
|
||
.EndText();
|
||
|
||
// 添加总页数占位符 XObject | Add total page count placeholder XObject
|
||
float placeholderX = startX + prefixWidth;
|
||
canvas.AddXObjectAt(_totalPagePlaceholder, placeholderX, bottomY - 2f);
|
||
_totalPagePositions.Add((canvas, placeholderX, bottomY));
|
||
|
||
// 写后缀文本 | Write suffix text
|
||
if (!string.IsNullOrEmpty(suffix))
|
||
{
|
||
canvas.BeginText()
|
||
.SetFontAndSize(_font, fontSize)
|
||
.MoveText(placeholderX + placeholderWidth, bottomY)
|
||
.ShowText(suffix)
|
||
.EndText();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 无总页数占位符,直接写文本 | No total pages placeholder, write text directly
|
||
var text = pageNumTemplate.Replace("{currentPage}", currentPage.ToString());
|
||
float textWidth = _font.GetWidth(text, fontSize);
|
||
float textX = rightX - textWidth;
|
||
|
||
canvas.BeginText()
|
||
.SetFontAndSize(_font, fontSize)
|
||
.MoveText(textX, bottomY)
|
||
.ShowText(text)
|
||
.EndText();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 文档关闭前回填总页数到所有占位符 | Write total page count to all placeholders before document close
|
||
/// </summary>
|
||
public void WriteTotal(PdfDocument pdfDoc)
|
||
{
|
||
int totalContentPages = pdfDoc.GetNumberOfPages() - _homepageCount;
|
||
var totalText = totalContentPages.ToString();
|
||
|
||
// 在占位符 XObject 上绘制总页数 | Draw total page count on placeholder XObject
|
||
var canvas = new iText.Kernel.Pdf.Canvas.PdfCanvas(_totalPagePlaceholder, pdfDoc);
|
||
canvas.BeginText()
|
||
.SetFontAndSize(_font, _footerConfig?.FontSize > 0 ? _footerConfig.FontSize : 8f)
|
||
.MoveText(0, 2f)
|
||
.ShowText(totalText)
|
||
.EndText();
|
||
canvas.Release();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从模板中查找已绑定的图像数据 | Find bound image data from template
|
||
/// </summary>
|
||
internal ImageData FindBoundImage(ReportTemplate template, string dataKey)
|
||
{
|
||
if (template?.Pages == null || string.IsNullOrEmpty(dataKey)) return null;
|
||
|
||
foreach (var page in template.Pages)
|
||
{
|
||
if (page.Elements == null) continue;
|
||
foreach (var element in page.Elements)
|
||
{
|
||
var found = FindImageInElement(element, dataKey);
|
||
if (found != null) return found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归搜索元素及其子元素中的图像数据 | Recursively search for image data in element and its children
|
||
/// </summary>
|
||
private ImageData FindImageInElement(TemplateElement element, string dataKey)
|
||
{
|
||
if (element == null) return null;
|
||
|
||
// 检查当前元素 | Check current element
|
||
if (string.Equals(element.Type, "image", StringComparison.OrdinalIgnoreCase)
|
||
&& string.Equals(element.DataKey, dataKey, StringComparison.OrdinalIgnoreCase)
|
||
&& element.ImageData != null)
|
||
{
|
||
return element.ImageData;
|
||
}
|
||
|
||
// 递归搜索子元素 | Recursively search children
|
||
if (element.Children != null)
|
||
{
|
||
foreach (var child in element.Children)
|
||
{
|
||
var found = FindImageInElement(child, dataKey);
|
||
if (found != null) return found;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 7.9 PDF 保存到文件 | PDF save to file
|
||
|
||
/// <summary>
|
||
/// 将 PDF 内存流保存到文件 | Save PDF memory stream to file
|
||
/// </summary>
|
||
/// <param name="pdfStream">PDF 内存流 | PDF memory stream</param>
|
||
/// <param name="filePath">输出文件路径 | Output file path</param>
|
||
/// <returns>保存结果(成功/失败)| Save result (success/failure)</returns>
|
||
public ReportResult SaveToFile(MemoryStream pdfStream, string filePath)
|
||
{
|
||
if (pdfStream == null)
|
||
{
|
||
return ReportResult.Failure("PDF 流为空,无法保存 | PDF stream is null, cannot save");
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(filePath))
|
||
{
|
||
return ReportResult.Failure("输出文件路径为空 | Output file path is empty");
|
||
}
|
||
|
||
try
|
||
{
|
||
_logger.Info("开始保存 PDF 到文件:{FilePath} | Saving PDF to file: {FilePath}", filePath);
|
||
|
||
// 确保目标目录存在 | Ensure target directory exists
|
||
var directory = System.IO.Path.GetDirectoryName(filePath);
|
||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||
{
|
||
Directory.CreateDirectory(directory);
|
||
}
|
||
|
||
// 保存流位置并重置 | Save stream position and reset
|
||
long originalPosition = pdfStream.Position;
|
||
pdfStream.Position = 0;
|
||
|
||
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||
{
|
||
pdfStream.CopyTo(fileStream);
|
||
}
|
||
|
||
// 恢复流位置 | Restore stream position
|
||
pdfStream.Position = originalPosition;
|
||
|
||
_logger.Info("PDF 文件保存成功:{FilePath} | PDF file saved successfully: {FilePath}", filePath);
|
||
return ReportResult.Success(pdfStream);
|
||
}
|
||
catch (UnauthorizedAccessException ex)
|
||
{
|
||
_logger.Error(ex, "PDF 保存失败:无写入权限 | PDF save failed: no write permission: {Path}", filePath);
|
||
return ReportResult.Failure($"无法写入文件,权限不足:{filePath} | Cannot write file, insufficient permissions: {filePath}", ex);
|
||
}
|
||
catch (DirectoryNotFoundException ex)
|
||
{
|
||
_logger.Error(ex, "PDF 保存失败:目录不存在 | PDF save failed: directory not found: {Path}", filePath);
|
||
return ReportResult.Failure($"目标目录不存在:{filePath} | Target directory not found: {filePath}", ex);
|
||
}
|
||
catch (IOException ex)
|
||
{
|
||
_logger.Error(ex, "PDF 保存失败:IO 错误 | PDF save failed: IO error: {Path}", filePath);
|
||
return ReportResult.Failure($"文件保存 IO 错误:{ex.Message} | File save IO error: {ex.Message}", ex);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error(ex, "PDF 保存失败:未知错误 | PDF save failed: unknown error: {Path}", filePath);
|
||
return ReportResult.Failure($"文件保存过程中发生错误:{ex.Message} | Error occurred during file save: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助方法 | Helper methods
|
||
|
||
/// <summary>
|
||
/// 解析十六进制颜色字符串为 iText Color 对象 | Parse hex color string to iText Color object
|
||
/// 支持格式:#RRGGBB 或 #RGB | Supports formats: #RRGGBB or #RGB
|
||
/// </summary>
|
||
/// <param name="hexColor">十六进制颜色字符串 | Hex color string</param>
|
||
/// <returns>iText Color 对象,解析失败返回黑色 | iText Color object, returns black on failure</returns>
|
||
private Color ParseColor(string hexColor)
|
||
{
|
||
if (string.IsNullOrEmpty(hexColor))
|
||
{
|
||
return ColorConstants.BLACK;
|
||
}
|
||
|
||
try
|
||
{
|
||
var hex = hexColor.TrimStart('#');
|
||
|
||
if (hex.Length == 3)
|
||
{
|
||
// 扩展 #RGB 为 #RRGGBB | Expand #RGB to #RRGGBB
|
||
hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}";
|
||
}
|
||
|
||
if (hex.Length == 6)
|
||
{
|
||
int r = Convert.ToInt32(hex.Substring(0, 2), 16);
|
||
int g = Convert.ToInt32(hex.Substring(2, 2), 16);
|
||
int b = Convert.ToInt32(hex.Substring(4, 2), 16);
|
||
return new DeviceRgb(r, g, b);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Warn("颜色解析失败:{Color},使用默认黑色 | Color parsing failed: {Color}, using default black: {Message}", hexColor, ex.Message);
|
||
}
|
||
|
||
return ColorConstants.BLACK;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析对齐方式字符串为 iText TextAlignment | Parse alignment string to iText TextAlignment
|
||
/// </summary>
|
||
/// <param name="align">对齐方式字符串(left/center/right)| Alignment string</param>
|
||
/// <returns>iText TextAlignment 枚举值 | iText TextAlignment enum value</returns>
|
||
private TextAlignment ParseTextAlignment(string align)
|
||
{
|
||
switch (align?.ToLowerInvariant())
|
||
{
|
||
case "center":
|
||
return TextAlignment.CENTER;
|
||
case "right":
|
||
return TextAlignment.RIGHT;
|
||
case "justify":
|
||
return TextAlignment.JUSTIFIED;
|
||
case "left":
|
||
default:
|
||
return TextAlignment.LEFT;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|