523 lines
23 KiB
C#
523 lines
23 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using XP.Common.Logging.Interfaces;
|
||
using XP.ReportEngine.Interfaces;
|
||
using XP.ReportEngine.Models;
|
||
|
||
namespace XP.ReportEngine.Services
|
||
{
|
||
/// <summary>
|
||
/// 页面排版引擎实现 | Page layout engine implementation
|
||
/// 负责计算页面元素位置、处理分页和自适应布局
|
||
/// Responsible for calculating element positions, handling pagination and adaptive layout
|
||
/// </summary>
|
||
public class PageLayoutEngine : ILayoutEngine
|
||
{
|
||
private readonly ILoggerService _logger;
|
||
private readonly JsonTemplateEngine _templateEngine;
|
||
|
||
// A4 页面尺寸(mm)| A4 page dimensions (mm)
|
||
private const float A4Width = 210f;
|
||
private const float A4Height = 297f;
|
||
|
||
// 默认行高估算(mm)| Default row height estimate (mm)
|
||
private const float DefaultRowHeight = 8f;
|
||
|
||
// 默认文本元素高度(mm)| Default text element height (mm)
|
||
private const float DefaultTextHeight = 10f;
|
||
|
||
// 默认分隔线高度(mm)| Default divider height (mm)
|
||
private const float DefaultDividerHeight = 2f;
|
||
|
||
/// <summary>
|
||
/// 构造函数 | Constructor
|
||
/// </summary>
|
||
/// <param name="logger">日志服务 | Logger service</param>
|
||
/// <param name="templateEngine">模板引擎(用于样式解析)| Template engine (for style resolution)</param>
|
||
public PageLayoutEngine(ILoggerService logger, JsonTemplateEngine templateEngine)
|
||
{
|
||
_logger = logger?.ForModule<PageLayoutEngine>() ?? throw new ArgumentNullException(nameof(logger));
|
||
_templateEngine = templateEngine ?? throw new ArgumentNullException(nameof(templateEngine));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算页面布局 | Calculate page layout
|
||
/// 遍历模板中所有页面和元素,根据定位方式计算最终坐标,处理分页和表格跨页
|
||
/// Iterates through all pages and elements in template, calculates final coordinates based on positioning mode,
|
||
/// handles pagination and table page-splitting
|
||
/// </summary>
|
||
/// <param name="template">绑定后的模板 | Bound template</param>
|
||
/// <param name="options">生成选项 | Generation options</param>
|
||
/// <returns>排版后的页面列表 | List of laid-out pages</returns>
|
||
public List<LayoutPage> CalculateLayout(ReportTemplate template, ReportGenerationOptions options)
|
||
{
|
||
if (template == null) throw new ArgumentNullException(nameof(template));
|
||
|
||
_logger.Info("开始排版计算 | Starting layout calculation");
|
||
|
||
var margins = template.Document?.Margins ?? new MarginSettings();
|
||
var availableWidth = A4Width - margins.Left - margins.Right;
|
||
var availableHeight = A4Height - margins.Top - margins.Bottom;
|
||
|
||
var pages = new List<LayoutPage>();
|
||
var currentPageNumber = 1;
|
||
|
||
if (template.Pages == null || template.Pages.Count == 0)
|
||
{
|
||
_logger.Warn("模板无页面定义 | Template has no page definitions");
|
||
return pages;
|
||
}
|
||
|
||
foreach (var templatePage in template.Pages)
|
||
{
|
||
if (templatePage.Elements == null || templatePage.Elements.Count == 0)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// 分离绝对定位和流式定位元素 | Separate absolute and flow positioned elements
|
||
var absoluteElements = templatePage.Elements
|
||
.Where(e => string.Equals(e.Positioning, "absolute", StringComparison.OrdinalIgnoreCase))
|
||
.ToList();
|
||
var flowElements = templatePage.Elements
|
||
.Where(e => string.Equals(e.Positioning, "flow", StringComparison.OrdinalIgnoreCase))
|
||
.ToList();
|
||
|
||
// 创建当前页面 | Create current page
|
||
var currentPage = new LayoutPage
|
||
{
|
||
PageNumber = currentPageNumber,
|
||
PageType = templatePage.Type,
|
||
Elements = new List<LayoutElement>()
|
||
};
|
||
pages.Add(currentPage);
|
||
|
||
// 处理绝对定位元素(不参与分页)| Process absolute positioned elements (no pagination)
|
||
foreach (var element in absoluteElements)
|
||
{
|
||
var layoutElement = ProcessAbsoluteElement(element, template, margins);
|
||
currentPage.Elements.Add(layoutElement);
|
||
}
|
||
|
||
// 处理流式定位元素(参与分页)| Process flow positioned elements (with pagination)
|
||
var currentY = margins.Top;
|
||
|
||
foreach (var element in flowElements)
|
||
{
|
||
var elementHeight = CalculateElementHeight(element);
|
||
var elementWidth = CalculateElementWidth(element, availableWidth);
|
||
|
||
// 强制分页元素 | Forced page break element
|
||
if (string.Equals(element.Type, "pagebreak", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
currentPageNumber++;
|
||
currentPage = new LayoutPage
|
||
{
|
||
PageNumber = currentPageNumber,
|
||
PageType = templatePage.Type,
|
||
Elements = new List<LayoutElement>()
|
||
};
|
||
pages.Add(currentPage);
|
||
currentY = margins.Top;
|
||
continue;
|
||
}
|
||
|
||
// 检查是否需要分页 | Check if pagination is needed
|
||
if (element.Type == "table" && element.Columns != null)
|
||
{
|
||
// 表格跨页拆分逻辑 | Table page-split logic
|
||
currentY = ProcessTableWithPageSplit(
|
||
element, template, margins, availableHeight, availableWidth,
|
||
currentY, pages, ref currentPage, ref currentPageNumber, templatePage.Type);
|
||
}
|
||
else
|
||
{
|
||
// 普通元素分页检查 | Normal element pagination check
|
||
if (currentY + elementHeight > margins.Top + availableHeight)
|
||
{
|
||
// 创建新页面 | Create new page
|
||
currentPageNumber++;
|
||
currentPage = new LayoutPage
|
||
{
|
||
PageNumber = currentPageNumber,
|
||
PageType = templatePage.Type,
|
||
Elements = new List<LayoutElement>()
|
||
};
|
||
pages.Add(currentPage);
|
||
currentY = margins.Top;
|
||
}
|
||
|
||
var layoutElement = CreateFlowLayoutElement(
|
||
element, template, margins, currentY, elementWidth, elementHeight);
|
||
currentPage.Elements.Add(layoutElement);
|
||
|
||
// 累计 Y 坐标 | Accumulate Y coordinate
|
||
currentY += elementHeight;
|
||
}
|
||
}
|
||
|
||
currentPageNumber++;
|
||
}
|
||
|
||
_logger.Info("排版计算完成,共 {PageCount} 页 | Layout calculation completed, {PageCount} pages total", pages.Count);
|
||
return pages;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理绝对定位元素 | Process absolute positioned element
|
||
/// 元素坐标 = Position + Margins 偏移
|
||
/// Element coordinates = Position + Margins offset
|
||
/// </summary>
|
||
private LayoutElement ProcessAbsoluteElement(TemplateElement element, ReportTemplate template, MarginSettings margins)
|
||
{
|
||
var x = margins.Left + (element.Position != null && element.Position.Length > 0 ? element.Position[0] : 0f);
|
||
var y = margins.Top + (element.Position != null && element.Position.Length > 1 ? element.Position[1] : 0f);
|
||
var width = element.Size != null && element.Size.Length > 0 ? element.Size[0] : 0f;
|
||
var height = element.Size != null && element.Size.Length > 1 ? element.Size[1] : 0f;
|
||
|
||
// 图像等比缩放 | Image proportional scaling
|
||
if (element.Type == "image" && width > 0 && height > 0)
|
||
{
|
||
var scaled = CalculateScaledImageDimensions(width, height, width, height);
|
||
width = scaled.Width;
|
||
height = scaled.Height;
|
||
}
|
||
|
||
var resolvedStyle = _templateEngine.ResolveStyle(template, element.Style);
|
||
|
||
return new LayoutElement
|
||
{
|
||
Source = element,
|
||
X = x,
|
||
Y = y,
|
||
Width = width,
|
||
Height = height,
|
||
ResolvedStyle = resolvedStyle,
|
||
ResolvedContent = element.Content,
|
||
ResolvedTableData = element.TableData,
|
||
ResolvedImage = element.ImageData
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建流式定位的布局元素 | Create flow positioned layout element
|
||
/// </summary>
|
||
private LayoutElement CreateFlowLayoutElement(
|
||
TemplateElement element, ReportTemplate template, MarginSettings margins,
|
||
float currentY, float width, float height)
|
||
{
|
||
var x = margins.Left;
|
||
|
||
// 如果元素有 Position 定义,使用 X 偏移 | If element has Position defined, use X offset
|
||
if (element.Position != null && element.Position.Length > 0)
|
||
{
|
||
x = margins.Left + element.Position[0];
|
||
}
|
||
|
||
// 图像等比缩放 | Image proportional scaling
|
||
if (element.Type == "image")
|
||
{
|
||
var targetWidth = width;
|
||
var targetHeight = height;
|
||
var imageWidth = element.Size != null && element.Size.Length > 0 ? element.Size[0] : width;
|
||
var imageHeight = element.Size != null && element.Size.Length > 1 ? element.Size[1] : height;
|
||
|
||
if (imageWidth > 0 && imageHeight > 0 && targetWidth > 0 && targetHeight > 0)
|
||
{
|
||
var scaled = CalculateScaledImageDimensions(imageWidth, imageHeight, targetWidth, targetHeight);
|
||
width = scaled.Width;
|
||
height = scaled.Height;
|
||
}
|
||
}
|
||
|
||
var resolvedStyle = _templateEngine.ResolveStyle(template, element.Style);
|
||
|
||
return new LayoutElement
|
||
{
|
||
Source = element,
|
||
X = x,
|
||
Y = currentY,
|
||
Width = width,
|
||
Height = height,
|
||
ResolvedStyle = resolvedStyle,
|
||
ResolvedContent = element.Content,
|
||
ResolvedTableData = element.TableData,
|
||
ResolvedImage = element.ImageData
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理表格跨页拆分 | Process table with page-split
|
||
/// 按行高计算剩余空间,超出时拆分到新页面,续页重复表头行
|
||
/// Calculate remaining space by row height, split to new page when exceeded, repeat header on continuation pages
|
||
/// </summary>
|
||
/// <returns>处理后的当前 Y 坐标 | Current Y coordinate after processing</returns>
|
||
private float ProcessTableWithPageSplit(
|
||
TemplateElement element, ReportTemplate template, MarginSettings margins,
|
||
float availableHeight, float availableWidth,
|
||
float currentY, List<LayoutPage> pages,
|
||
ref LayoutPage currentPage, ref int currentPageNumber, string pageType)
|
||
{
|
||
var resolvedStyle = _templateEngine.ResolveStyle(template, element.Style);
|
||
var tableWidth = element.Size != null && element.Size.Length > 0 ? element.Size[0] : availableWidth;
|
||
|
||
// 计算表头高度(1 行)| Calculate header height (1 row)
|
||
var headerHeight = DefaultRowHeight;
|
||
|
||
// 获取表格数据行数 | Get table data row count
|
||
var tableData = GetTableDataFromElement(element);
|
||
var totalDataRows = tableData?.Count ?? 0;
|
||
|
||
if (totalDataRows == 0)
|
||
{
|
||
// 空表格,仅渲染表头 | Empty table, render header only
|
||
var emptyTableHeight = headerHeight;
|
||
if (currentY + emptyTableHeight > margins.Top + availableHeight)
|
||
{
|
||
currentPageNumber++;
|
||
currentPage = new LayoutPage
|
||
{
|
||
PageNumber = currentPageNumber,
|
||
PageType = pageType,
|
||
Elements = new List<LayoutElement>()
|
||
};
|
||
pages.Add(currentPage);
|
||
currentY = margins.Top;
|
||
}
|
||
|
||
var emptyTableElement = new LayoutElement
|
||
{
|
||
Source = element,
|
||
X = margins.Left,
|
||
Y = currentY,
|
||
Width = tableWidth,
|
||
Height = emptyTableHeight,
|
||
ResolvedStyle = resolvedStyle,
|
||
ResolvedContent = element.Content,
|
||
ResolvedTableData = tableData
|
||
};
|
||
currentPage.Elements.Add(emptyTableElement);
|
||
currentY += emptyTableHeight;
|
||
return currentY;
|
||
}
|
||
|
||
// 计算当前页面剩余空间 | Calculate remaining space on current page
|
||
var remainingHeight = (margins.Top + availableHeight) - currentY;
|
||
var totalTableHeight = headerHeight + (totalDataRows * DefaultRowHeight);
|
||
|
||
// 如果整个表格能放下,直接放置 | If entire table fits, place directly
|
||
if (totalTableHeight <= remainingHeight)
|
||
{
|
||
var tableElement = new LayoutElement
|
||
{
|
||
Source = element,
|
||
X = margins.Left,
|
||
Y = currentY,
|
||
Width = tableWidth,
|
||
Height = totalTableHeight,
|
||
ResolvedStyle = resolvedStyle,
|
||
ResolvedContent = element.Content,
|
||
ResolvedTableData = tableData
|
||
};
|
||
currentPage.Elements.Add(tableElement);
|
||
currentY += totalTableHeight;
|
||
return currentY;
|
||
}
|
||
|
||
// 需要跨页拆分 | Need to split across pages
|
||
var currentRowIndex = 0;
|
||
|
||
while (currentRowIndex < totalDataRows)
|
||
{
|
||
// 计算当前页面可容纳的数据行数(需预留表头空间)| Calculate rows that fit on current page (reserve header space)
|
||
var currentRemainingHeight = (margins.Top + availableHeight) - currentY;
|
||
var rowsOnCurrentPage = (int)Math.Floor((currentRemainingHeight - headerHeight) / DefaultRowHeight);
|
||
|
||
if (rowsOnCurrentPage <= 0)
|
||
{
|
||
// 当前页面空间不足以放置表头+至少一行数据,创建新页面
|
||
// Current page doesn't have space for header + at least one data row, create new page
|
||
currentPageNumber++;
|
||
currentPage = new LayoutPage
|
||
{
|
||
PageNumber = currentPageNumber,
|
||
PageType = pageType,
|
||
Elements = new List<LayoutElement>()
|
||
};
|
||
pages.Add(currentPage);
|
||
currentY = margins.Top;
|
||
currentRemainingHeight = availableHeight;
|
||
rowsOnCurrentPage = (int)Math.Floor((currentRemainingHeight - headerHeight) / DefaultRowHeight);
|
||
}
|
||
|
||
// 确定本页实际放置的行数 | Determine actual rows to place on this page
|
||
var rowsToPlace = Math.Min(rowsOnCurrentPage, totalDataRows - currentRowIndex);
|
||
var splitData = tableData.Skip(currentRowIndex).Take(rowsToPlace).ToList();
|
||
var splitHeight = headerHeight + (rowsToPlace * DefaultRowHeight);
|
||
|
||
var splitElement = new LayoutElement
|
||
{
|
||
Source = element,
|
||
X = margins.Left,
|
||
Y = currentY,
|
||
Width = tableWidth,
|
||
Height = splitHeight,
|
||
ResolvedStyle = resolvedStyle,
|
||
ResolvedContent = element.Content,
|
||
ResolvedTableData = splitData
|
||
};
|
||
currentPage.Elements.Add(splitElement);
|
||
currentY += splitHeight;
|
||
currentRowIndex += rowsToPlace;
|
||
|
||
// 如果还有剩余行,创建新页面继续 | If there are remaining rows, create new page to continue
|
||
if (currentRowIndex < totalDataRows)
|
||
{
|
||
currentPageNumber++;
|
||
currentPage = new LayoutPage
|
||
{
|
||
PageNumber = currentPageNumber,
|
||
PageType = pageType,
|
||
Elements = new List<LayoutElement>()
|
||
};
|
||
pages.Add(currentPage);
|
||
currentY = margins.Top;
|
||
}
|
||
}
|
||
|
||
return currentY;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算图像等比缩放尺寸 | Calculate proportionally scaled image dimensions
|
||
/// 保持宽高比,确保缩放后的宽度和高度均不超过目标区域
|
||
/// Maintain aspect ratio, ensure scaled width and height don't exceed target area
|
||
/// </summary>
|
||
/// <param name="imageWidth">原始图像宽度 | Original image width</param>
|
||
/// <param name="imageHeight">原始图像高度 | Original image height</param>
|
||
/// <param name="targetWidth">目标区域宽度 | Target area width</param>
|
||
/// <param name="targetHeight">目标区域高度 | Target area height</param>
|
||
/// <returns>缩放后的尺寸 | Scaled dimensions</returns>
|
||
public (float Width, float Height) CalculateScaledImageDimensions(
|
||
float imageWidth, float imageHeight, float targetWidth, float targetHeight)
|
||
{
|
||
if (imageWidth <= 0 || imageHeight <= 0 || targetWidth <= 0 || targetHeight <= 0)
|
||
{
|
||
return (0f, 0f);
|
||
}
|
||
|
||
// 如果图像已经在目标区域内,无需缩放 | If image already fits, no scaling needed
|
||
if (imageWidth <= targetWidth && imageHeight <= targetHeight)
|
||
{
|
||
return (imageWidth, imageHeight);
|
||
}
|
||
|
||
// 计算宽度和高度的缩放比例,取较小值以确保两个维度都不超出
|
||
// Calculate scale ratios for width and height, use the smaller one to ensure both dimensions fit
|
||
var widthRatio = targetWidth / imageWidth;
|
||
var heightRatio = targetHeight / imageHeight;
|
||
var scale = Math.Min(widthRatio, heightRatio);
|
||
|
||
var scaledWidth = imageWidth * scale;
|
||
var scaledHeight = imageHeight * scale;
|
||
|
||
return (scaledWidth, scaledHeight);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算元素高度 | Calculate element height
|
||
/// 根据元素类型和 Size 定义确定高度
|
||
/// Determine height based on element type and Size definition
|
||
/// </summary>
|
||
private float CalculateElementHeight(TemplateElement element)
|
||
{
|
||
// 如果有明确的 Size 定义,使用 Size[1] 作为高度 | If Size is defined, use Size[1] as height
|
||
if (element.Size != null && element.Size.Length > 1 && element.Size[1] > 0)
|
||
{
|
||
return element.Size[1];
|
||
}
|
||
|
||
// 根据元素类型使用默认高度 | Use default height based on element type
|
||
return element.Type?.ToLowerInvariant() switch
|
||
{
|
||
"text" => DefaultTextHeight,
|
||
"divider" => DefaultDividerHeight,
|
||
"spacer" => element.Size is { Length: >= 2 } ? element.Size[1] : DefaultTextHeight,
|
||
"row" => CalculateRowHeight(element),
|
||
"pagebreak" => 0f,
|
||
"image" => DefaultTextHeight,
|
||
"table" => CalculateTableHeight(element),
|
||
_ => DefaultTextHeight
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算表格高度 | Calculate table height
|
||
/// 表头行 + 数据行数 × 默认行高
|
||
/// Header row + data row count × default row height
|
||
/// </summary>
|
||
private float CalculateTableHeight(TemplateElement element)
|
||
{
|
||
var tableData = GetTableDataFromElement(element);
|
||
var dataRowCount = tableData?.Count ?? 0;
|
||
// 表头 1 行 + 数据行 | 1 header row + data rows
|
||
return DefaultRowHeight + (dataRowCount * DefaultRowHeight);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算 Row 容器高度 | Calculate row container height
|
||
/// 取子元素中最大高度,如果有 Size 定义则优先使用
|
||
/// Uses max child height, or Size definition if available
|
||
/// </summary>
|
||
private float CalculateRowHeight(TemplateElement element)
|
||
{
|
||
// 如果 row 本身有 Size[1] 定义,直接使用 | If row has Size[1], use it directly
|
||
if (element.Size != null && element.Size.Length > 1 && element.Size[1] > 0)
|
||
{
|
||
return element.Size[1];
|
||
}
|
||
|
||
// 否则取子元素中最大高度 | Otherwise use max child height
|
||
if (element.Children == null || element.Children.Count == 0)
|
||
return DefaultTextHeight;
|
||
|
||
float maxHeight = 0;
|
||
foreach (var child in element.Children)
|
||
{
|
||
float childHeight = DefaultTextHeight;
|
||
if (child.Size != null && child.Size.Length > 1 && child.Size[1] > 0)
|
||
{
|
||
childHeight = child.Size[1];
|
||
}
|
||
if (childHeight > maxHeight) maxHeight = childHeight;
|
||
}
|
||
return maxHeight > 0 ? maxHeight : DefaultTextHeight;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算元素宽度 | Calculate element width
|
||
/// </summary>
|
||
private float CalculateElementWidth(TemplateElement element, float availableWidth)
|
||
{
|
||
if (element.Size != null && element.Size.Length > 0 && element.Size[0] > 0)
|
||
{
|
||
return element.Size[0];
|
||
}
|
||
return availableWidth;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从元素获取表格数据 | Get table data from element
|
||
/// 表格数据在数据绑定阶段通过 TableData 属性填充
|
||
/// Table data is populated during data binding phase via TableData property
|
||
/// </summary>
|
||
private List<Dictionary<string, object>> GetTableDataFromElement(TemplateElement element)
|
||
{
|
||
// 表格数据在数据绑定阶段已填充到 TemplateElement.TableData
|
||
// Table data is populated during data binding phase into TemplateElement.TableData
|
||
return element.TableData;
|
||
}
|
||
}
|
||
}
|