Files

523 lines
23 KiB
C#
Raw Permalink 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.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;
}
}
}