Files
XplorePlane/XP.ReportEngine/Services/ITextPdfRenderer.cs
T

1482 lines
63 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}