报告ReportEngineBase修改语言资源文件,修改模板和报告输出功能。
This commit is contained in:
@@ -62,6 +62,7 @@ namespace XP.ReportEngine.Services
|
||||
private PdfFont _westernFont;
|
||||
private bool _fontsInitialized;
|
||||
private readonly object _fontLock = new();
|
||||
private ReportTemplate _currentTemplate;
|
||||
|
||||
public ITextPdfRenderer(ILoggerService logger, ILocalizationService localizationService)
|
||||
{
|
||||
@@ -77,10 +78,12 @@ namespace XP.ReportEngine.Services
|
||||
/// </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)
|
||||
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;
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
@@ -102,8 +105,27 @@ namespace XP.ReportEngine.Services
|
||||
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++)
|
||||
@@ -114,10 +136,27 @@ namespace XP.ReportEngine.Services
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
// 文档关闭前回填总页数占位符 | Fill total page count placeholder before closing
|
||||
if (headerFooterHandler != null)
|
||||
{
|
||||
headerFooterHandler.WriteTotal(pdfDocument);
|
||||
}
|
||||
|
||||
// 仅关闭 Document(它会级联关闭 PdfDocument 和 PdfWriter)
|
||||
// Only close Document (it cascades to PdfDocument and PdfWriter)
|
||||
document.Close();
|
||||
@@ -178,6 +217,15 @@ namespace XP.ReportEngine.Services
|
||||
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;
|
||||
@@ -198,6 +246,10 @@ namespace XP.ReportEngine.Services
|
||||
|
||||
var paragraph = new Paragraph(content);
|
||||
|
||||
// 设置紧凑的默认段落间距 | Set compact default paragraph spacing
|
||||
paragraph.SetMarginTop(0);
|
||||
paragraph.SetMarginBottom(2f);
|
||||
|
||||
// 应用字体 | Apply font
|
||||
var font = GetFontForCurrentLanguage();
|
||||
paragraph.SetFont(font);
|
||||
@@ -224,6 +276,24 @@ namespace XP.ReportEngine.Services
|
||||
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));
|
||||
|
||||
@@ -243,6 +313,16 @@ namespace XP.ReportEngine.Services
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -419,6 +499,27 @@ namespace XP.ReportEngine.Services
|
||||
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)
|
||||
@@ -596,6 +697,25 @@ namespace XP.ReportEngine.Services
|
||||
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));
|
||||
@@ -605,6 +725,16 @@ namespace XP.ReportEngine.Services
|
||||
}
|
||||
}
|
||||
|
||||
// 应用样式中的边距 | 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);
|
||||
}
|
||||
|
||||
@@ -647,6 +777,555 @@ namespace XP.ReportEngine.Services
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user