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;
}
}
}