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 { /// /// 页面排版引擎实现 | Page layout engine implementation /// 负责计算页面元素位置、处理分页和自适应布局 /// Responsible for calculating element positions, handling pagination and adaptive layout /// 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; /// /// 构造函数 | Constructor /// /// 日志服务 | Logger service /// 模板引擎(用于样式解析)| Template engine (for style resolution) public PageLayoutEngine(ILoggerService logger, JsonTemplateEngine templateEngine) { _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger)); _templateEngine = templateEngine ?? throw new ArgumentNullException(nameof(templateEngine)); } /// /// 计算页面布局 | Calculate page layout /// 遍历模板中所有页面和元素,根据定位方式计算最终坐标,处理分页和表格跨页 /// Iterates through all pages and elements in template, calculates final coordinates based on positioning mode, /// handles pagination and table page-splitting /// /// 绑定后的模板 | Bound template /// 生成选项 | Generation options /// 排版后的页面列表 | List of laid-out pages public List 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(); 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() }; 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() }; 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() }; 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; } /// /// 处理绝对定位元素 | Process absolute positioned element /// 元素坐标 = Position + Margins 偏移 /// Element coordinates = Position + Margins offset /// 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 }; } /// /// 创建流式定位的布局元素 | Create flow positioned layout element /// 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 }; } /// /// 处理表格跨页拆分 | Process table with page-split /// 按行高计算剩余空间,超出时拆分到新页面,续页重复表头行 /// Calculate remaining space by row height, split to new page when exceeded, repeat header on continuation pages /// /// 处理后的当前 Y 坐标 | Current Y coordinate after processing private float ProcessTableWithPageSplit( TemplateElement element, ReportTemplate template, MarginSettings margins, float availableHeight, float availableWidth, float currentY, List 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() }; 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() }; 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() }; pages.Add(currentPage); currentY = margins.Top; } } return currentY; } /// /// 计算图像等比缩放尺寸 | Calculate proportionally scaled image dimensions /// 保持宽高比,确保缩放后的宽度和高度均不超过目标区域 /// Maintain aspect ratio, ensure scaled width and height don't exceed target area /// /// 原始图像宽度 | Original image width /// 原始图像高度 | Original image height /// 目标区域宽度 | Target area width /// 目标区域高度 | Target area height /// 缩放后的尺寸 | Scaled dimensions 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); } /// /// 计算元素高度 | Calculate element height /// 根据元素类型和 Size 定义确定高度 /// Determine height based on element type and Size definition /// 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 }; } /// /// 计算表格高度 | Calculate table height /// 表头行 + 数据行数 × 默认行高 /// Header row + data row count × default row height /// private float CalculateTableHeight(TemplateElement element) { var tableData = GetTableDataFromElement(element); var dataRowCount = tableData?.Count ?? 0; // 表头 1 行 + 数据行 | 1 header row + data rows return DefaultRowHeight + (dataRowCount * DefaultRowHeight); } /// /// 计算 Row 容器高度 | Calculate row container height /// 取子元素中最大高度,如果有 Size 定义则优先使用 /// Uses max child height, or Size definition if available /// 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; } /// /// 计算元素宽度 | Calculate element width /// 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; } /// /// 从元素获取表格数据 | Get table data from element /// 表格数据在数据绑定阶段通过 TableData 属性填充 /// Table data is populated during data binding phase via TableData property /// private List> GetTableDataFromElement(TemplateElement element) { // 表格数据在数据绑定阶段已填充到 TemplateElement.TableData // Table data is populated during data binding phase into TemplateElement.TableData return element.TableData; } } }