diff --git a/XP.ReportEngine/Fonts/.gitkeep b/XP.ReportEngine/Fonts/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/XP.ReportEngine/Fonts/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/XP.ReportEngine/Fonts/README.md b/XP.ReportEngine/Fonts/README.md
new file mode 100644
index 0000000..0c24001
--- /dev/null
+++ b/XP.ReportEngine/Fonts/README.md
@@ -0,0 +1,18 @@
+# 字体文件目录 | Font Files Directory
+
+## 所需字体文件 | Required Font Files
+
+请将以下字体文件手动添加到此目录:
+
+1. **NotoSansCJKsc-Regular.otf** — 中文字体(CJK 字符支持)| Chinese font (CJK character support)
+2. **NotoSans-Regular.ttf** — 西文字体 | Western font
+
+## 下载地址 | Download Links
+
+- Noto Sans CJK: https://github.com/googlefonts/noto-cjk/releases
+- Noto Sans: https://fonts.google.com/noto/specimen/Noto+Sans
+
+## 配置说明 | Configuration Notes
+
+字体文件已在 .csproj 中配置为嵌入资源(EmbeddedResource),添加文件后无需额外配置。
+Font files are configured as EmbeddedResource in .csproj, no additional configuration needed after adding files.
diff --git a/XP.ReportEngine/Interfaces/IDataBinder.cs b/XP.ReportEngine/Interfaces/IDataBinder.cs
new file mode 100644
index 0000000..7063ff8
--- /dev/null
+++ b/XP.ReportEngine/Interfaces/IDataBinder.cs
@@ -0,0 +1,18 @@
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Interfaces
+{
+ ///
+ /// 数据绑定器接口 | Data binder interface
+ ///
+ public interface IDataBinder
+ {
+ ///
+ /// 将上下文数据绑定到模板 | Bind context data to template
+ ///
+ /// 报告模板 | Report template
+ /// 报告上下文 | Report context
+ /// 绑定后的模板(元素内容已替换)| Bound template with resolved content
+ ReportTemplate Bind(ReportTemplate template, ReportContext context);
+ }
+}
diff --git a/XP.ReportEngine/Interfaces/ILayoutEngine.cs b/XP.ReportEngine/Interfaces/ILayoutEngine.cs
new file mode 100644
index 0000000..732ff3e
--- /dev/null
+++ b/XP.ReportEngine/Interfaces/ILayoutEngine.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Interfaces
+{
+ ///
+ /// 排版引擎接口 | Layout engine interface
+ ///
+ public interface ILayoutEngine
+ {
+ ///
+ /// 计算页面布局 | Calculate page layout
+ ///
+ /// 绑定后的模板 | Bound template
+ /// 生成选项 | Generation options
+ /// 排版后的页面列表 | List of laid-out pages
+ List CalculateLayout(ReportTemplate template, ReportGenerationOptions options);
+ }
+}
diff --git a/XP.ReportEngine/Interfaces/IPdfRenderer.cs b/XP.ReportEngine/Interfaces/IPdfRenderer.cs
new file mode 100644
index 0000000..fa159e1
--- /dev/null
+++ b/XP.ReportEngine/Interfaces/IPdfRenderer.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.IO;
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Interfaces
+{
+ ///
+ /// PDF 渲染器接口 | PDF renderer interface
+ ///
+ public interface IPdfRenderer
+ {
+ ///
+ /// 将排版结果渲染为 PDF | Render layout result to PDF
+ ///
+ /// 排版后的页面列表 | Laid-out pages
+ /// 生成选项 | Generation options
+ /// PDF 内存流 | PDF memory stream
+ MemoryStream Render(List pages, ReportGenerationOptions options);
+ }
+}
diff --git a/XP.ReportEngine/Interfaces/IReportDataAdapter.cs b/XP.ReportEngine/Interfaces/IReportDataAdapter.cs
new file mode 100644
index 0000000..264ab7c
--- /dev/null
+++ b/XP.ReportEngine/Interfaces/IReportDataAdapter.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Interfaces
+{
+ ///
+ /// 报告数据适配器接口 | Report data adapter interface
+ /// 将 XP.ImageProcessing 的 OutputData 转换为 ReportContext
+ ///
+ public interface IReportDataAdapter
+ {
+ ///
+ /// 将处理器输出数据适配为报告上下文 | Adapt processor output data to report context
+ ///
+ /// 处理器输出字典列表 | List of processor output dictionaries
+ /// 报告元数据 | Report metadata
+ /// 报告上下文 | Report context
+ ReportContext Adapt(List processorOutputs, ReportMetadata metadata);
+ }
+}
diff --git a/XP.ReportEngine/Interfaces/IReportGenerator.cs b/XP.ReportEngine/Interfaces/IReportGenerator.cs
new file mode 100644
index 0000000..4d2e15d
--- /dev/null
+++ b/XP.ReportEngine/Interfaces/IReportGenerator.cs
@@ -0,0 +1,19 @@
+using System.Threading.Tasks;
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Interfaces
+{
+ ///
+ /// 报告生成器接口(格式无关)| Report generator interface (format-agnostic)
+ ///
+ public interface IReportGenerator
+ {
+ ///
+ /// 异步生成报告 | Generate report asynchronously
+ ///
+ /// 报告上下文数据 | Report context data
+ /// 生成选项 | Generation options
+ /// 生成结果 | Generation result
+ Task GenerateAsync(ReportContext context, ReportGenerationOptions options);
+ }
+}
diff --git a/XP.ReportEngine/Interfaces/IReportGeneratorFactory.cs b/XP.ReportEngine/Interfaces/IReportGeneratorFactory.cs
new file mode 100644
index 0000000..56f1d72
--- /dev/null
+++ b/XP.ReportEngine/Interfaces/IReportGeneratorFactory.cs
@@ -0,0 +1,15 @@
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Interfaces
+{
+ ///
+ /// 报告生成器工厂接口 | Report generator factory interface
+ ///
+ public interface IReportGeneratorFactory
+ {
+ ///
+ /// 根据输出格式创建生成器 | Create generator by output format
+ ///
+ IReportGenerator Create(ReportOutputFormat format);
+ }
+}
diff --git a/XP.ReportEngine/Interfaces/ITemplateEngine.cs b/XP.ReportEngine/Interfaces/ITemplateEngine.cs
new file mode 100644
index 0000000..7596ab4
--- /dev/null
+++ b/XP.ReportEngine/Interfaces/ITemplateEngine.cs
@@ -0,0 +1,22 @@
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Interfaces
+{
+ ///
+ /// 模板引擎接口 | Template engine interface
+ ///
+ public interface ITemplateEngine
+ {
+ ///
+ /// 加载并验证模板 | Load and validate template
+ ///
+ /// 模板文件路径 | Template file path
+ /// 解析后的模板对象 | Parsed template object
+ ReportTemplate LoadTemplate(string templatePath);
+
+ ///
+ /// 验证模板结构完整性 | Validate template structure integrity
+ ///
+ TemplateValidationResult Validate(ReportTemplate template);
+ }
+}
diff --git a/XP.ReportEngine/Models/ImageData.cs b/XP.ReportEngine/Models/ImageData.cs
new file mode 100644
index 0000000..b1aa0ed
--- /dev/null
+++ b/XP.ReportEngine/Models/ImageData.cs
@@ -0,0 +1,35 @@
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 图像数据封装 | Image data wrapper
+ ///
+ public class ImageData
+ {
+ ///
+ /// 图像来源类型 | Image source type
+ ///
+ public ImageSourceType SourceType { get; set; }
+
+ ///
+ /// 字节数组数据(当 SourceType 为 Bytes 时)| Byte array data
+ ///
+ public byte[] Bytes { get; set; }
+
+ ///
+ /// 文件路径(当 SourceType 为 FilePath 时)| File path
+ ///
+ public string FilePath { get; set; }
+
+ ///
+ /// BitmapSource 引用(当 SourceType 为 BitmapSource 时)| BitmapSource reference
+ ///
+ public object BitmapSource { get; set; }
+ }
+
+ public enum ImageSourceType
+ {
+ Bytes,
+ FilePath,
+ BitmapSource
+ }
+}
diff --git a/XP.ReportEngine/Models/LayoutPage.cs b/XP.ReportEngine/Models/LayoutPage.cs
new file mode 100644
index 0000000..6658e97
--- /dev/null
+++ b/XP.ReportEngine/Models/LayoutPage.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 排版后的页面 | Laid-out page
+ ///
+ public class LayoutPage
+ {
+ public int PageNumber { get; set; }
+ public List Elements { get; set; } = new();
+ }
+
+ ///
+ /// 排版后的元素(含计算后的绝对坐标)| Laid-out element with computed absolute coordinates
+ ///
+ public class LayoutElement
+ {
+ public TemplateElement Source { get; set; }
+ public float X { get; set; }
+ public float Y { get; set; }
+ public float Width { get; set; }
+ public float Height { get; set; }
+ public StyleDefinition ResolvedStyle { get; set; }
+ public string ResolvedContent { get; set; }
+ public List> ResolvedTableData { get; set; }
+ public ImageData ResolvedImage { get; set; }
+ }
+}
diff --git a/XP.ReportEngine/Models/ProcessorOutput.cs b/XP.ReportEngine/Models/ProcessorOutput.cs
new file mode 100644
index 0000000..3594ad7
--- /dev/null
+++ b/XP.ReportEngine/Models/ProcessorOutput.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 处理器输出数据封装 | Processor output data wrapper
+ ///
+ public class ProcessorOutput
+ {
+ ///
+ /// 处理器类型名称 | Processor type name
+ ///
+ public string ProcessorType { get; set; }
+
+ ///
+ /// 输出数据字典 | Output data dictionary
+ ///
+ public Dictionary OutputData { get; set; } = new();
+
+ ///
+ /// 关联的已标注图像 | Associated annotated image
+ ///
+ public ImageData AnnotatedImage { get; set; }
+ }
+}
diff --git a/XP.ReportEngine/Models/ReportContext.cs b/XP.ReportEngine/Models/ReportContext.cs
new file mode 100644
index 0000000..6b08c73
--- /dev/null
+++ b/XP.ReportEngine/Models/ReportContext.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 报告上下文,包含生成报告所需的全部数据 | Report context containing all data needed for report generation
+ ///
+ public class ReportContext
+ {
+ ///
+ /// 报告元数据 | Report metadata
+ ///
+ public ReportMetadata Metadata { get; set; }
+
+ ///
+ /// 检测结果数据集合(按处理器类型分组)| Inspection result data grouped by processor type
+ ///
+ public List ResultGroups { get; set; } = new();
+
+ ///
+ /// 图像数据字典(键为 dataKey,值为图像数据)| Image data dictionary (key=dataKey, value=image data)
+ ///
+ public Dictionary Images { get; set; } = new();
+
+ ///
+ /// 自定义属性字典(用于数据绑定的扁平化键值对)| Custom properties for data binding
+ ///
+ public Dictionary Properties { get; set; } = new();
+ }
+
+ ///
+ /// 报告元数据 | Report metadata
+ ///
+ public class ReportMetadata
+ {
+ public string ReportId { get; set; }
+ public DateTime InspectionDate { get; set; }
+ public string SampleName { get; set; }
+ public string OperatorName { get; set; }
+ public string Description { get; set; }
+ }
+
+ ///
+ /// 检测结果分组 | Inspection result group
+ ///
+ public class InspectionResultGroup
+ {
+ ///
+ /// 处理器类型标识 | Processor type identifier
+ ///
+ public string ProcessorType { get; set; }
+
+ ///
+ /// 数据来源标识 | Data source identifier
+ ///
+ public string SourceId { get; set; }
+
+ ///
+ /// 分类结果(Pass/Fail)| Classification result
+ ///
+ public string Classification { get; set; }
+
+ ///
+ /// 结果键值对 | Result key-value pairs
+ ///
+ public Dictionary Data { get; set; } = new();
+
+ ///
+ /// 表格数据行(用于表格渲染)| Table data rows for table rendering
+ ///
+ public List> TableRows { get; set; } = new();
+ }
+}
diff --git a/XP.ReportEngine/Models/ReportGenerationOptions.cs b/XP.ReportEngine/Models/ReportGenerationOptions.cs
new file mode 100644
index 0000000..a111849
--- /dev/null
+++ b/XP.ReportEngine/Models/ReportGenerationOptions.cs
@@ -0,0 +1,30 @@
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 报告生成选项 | Report generation options
+ ///
+ public class ReportGenerationOptions
+ {
+ ///
+ /// 模板文件路径 | Template file path
+ ///
+ public string TemplatePath { get; set; }
+
+ ///
+ /// 输出文件路径(可选,为 null 时仅返回 MemoryStream)| Output file path (optional)
+ ///
+ public string OutputFilePath { get; set; }
+
+ ///
+ /// 输出格式 | Output format
+ ///
+ public ReportOutputFormat Format { get; set; } = ReportOutputFormat.Pdf;
+ }
+
+ public enum ReportOutputFormat
+ {
+ Pdf,
+ Excel,
+ Csv
+ }
+}
diff --git a/XP.ReportEngine/Models/ReportResult.cs b/XP.ReportEngine/Models/ReportResult.cs
new file mode 100644
index 0000000..c95753c
--- /dev/null
+++ b/XP.ReportEngine/Models/ReportResult.cs
@@ -0,0 +1,22 @@
+using System;
+using System.IO;
+
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 报告生成结果 | Report generation result
+ ///
+ public class ReportResult
+ {
+ public bool IsSuccess { get; set; }
+ public MemoryStream PdfStream { get; set; }
+ public string ErrorMessage { get; set; }
+ public Exception Exception { get; set; }
+
+ public static ReportResult Success(MemoryStream stream)
+ => new() { IsSuccess = true, PdfStream = stream };
+
+ public static ReportResult Failure(string message, Exception ex = null)
+ => new() { IsSuccess = false, ErrorMessage = message, Exception = ex };
+ }
+}
diff --git a/XP.ReportEngine/Models/ReportTemplate.cs b/XP.ReportEngine/Models/ReportTemplate.cs
new file mode 100644
index 0000000..56e2ba5
--- /dev/null
+++ b/XP.ReportEngine/Models/ReportTemplate.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 报告模板定义 | Report template definition
+ ///
+ public class ReportTemplate
+ {
+ public DocumentSettings Document { get; set; }
+ public List Pages { get; set; } = new();
+ public Dictionary Styles { get; set; } = new();
+ }
+
+ public class DocumentSettings
+ {
+ public string PageSize { get; set; } = "A4";
+ public string Orientation { get; set; } = "Portrait";
+ public MarginSettings Margins { get; set; } = new();
+ }
+
+ public class MarginSettings
+ {
+ public float Top { get; set; } = 20f;
+ public float Bottom { get; set; } = 20f;
+ public float Left { get; set; } = 20f;
+ public float Right { get; set; } = 20f;
+ }
+
+ public class TemplatePage
+ {
+ ///
+ /// 页面类型:homepage / metricData / defectDetails / bgaInspection / voidInspection / viaFillInspection
+ ///
+ public string Type { get; set; }
+ public List Elements { get; set; } = new();
+ }
+}
diff --git a/XP.ReportEngine/Models/TemplateElement.cs b/XP.ReportEngine/Models/TemplateElement.cs
new file mode 100644
index 0000000..b807db5
--- /dev/null
+++ b/XP.ReportEngine/Models/TemplateElement.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+
+namespace XP.ReportEngine.Models
+{
+ public class TemplateElement
+ {
+ ///
+ /// 元素类型:text / image / table / divider / grid
+ ///
+ public string Type { get; set; }
+
+ ///
+ /// 文本内容(可含 ${} 绑定表达式)| Text content with binding expressions
+ ///
+ public string Content { get; set; }
+
+ ///
+ /// 样式名称引用 | Style name reference
+ ///
+ public string Style { get; set; }
+
+ ///
+ /// 数据绑定键 | Data binding key
+ ///
+ public string DataKey { get; set; }
+
+ ///
+ /// 位置坐标 [x, y](mm)| Position coordinates in mm
+ ///
+ public float[] Position { get; set; }
+
+ ///
+ /// 尺寸 [width, height](mm)| Size in mm
+ ///
+ public float[] Size { get; set; }
+
+ ///
+ /// 表格列定义 | Table column definitions
+ ///
+ public List Columns { get; set; }
+
+ ///
+ /// 是否显示边框 | Whether to show border
+ ///
+ public bool Border { get; set; }
+
+ ///
+ /// 定位方式:absolute / flow | Positioning mode
+ ///
+ public string Positioning { get; set; } = "absolute";
+
+ ///
+ /// 表格数据行(数据绑定阶段填充,用于排版计算和渲染)
+ /// Table data rows (populated during data binding phase, used for layout calculation and rendering)
+ ///
+ [Newtonsoft.Json.JsonIgnore]
+ public List> TableData { get; set; }
+
+ ///
+ /// 图像数据(数据绑定阶段填充)| Image data (populated during data binding phase)
+ ///
+ [Newtonsoft.Json.JsonIgnore]
+ public ImageData ImageData { get; set; }
+ }
+
+ public class ColumnDefinition
+ {
+ public string Header { get; set; }
+ public string Field { get; set; }
+ public float Width { get; set; }
+ public string Align { get; set; } = "left";
+ }
+
+ public class StyleDefinition
+ {
+ public string Font { get; set; }
+ public float Size { get; set; } = 12f;
+ public bool Bold { get; set; }
+ public bool Italic { get; set; }
+ public string Color { get; set; } = "#000000";
+ public string Align { get; set; } = "left";
+ public string BackgroundColor { get; set; }
+ }
+}
diff --git a/XP.ReportEngine/Models/TemplateValidationResult.cs b/XP.ReportEngine/Models/TemplateValidationResult.cs
new file mode 100644
index 0000000..6e2191a
--- /dev/null
+++ b/XP.ReportEngine/Models/TemplateValidationResult.cs
@@ -0,0 +1,17 @@
+namespace XP.ReportEngine.Models
+{
+ ///
+ /// 模板验证结果 | Template validation result
+ ///
+ public class TemplateValidationResult
+ {
+ public bool IsValid { get; set; }
+ public string ErrorMessage { get; set; }
+
+ public static TemplateValidationResult Valid()
+ => new() { IsValid = true };
+
+ public static TemplateValidationResult Invalid(string message)
+ => new() { IsValid = false, ErrorMessage = message };
+ }
+}
diff --git a/XP.ReportEngine/Module/.gitkeep b/XP.ReportEngine/Module/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/XP.ReportEngine/Module/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/XP.ReportEngine/Resources/Resources.en-US.resx b/XP.ReportEngine/Resources/Resources.en-US.resx
new file mode 100644
index 0000000..459ff73
--- /dev/null
+++ b/XP.ReportEngine/Resources/Resources.en-US.resx
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
diff --git a/XP.ReportEngine/Resources/Resources.resx b/XP.ReportEngine/Resources/Resources.resx
new file mode 100644
index 0000000..44e9f97
--- /dev/null
+++ b/XP.ReportEngine/Resources/Resources.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
diff --git a/XP.ReportEngine/Resources/Resources.zh-CN.resx b/XP.ReportEngine/Resources/Resources.zh-CN.resx
new file mode 100644
index 0000000..459ff73
--- /dev/null
+++ b/XP.ReportEngine/Resources/Resources.zh-CN.resx
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
diff --git a/XP.ReportEngine/Resources/Resources.zh-TW.resx b/XP.ReportEngine/Resources/Resources.zh-TW.resx
new file mode 100644
index 0000000..459ff73
--- /dev/null
+++ b/XP.ReportEngine/Resources/Resources.zh-TW.resx
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
diff --git a/XP.ReportEngine/Services/.gitkeep b/XP.ReportEngine/Services/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/XP.ReportEngine/Services/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/XP.ReportEngine/Services/ExpressionDataBinder.cs b/XP.ReportEngine/Services/ExpressionDataBinder.cs
new file mode 100644
index 0000000..4c58dcb
--- /dev/null
+++ b/XP.ReportEngine/Services/ExpressionDataBinder.cs
@@ -0,0 +1,365 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+using XP.Common.Localization.Enums;
+using XP.Common.Localization.Interfaces;
+using XP.Common.Logging.Interfaces;
+using XP.ReportEngine.Interfaces;
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Services
+{
+ ///
+ /// 表达式数据绑定器实现 | Expression data binder implementation
+ /// 支持 ${} 语法的数据绑定、格式化函数和本地化键解析
+ /// Supports ${} syntax data binding, format functions and localization key resolution
+ ///
+ public class ExpressionDataBinder : IDataBinder
+ {
+ private readonly ILoggerService _logger;
+ private readonly ILocalizationService _localizationService;
+
+ private static readonly Regex ExpressionPattern = new(@"\$\{([^}]+)\}", RegexOptions.Compiled);
+ private static readonly Regex LocalizationPattern = new(@"^loc:(.+)$", RegexOptions.Compiled);
+ private static readonly Regex FunctionPattern = new(@"^(\w+)\((.+)\)$", RegexOptions.Compiled);
+ private static readonly Regex IndexPattern = new(@"^([^\[]+)\[(\d+)\]$", RegexOptions.Compiled);
+
+ public ExpressionDataBinder(ILoggerService logger, ILocalizationService localizationService)
+ {
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
+ }
+
+ public ReportTemplate Bind(ReportTemplate template, ReportContext context)
+ {
+ if (template == null) throw new ArgumentNullException(nameof(template));
+ if (context == null) throw new ArgumentNullException(nameof(context));
+
+ _logger.Info("开始数据绑定 | Starting data binding");
+ var clonedTemplate = DeepClone(template);
+
+ if (clonedTemplate.Pages != null)
+ {
+ foreach (var page in clonedTemplate.Pages)
+ {
+ if (page.Elements == null) continue;
+ foreach (var element in page.Elements)
+ {
+ BindElement(element, context);
+ }
+ }
+ }
+
+ _logger.Info("数据绑定完成 | Data binding completed");
+ return clonedTemplate;
+ }
+
+
+ private void BindElement(TemplateElement element, ReportContext context)
+ {
+ if (string.IsNullOrEmpty(element.Content)) return;
+ element.Content = ResolveAllExpressions(element.Content, context);
+ }
+
+ private string ResolveAllExpressions(string input, ReportContext context)
+ {
+ return ExpressionPattern.Replace(input, match =>
+ {
+ var expression = match.Groups[1].Value.Trim();
+ return ResolveExpression(expression, context);
+ });
+ }
+
+ private string ResolveExpression(string expression, ReportContext context)
+ {
+ // 1. 本地化键 loc:ResourceKey | Localization key
+ var locMatch = LocalizationPattern.Match(expression);
+ if (locMatch.Success)
+ {
+ var resourceKey = locMatch.Groups[1].Value.Trim();
+ return ResolveLocalizationKey(resourceKey);
+ }
+
+ // 2. 格式化函数 functionName(params) | Format function
+ var funcMatch = FunctionPattern.Match(expression);
+ if (funcMatch.Success)
+ {
+ var functionName = funcMatch.Groups[1].Value;
+ var paramExpression = funcMatch.Groups[2].Value.Trim();
+ return ResolveFormatFunction(functionName, paramExpression, context);
+ }
+
+ // 3. 属性路径 | Property path
+ return ResolvePropertyPath(expression, context);
+ }
+
+ private string ResolveLocalizationKey(string resourceKey)
+ {
+ try
+ {
+ var value = _localizationService.GetString(resourceKey);
+ if (value == null)
+ {
+ _logger.Warn("本地化键未找到: {Key} | Localization key not found: {Key}", resourceKey);
+ return string.Empty;
+ }
+ return value;
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("解析本地化键失败: {Key}, 错误: {Message} | Failed to resolve localization key: {Key}, error: {Message}", resourceKey, ex.Message);
+ return string.Empty;
+ }
+ }
+
+
+ private string ResolveFormatFunction(string functionName, string paramExpression, ReportContext context)
+ {
+ switch (functionName.ToLowerInvariant())
+ {
+ case "formatdate":
+ {
+ var value = ResolvePropertyValue(paramExpression, context);
+ return FormatDate(value);
+ }
+ case "formatnumber":
+ {
+ var parts = paramExpression.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ var value = ResolvePropertyValue(parts[0].Trim(), context);
+ var decimals = parts.Length > 1 && int.TryParse(parts[1].Trim(), out var d) ? d : 2;
+ return FormatNumber(value, decimals);
+ }
+ case "formatpercent":
+ {
+ var value = ResolvePropertyValue(paramExpression, context);
+ return FormatPercent(value);
+ }
+ default:
+ {
+ _logger.Warn("未知的格式化函数: {FunctionName} | Unknown format function: {FunctionName}", functionName);
+ return string.Empty;
+ }
+ }
+ }
+
+ private string ResolvePropertyPath(string path, ReportContext context)
+ {
+ var value = ResolvePropertyValue(path, context);
+ if (value == null)
+ {
+ _logger.Warn("绑定属性未找到: {Path},替换为空字符串 | Binding property not found: {Path}, replacing with empty string", path);
+ return string.Empty;
+ }
+ return ConvertToString(value);
+ }
+
+ private object ResolvePropertyValue(string path, ReportContext context)
+ {
+ if (string.IsNullOrWhiteSpace(path)) return null;
+ path = path.Trim();
+
+ // 优先从 Properties 字典查找 | First look up in Properties dictionary
+ if (context.Properties != null && context.Properties.TryGetValue(path, out var directValue))
+ {
+ return directValue;
+ }
+
+ // 尝试从 context 对象解析嵌套路径 | Try nested path from context object
+ var resolved = ResolveNestedPath(path, context);
+ return resolved;
+ }
+
+
+ private object ResolveNestedPath(string path, object root)
+ {
+ if (root == null || string.IsNullOrWhiteSpace(path)) return null;
+
+ var segments = path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
+ var current = root;
+
+ foreach (var segment in segments)
+ {
+ if (current == null) return null;
+
+ var indexMatch = IndexPattern.Match(segment);
+ if (indexMatch.Success)
+ {
+ var propertyName = indexMatch.Groups[1].Value;
+ var index = int.Parse(indexMatch.Groups[2].Value);
+ current = GetPropertyValue(current, propertyName);
+ if (current == null) return null;
+ current = GetIndexedValue(current, index);
+ }
+ else
+ {
+ current = GetPropertyValue(current, segment);
+ }
+ }
+
+ return current;
+ }
+
+ private object GetPropertyValue(object obj, string propertyName)
+ {
+ if (obj == null || string.IsNullOrWhiteSpace(propertyName)) return null;
+
+ // 字典访问 | Dictionary access
+ if (obj is IDictionary dict)
+ {
+ if (dict.TryGetValue(propertyName, out var dictValue))
+ return dictValue;
+ foreach (var kvp in dict)
+ {
+ if (string.Equals(kvp.Key, propertyName, StringComparison.OrdinalIgnoreCase))
+ return kvp.Value;
+ }
+ return null;
+ }
+
+ // 反射获取属性 | Reflection property access
+ var type = obj.GetType();
+ var propInfo = type.GetProperty(propertyName,
+ BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
+ if (propInfo != null)
+ {
+ try { return propInfo.GetValue(obj); }
+ catch { return null; }
+ }
+
+ return null;
+ }
+
+ private object GetIndexedValue(object obj, int index)
+ {
+ if (obj == null || index < 0) return null;
+
+ if (obj is IList list)
+ {
+ return index < list.Count ? list[index] : null;
+ }
+
+ if (obj is IEnumerable enumerable)
+ {
+ var i = 0;
+ foreach (var item in enumerable)
+ {
+ if (i == index) return item;
+ i++;
+ }
+ }
+
+ return null;
+ }
+
+
+ private string FormatDate(object value)
+ {
+ if (value == null) return string.Empty;
+
+ DateTime dateTime;
+ if (value is DateTime dt)
+ {
+ dateTime = dt;
+ }
+ else if (DateTime.TryParse(value.ToString(), out var parsed))
+ {
+ dateTime = parsed;
+ }
+ else
+ {
+ _logger.Warn("无法将值转换为日期: {Value} | Cannot convert value to date: {Value}", value);
+ return value.ToString();
+ }
+
+ var format = _localizationService.CurrentLanguage switch
+ {
+ SupportedLanguage.ZhCN => "yyyy年MM月dd日",
+ SupportedLanguage.ZhTW => "yyyy年MM月dd日",
+ SupportedLanguage.EnUS => "MM/dd/yyyy",
+ _ => "yyyy-MM-dd"
+ };
+
+ return dateTime.ToString(format);
+ }
+
+ private string FormatNumber(object value, int decimals)
+ {
+ if (value == null) return string.Empty;
+
+ if (!TryConvertToDouble(value, out var number))
+ {
+ _logger.Warn("无法将值转换为数字: {Value} | Cannot convert value to number: {Value}", value);
+ return value.ToString();
+ }
+
+ var culture = GetCultureInfo();
+ return number.ToString($"N{decimals}", culture);
+ }
+
+ private string FormatPercent(object value)
+ {
+ if (value == null) return string.Empty;
+
+ if (!TryConvertToDouble(value, out var number))
+ {
+ _logger.Warn("无法将值转换为百分比: {Value} | Cannot convert value to percentage: {Value}", value);
+ return value.ToString();
+ }
+
+ // 值在 0-1 范围内视为小数百分比 | Values in 0-1 range treated as decimal percentage
+ if (number >= 0 && number <= 1)
+ {
+ number *= 100;
+ }
+
+ var culture = GetCultureInfo();
+ return number.ToString("F2", culture) + "%";
+ }
+
+
+ private bool TryConvertToDouble(object value, out double result)
+ {
+ result = 0;
+ if (value == null) return false;
+
+ switch (value)
+ {
+ case double d: result = d; return true;
+ case float f: result = f; return true;
+ case int i: result = i; return true;
+ case long l: result = l; return true;
+ case decimal dec: result = (double)dec; return true;
+ default:
+ return double.TryParse(value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out result);
+ }
+ }
+
+ private CultureInfo GetCultureInfo()
+ {
+ return _localizationService.CurrentLanguage switch
+ {
+ SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
+ SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
+ SupportedLanguage.EnUS => new CultureInfo("en-US"),
+ _ => CultureInfo.InvariantCulture
+ };
+ }
+
+ private string ConvertToString(object value)
+ {
+ if (value == null) return string.Empty;
+ if (value is DateTime dt) return FormatDate(dt);
+ return value.ToString();
+ }
+
+ private ReportTemplate DeepClone(ReportTemplate template)
+ {
+ var json = JsonConvert.SerializeObject(template);
+ return JsonConvert.DeserializeObject(json);
+ }
+ }
+}
diff --git a/XP.ReportEngine/Services/ITextPdfRenderer.cs b/XP.ReportEngine/Services/ITextPdfRenderer.cs
new file mode 100644
index 0000000..784d6f9
--- /dev/null
+++ b/XP.ReportEngine/Services/ITextPdfRenderer.cs
@@ -0,0 +1,773 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Windows.Media.Imaging;
+using iText.IO.Font;
+using iText.Kernel.Colors;
+using iText.Kernel.Font;
+using iText.Kernel.Geom;
+using iText.Kernel.Pdf;
+using iText.Kernel.Pdf.Canvas.Draw;
+using iText.Layout;
+using iText.Layout.Borders;
+using iText.Layout.Element;
+using iText.Layout.Properties;
+using XP.Common.Localization.Enums;
+using XP.Common.Localization.Interfaces;
+using XP.Common.Logging.Interfaces;
+using XP.ReportEngine.Interfaces;
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Services
+{
+ ///
+ /// iText 7 PDF 渲染器实现 | iText 7 PDF renderer implementation
+ ///
+ public class ITextPdfRenderer : IPdfRenderer
+ {
+ private readonly ILoggerService _logger;
+ private readonly ILocalizationService _localizationService;
+
+ ///
+ /// mm 到 points 的转换系数 | mm to points conversion factor
+ ///
+ private const float MmToPoints = 2.83465f;
+
+ ///
+ /// A4 页面宽度(mm)| A4 page width in mm
+ ///
+ private const float A4WidthMm = 210f;
+
+ ///
+ /// A4 页面高度(mm)| A4 page height in mm
+ ///
+ private const float A4HeightMm = 297f;
+
+ ///
+ /// 表头背景色 | Table header background color
+ ///
+ private const string HeaderBackgroundColor = "#E0E0E0";
+
+ ///
+ /// 表格奇数行背景色 | Table odd row background color
+ ///
+ private const string OddRowBackgroundColor = "#FFFFFF";
+
+ ///
+ /// 表格偶数行背景色 | Table even row background color
+ ///
+ private const string EvenRowBackgroundColor = "#F5F5F5";
+
+ private PdfFont _cjkFont;
+ private PdfFont _westernFont;
+
+ public ITextPdfRenderer(ILoggerService logger, ILocalizationService localizationService)
+ {
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
+ InitializeFonts();
+ }
+
+ #region 7.1 基础 PDF 文档创建 | Basic PDF document creation
+
+ ///
+ /// 将排版结果渲染为 PDF 内存流 | Render layout result to PDF memory stream
+ ///
+ /// 排版后的页面列表 | Laid-out pages
+ /// 生成选项 | Generation options
+ /// PDF 内存流 | PDF memory stream
+ public MemoryStream Render(List pages, ReportGenerationOptions options)
+ {
+ _logger.Info("开始 PDF 渲染,共 {PageCount} 页 | Starting PDF rendering, {PageCount} pages", pages?.Count ?? 0);
+
+ var memoryStream = new MemoryStream();
+
+ try
+ {
+ using (var writer = new PdfWriter(memoryStream, new WriterProperties().SetFullCompressionMode(true)))
+ {
+ // 防止 PdfWriter 关闭时关闭底层流 | Prevent PdfWriter from closing the underlying stream
+ writer.SetCloseStream(false);
+
+ using (var pdfDocument = new PdfDocument(writer))
+ {
+ // 设置 A4 页面尺寸 | Set A4 page size
+ pdfDocument.SetDefaultPageSize(PageSize.A4);
+
+ using (var document = new Document(pdfDocument))
+ {
+ // 设置默认边距(使用默认 20mm)| Set default margins (20mm default)
+ float marginTop = 20f * MmToPoints;
+ float marginBottom = 20f * MmToPoints;
+ float marginLeft = 20f * MmToPoints;
+ float marginRight = 20f * MmToPoints;
+
+ document.SetMargins(marginTop, marginRight, marginBottom, marginLeft);
+
+ if (pages != null && pages.Count > 0)
+ {
+ for (int i = 0; i < pages.Count; i++)
+ {
+ if (i > 0)
+ {
+ // 添加新页面 | Add new page
+ document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
+ }
+
+ RenderPage(document, pages[i]);
+ }
+ }
+ }
+ }
+ }
+
+ // 重置流位置以便后续读取 | Reset stream position for subsequent reading
+ memoryStream.Position = 0;
+ _logger.Info("PDF 渲染完成 | PDF rendering completed");
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "PDF 渲染过程中发生错误 | Error occurred during PDF rendering: {Message}", ex.Message);
+ throw;
+ }
+
+ return memoryStream;
+ }
+
+ ///
+ /// 渲染单个页面 | Render a single page
+ ///
+ private void RenderPage(Document document, LayoutPage page)
+ {
+ if (page?.Elements == null) return;
+
+ foreach (var element in page.Elements)
+ {
+ try
+ {
+ RenderElement(document, element);
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("渲染元素失败,跳过该元素 | Failed to render element, skipping: {Message}", ex.Message);
+ }
+ }
+ }
+
+ ///
+ /// 根据元素类型分发渲染 | Dispatch rendering based on element type
+ ///
+ private void RenderElement(Document document, LayoutElement element)
+ {
+ if (element?.Source == null) return;
+
+ var elementType = element.Source.Type?.ToLowerInvariant();
+
+ switch (elementType)
+ {
+ case "text":
+ RenderTextElement(document, element);
+ break;
+ case "image":
+ RenderImageElement(document, element);
+ break;
+ case "table":
+ RenderTableElement(document, element);
+ break;
+ case "divider":
+ RenderDividerElement(document, element);
+ break;
+ default:
+ _logger.Warn("未知的元素类型:{Type},跳过渲染 | Unknown element type: {Type}, skipping", elementType);
+ break;
+ }
+ }
+
+ #endregion
+
+ #region 7.2 文本元素渲染 | Text element rendering
+
+ ///
+ /// 渲染文本元素 | Render text element
+ ///
+ private void RenderTextElement(Document document, LayoutElement element)
+ {
+ var content = element.ResolvedContent ?? string.Empty;
+ var style = element.ResolvedStyle ?? new StyleDefinition();
+
+ var paragraph = new Paragraph(content);
+
+ // 应用字体 | Apply font
+ var font = GetFontForCurrentLanguage();
+ paragraph.SetFont(font);
+
+ // 应用字体大小 | Apply font size
+ paragraph.SetFontSize(style.Size);
+
+ // 应用粗体 | Apply bold
+ if (style.Bold)
+ {
+ paragraph.SetBold();
+ }
+
+ // 应用斜体 | Apply italic
+ if (style.Italic)
+ {
+ paragraph.SetItalic();
+ }
+
+ // 应用字体颜色 | Apply font color
+ var color = ParseColor(style.Color);
+ if (color != null)
+ {
+ paragraph.SetFontColor(color);
+ }
+
+ // 应用对齐方式 | Apply text alignment
+ paragraph.SetTextAlignment(ParseTextAlignment(style.Align));
+
+ // 应用背景色 | Apply background color
+ if (!string.IsNullOrEmpty(style.BackgroundColor))
+ {
+ var bgColor = ParseColor(style.BackgroundColor);
+ if (bgColor != null)
+ {
+ paragraph.SetBackgroundColor(bgColor);
+ }
+ }
+
+ // 设置固定位置(如果有坐标信息)| Set fixed position if coordinates available
+ if (element.Width > 0)
+ {
+ paragraph.SetWidth(element.Width * MmToPoints);
+ }
+
+ document.Add(paragraph);
+ }
+
+ #endregion
+
+ #region 7.3 字体管理 | Font management
+
+ ///
+ /// 初始化字体(加载嵌入资源字体)| Initialize fonts (load embedded resource fonts)
+ ///
+ private void InitializeFonts()
+ {
+ try
+ {
+ _cjkFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSansCJKsc-Regular.otf");
+ _logger.Info("CJK 字体加载成功 | CJK font loaded successfully");
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("CJK 嵌入字体加载失败,将使用后备字体 | CJK embedded font load failed, will use fallback: {Message}", ex.Message);
+ _cjkFont = null;
+ }
+
+ try
+ {
+ _westernFont = LoadEmbeddedFont("XP.ReportEngine.Fonts.NotoSans-Regular.ttf");
+ _logger.Info("西文字体加载成功 | Western font loaded successfully");
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("西文嵌入字体加载失败,将使用后备字体 | Western embedded font load failed, will use fallback: {Message}", ex.Message);
+ _westernFont = null;
+ }
+
+ // 如果嵌入字体都不可用,使用 iText 内置 Helvetica | If embedded fonts unavailable, use built-in Helvetica
+ if (_cjkFont == null && _westernFont == null)
+ {
+ _logger.Warn("所有嵌入字体不可用,使用 Helvetica 后备字体 | All embedded fonts unavailable, using Helvetica fallback");
+ }
+ }
+
+ ///
+ /// 从嵌入资源加载字体 | Load font from embedded resource
+ ///
+ /// 嵌入资源名称 | Embedded resource name
+ /// PDF 字体对象 | PDF font object
+ private PdfFont LoadEmbeddedFont(string resourceName)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ using (var stream = assembly.GetManifestResourceStream(resourceName))
+ {
+ if (stream == null)
+ {
+ throw new FileNotFoundException($"嵌入资源未找到 | Embedded resource not found: {resourceName}");
+ }
+
+ var fontBytes = new byte[stream.Length];
+ stream.Read(fontBytes, 0, fontBytes.Length);
+
+ var fontProgram = FontProgramFactory.CreateFont(fontBytes);
+ return PdfFontFactory.CreateFont(fontProgram, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);
+ }
+ }
+
+ ///
+ /// 根据当前语言获取合适的字体 | Get appropriate font based on current language
+ /// zh-CN、zh-TW → CJK 字体;en-US → 西文字体
+ ///
+ /// PDF 字体 | PDF font
+ private PdfFont GetFontForCurrentLanguage()
+ {
+ var language = _localizationService.CurrentLanguage;
+
+ PdfFont selectedFont;
+ switch (language)
+ {
+ case SupportedLanguage.ZhCN:
+ case SupportedLanguage.ZhTW:
+ selectedFont = _cjkFont;
+ break;
+ case SupportedLanguage.EnUS:
+ default:
+ selectedFont = _westernFont;
+ break;
+ }
+
+ // 后备字体逻辑 | Fallback font logic
+ if (selectedFont != null)
+ {
+ return selectedFont;
+ }
+
+ // 尝试使用另一种嵌入字体 | Try the other embedded font
+ if (_cjkFont != null) return _cjkFont;
+ if (_westernFont != null) return _westernFont;
+
+ // 最终后备:使用 iText 内置 Helvetica | Final fallback: use built-in Helvetica
+ return PdfFontFactory.CreateFont(iText.IO.Font.Constants.StandardFonts.HELVETICA);
+ }
+
+ #endregion
+
+ #region 7.4 图像嵌入渲染 | Image embedding rendering
+
+ ///
+ /// 渲染图像元素 | Render image element
+ ///
+ private void RenderImageElement(Document document, LayoutElement element)
+ {
+ var imageData = element.ResolvedImage;
+
+ // 如果图像数据缺失,渲染占位矩形 | If image data is missing, render placeholder
+ if (imageData == null)
+ {
+ _logger.Warn("图像数据为空,渲染占位矩形 | Image data is null, rendering placeholder");
+ RenderImagePlaceholder(document, element);
+ return;
+ }
+
+ try
+ {
+ byte[] imageBytes = GetImageBytes(imageData);
+
+ if (imageBytes == null || imageBytes.Length == 0)
+ {
+ _logger.Warn("图像字节数据为空,渲染占位矩形 | Image byte data is empty, rendering placeholder");
+ RenderImagePlaceholder(document, element);
+ return;
+ }
+
+ // 创建 iText 图像对象 | Create iText image object
+ var iTextImageData = iText.IO.Image.ImageDataFactory.Create(imageBytes);
+ var image = new Image(iTextImageData);
+
+ // 计算目标区域尺寸(mm → points)| Calculate target area size (mm → points)
+ float targetWidthPt = element.Width * MmToPoints;
+ float targetHeightPt = element.Height * MmToPoints;
+
+ // 等比缩放以适应目标区域 | Scale proportionally to fit target area
+ 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);
+ }
+ else if (targetWidthPt > 0)
+ {
+ image.SetWidth(targetWidthPt);
+ image.ScaleToFit(targetWidthPt, float.MaxValue);
+ }
+
+ // 应用边框 | Apply border
+ if (element.Source?.Border == true)
+ {
+ image.SetBorder(new SolidBorder(ColorConstants.BLACK, 1f));
+ }
+
+ document.Add(image);
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("图像渲染失败,渲染占位矩形 | Image rendering failed, rendering placeholder: {Message}", ex.Message);
+ RenderImagePlaceholder(document, element);
+ }
+ }
+
+ ///
+ /// 从 ImageData 获取字节数组 | Get byte array from ImageData
+ ///
+ private byte[] GetImageBytes(ImageData imageData)
+ {
+ switch (imageData.SourceType)
+ {
+ case ImageSourceType.Bytes:
+ return imageData.Bytes;
+
+ case ImageSourceType.FilePath:
+ if (!string.IsNullOrEmpty(imageData.FilePath) && File.Exists(imageData.FilePath))
+ {
+ return File.ReadAllBytes(imageData.FilePath);
+ }
+ _logger.Warn("图像文件不存在:{Path} | Image file not found: {Path}", imageData.FilePath);
+ return null;
+
+ case ImageSourceType.BitmapSource:
+ if (imageData.BitmapSource is BitmapSource bitmapSource)
+ {
+ return ConvertBitmapSourceToBytes(bitmapSource);
+ }
+ _logger.Warn("BitmapSource 对象无效 | BitmapSource object is invalid");
+ return null;
+
+ default:
+ _logger.Warn("未知的图像来源类型:{Type} | Unknown image source type: {Type}", imageData.SourceType);
+ return null;
+ }
+ }
+
+ #endregion
+
+ #region 7.5 BitmapSource 转 byte[] | BitmapSource to byte[] conversion
+
+ ///
+ /// 将 BitmapSource 转换为 PNG 编码的字节数组 | Convert BitmapSource to PNG-encoded byte array
+ ///
+ /// WPF BitmapSource 对象 | WPF BitmapSource object
+ /// PNG 编码的字节数组 | PNG-encoded byte array
+ public static byte[] ConvertBitmapSourceToBytes(BitmapSource bitmapSource)
+ {
+ if (bitmapSource == null)
+ {
+ return null;
+ }
+
+ using (var memoryStream = new MemoryStream())
+ {
+ var encoder = new PngBitmapEncoder();
+ encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
+ encoder.Save(memoryStream);
+ return memoryStream.ToArray();
+ }
+ }
+
+ #endregion
+
+ #region 7.6 图像占位矩形渲染 | Image placeholder rendering
+
+ ///
+ /// 渲染图像缺失时的占位矩形(带"无图像 | No Image"文本标签)
+ /// Render placeholder rectangle when image is missing (with "无图像 | No Image" text label)
+ ///
+ private void RenderImagePlaceholder(Document document, LayoutElement element)
+ {
+ float widthPt = element.Width > 0 ? element.Width * MmToPoints : 100f * MmToPoints;
+ float heightPt = element.Height > 0 ? element.Height * MmToPoints : 60f * MmToPoints;
+
+ // 使用表格模拟占位矩形(带边框和居中文本)| Use table to simulate placeholder rectangle
+ var table = new Table(1);
+ table.SetWidth(widthPt);
+
+ var cell = new Cell();
+ cell.SetHeight(heightPt);
+ cell.SetBorder(new SolidBorder(ColorConstants.GRAY, 1f));
+ cell.SetBackgroundColor(new DeviceRgb(245, 245, 245));
+
+ // 居中显示"无图像 | No Image"文本 | Center "无图像 | No Image" text
+ var placeholderText = new Paragraph("无图像 | No Image");
+ var font = GetFontForCurrentLanguage();
+ placeholderText.SetFont(font);
+ placeholderText.SetFontSize(10f);
+ placeholderText.SetFontColor(ColorConstants.GRAY);
+ placeholderText.SetTextAlignment(TextAlignment.CENTER);
+
+ cell.SetVerticalAlignment(VerticalAlignment.MIDDLE);
+ cell.Add(placeholderText);
+ table.AddCell(cell);
+
+ document.Add(table);
+ }
+
+ #endregion
+
+ #region 7.7 表格渲染 | Table rendering
+
+ ///
+ /// 渲染表格元素 | Render table element
+ ///
+ private void RenderTableElement(Document document, LayoutElement element)
+ {
+ var columns = element.Source?.Columns;
+ var tableData = element.ResolvedTableData;
+
+ if (columns == null || columns.Count == 0)
+ {
+ _logger.Warn("表格列定义为空,跳过渲染 | Table column definitions are empty, skipping");
+ return;
+ }
+
+ // 计算列宽(mm → points)| Calculate column widths (mm → points)
+ var columnWidths = new float[columns.Count];
+ for (int i = 0; i < columns.Count; i++)
+ {
+ columnWidths[i] = columns[i].Width > 0 ? columns[i].Width * MmToPoints : 30f * MmToPoints;
+ }
+
+ var table = new Table(columnWidths);
+ table.SetWidth(UnitValue.CreatePercentValue(100));
+
+ var font = GetFontForCurrentLanguage();
+
+ // 渲染表头行 | Render header row
+ var headerBgColor = ParseColor(HeaderBackgroundColor);
+ foreach (var column in columns)
+ {
+ var headerCell = new Cell();
+ var headerParagraph = new Paragraph(column.Header ?? string.Empty);
+ headerParagraph.SetFont(font);
+ headerParagraph.SetFontSize(10f);
+ headerParagraph.SetBold();
+ headerParagraph.SetTextAlignment(ParseTextAlignment(column.Align));
+
+ headerCell.Add(headerParagraph);
+ headerCell.SetBackgroundColor(headerBgColor);
+ headerCell.SetBorder(new SolidBorder(ColorConstants.LIGHT_GRAY, 0.5f));
+
+ table.AddHeaderCell(headerCell);
+ }
+
+ // 渲染数据行(交替背景色)| Render data rows (alternating background colors)
+ if (tableData != null)
+ {
+ for (int rowIndex = 0; rowIndex < tableData.Count; rowIndex++)
+ {
+ var rowData = tableData[rowIndex];
+ var rowBgColor = rowIndex % 2 == 0
+ ? ParseColor(OddRowBackgroundColor)
+ : ParseColor(EvenRowBackgroundColor);
+
+ foreach (var column in columns)
+ {
+ var dataCell = new Cell();
+
+ // 从行数据中获取字段值 | Get field value from row data
+ string cellValue = string.Empty;
+ if (rowData != null && !string.IsNullOrEmpty(column.Field) && rowData.ContainsKey(column.Field))
+ {
+ cellValue = rowData[column.Field]?.ToString() ?? string.Empty;
+ }
+
+ var cellParagraph = new Paragraph(cellValue);
+ cellParagraph.SetFont(font);
+ cellParagraph.SetFontSize(9f);
+ cellParagraph.SetTextAlignment(ParseTextAlignment(column.Align));
+
+ dataCell.Add(cellParagraph);
+ dataCell.SetBackgroundColor(rowBgColor);
+ dataCell.SetBorder(new SolidBorder(ColorConstants.LIGHT_GRAY, 0.5f));
+
+ table.AddCell(dataCell);
+ }
+ }
+ }
+
+ document.Add(table);
+ }
+
+ #endregion
+
+ #region 7.8 分隔线渲染 | Divider rendering
+
+ ///
+ /// 渲染分隔线元素 | Render divider element
+ ///
+ private void RenderDividerElement(Document document, LayoutElement element)
+ {
+ var style = element.ResolvedStyle;
+
+ // 确定分隔线颜色(从样式或默认灰色)| Determine divider color (from style or default gray)
+ Color lineColor = ColorConstants.GRAY;
+ if (style != null && !string.IsNullOrEmpty(style.Color))
+ {
+ var parsedColor = ParseColor(style.Color);
+ if (parsedColor != null)
+ {
+ lineColor = parsedColor;
+ }
+ }
+
+ // 使用 LineSeparator 渲染水平分隔线 | Use LineSeparator to render horizontal divider
+ var lineSeparator = new LineSeparator(new SolidLine(1f));
+ lineSeparator.SetStrokeColor(lineColor);
+
+ // 设置宽度为可用区域全宽 | Set width to full available area
+ if (element.Width > 0)
+ {
+ lineSeparator.SetWidth(element.Width * MmToPoints);
+ }
+
+ // 添加上下间距 | Add vertical spacing
+ lineSeparator.SetMarginTop(5f);
+ lineSeparator.SetMarginBottom(5f);
+
+ document.Add(lineSeparator);
+ }
+
+ #endregion
+
+ #region 7.9 PDF 保存到文件 | PDF save to file
+
+ ///
+ /// 将 PDF 内存流保存到文件 | Save PDF memory stream to file
+ ///
+ /// PDF 内存流 | PDF memory stream
+ /// 输出文件路径 | Output file path
+ /// 保存结果(成功/失败)| Save result (success/failure)
+ public ReportResult SaveToFile(MemoryStream pdfStream, string filePath)
+ {
+ if (pdfStream == null)
+ {
+ return ReportResult.Failure("PDF 流为空,无法保存 | PDF stream is null, cannot save");
+ }
+
+ if (string.IsNullOrWhiteSpace(filePath))
+ {
+ return ReportResult.Failure("输出文件路径为空 | Output file path is empty");
+ }
+
+ try
+ {
+ _logger.Info("开始保存 PDF 到文件:{FilePath} | Saving PDF to file: {FilePath}", filePath);
+
+ // 确保目标目录存在 | Ensure target directory exists
+ var directory = System.IO.Path.GetDirectoryName(filePath);
+ if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ // 保存流位置并重置 | Save stream position and reset
+ long originalPosition = pdfStream.Position;
+ pdfStream.Position = 0;
+
+ using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ pdfStream.CopyTo(fileStream);
+ }
+
+ // 恢复流位置 | Restore stream position
+ pdfStream.Position = originalPosition;
+
+ _logger.Info("PDF 文件保存成功:{FilePath} | PDF file saved successfully: {FilePath}", filePath);
+ return ReportResult.Success(pdfStream);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.Error(ex, "PDF 保存失败:无写入权限 | PDF save failed: no write permission: {Path}", filePath);
+ return ReportResult.Failure($"无法写入文件,权限不足:{filePath} | Cannot write file, insufficient permissions: {filePath}", ex);
+ }
+ catch (DirectoryNotFoundException ex)
+ {
+ _logger.Error(ex, "PDF 保存失败:目录不存在 | PDF save failed: directory not found: {Path}", filePath);
+ return ReportResult.Failure($"目标目录不存在:{filePath} | Target directory not found: {filePath}", ex);
+ }
+ catch (IOException ex)
+ {
+ _logger.Error(ex, "PDF 保存失败:IO 错误 | PDF save failed: IO error: {Path}", filePath);
+ return ReportResult.Failure($"文件保存 IO 错误:{ex.Message} | File save IO error: {ex.Message}", ex);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "PDF 保存失败:未知错误 | PDF save failed: unknown error: {Path}", filePath);
+ return ReportResult.Failure($"文件保存过程中发生错误:{ex.Message} | Error occurred during file save: {ex.Message}", ex);
+ }
+ }
+
+ #endregion
+
+ #region 辅助方法 | Helper methods
+
+ ///
+ /// 解析十六进制颜色字符串为 iText Color 对象 | Parse hex color string to iText Color object
+ /// 支持格式:#RRGGBB 或 #RGB | Supports formats: #RRGGBB or #RGB
+ ///
+ /// 十六进制颜色字符串 | Hex color string
+ /// iText Color 对象,解析失败返回黑色 | iText Color object, returns black on failure
+ private Color ParseColor(string hexColor)
+ {
+ if (string.IsNullOrEmpty(hexColor))
+ {
+ return ColorConstants.BLACK;
+ }
+
+ try
+ {
+ var hex = hexColor.TrimStart('#');
+
+ if (hex.Length == 3)
+ {
+ // 扩展 #RGB 为 #RRGGBB | Expand #RGB to #RRGGBB
+ hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}";
+ }
+
+ if (hex.Length == 6)
+ {
+ int r = Convert.ToInt32(hex.Substring(0, 2), 16);
+ int g = Convert.ToInt32(hex.Substring(2, 2), 16);
+ int b = Convert.ToInt32(hex.Substring(4, 2), 16);
+ return new DeviceRgb(r, g, b);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Warn("颜色解析失败:{Color},使用默认黑色 | Color parsing failed: {Color}, using default black: {Message}", hexColor, ex.Message);
+ }
+
+ return ColorConstants.BLACK;
+ }
+
+ ///
+ /// 解析对齐方式字符串为 iText TextAlignment | Parse alignment string to iText TextAlignment
+ ///
+ /// 对齐方式字符串(left/center/right)| Alignment string
+ /// iText TextAlignment 枚举值 | iText TextAlignment enum value
+ private TextAlignment ParseTextAlignment(string align)
+ {
+ switch (align?.ToLowerInvariant())
+ {
+ case "center":
+ return TextAlignment.CENTER;
+ case "right":
+ return TextAlignment.RIGHT;
+ case "justify":
+ return TextAlignment.JUSTIFIED;
+ case "left":
+ default:
+ return TextAlignment.LEFT;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/XP.ReportEngine/Services/JsonTemplateEngine.cs b/XP.ReportEngine/Services/JsonTemplateEngine.cs
new file mode 100644
index 0000000..d7f9036
--- /dev/null
+++ b/XP.ReportEngine/Services/JsonTemplateEngine.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Newtonsoft.Json;
+using XP.Common.Logging.Interfaces;
+using XP.ReportEngine.Interfaces;
+using XP.ReportEngine.Models;
+
+namespace XP.ReportEngine.Services
+{
+ ///
+ /// JSON 模板引擎实现 | JSON template engine implementation
+ /// 负责加载、反序列化和验证 JSON 格式的报告模板
+ /// Responsible for loading, deserializing and validating JSON report templates
+ ///
+ public class JsonTemplateEngine : ITemplateEngine
+ {
+ private readonly ILoggerService _logger;
+
+ ///
+ /// 默认样式定义(当模板引用未定义的样式名称时使用)
+ /// Default style definition (used when template references undefined style name)
+ ///
+ public static readonly StyleDefinition DefaultStyle = new()
+ {
+ Font = null,
+ Size = 12f,
+ Bold = false,
+ Italic = false,
+ Color = "#000000",
+ Align = "left",
+ BackgroundColor = null
+ };
+
+ ///
+ /// 构造函数 | Constructor
+ ///
+ /// 日志服务 | Logger service
+ public JsonTemplateEngine(ILoggerService logger)
+ {
+ _logger = logger?.ForModule() ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ /// 加载并反序列化 JSON 模板文件 | Load and deserialize JSON template file
+ ///
+ /// 模板文件路径 | Template file path
+ /// 解析后的模板对象,文件不存在时返回 null | Parsed template object, null if file not found
+ public ReportTemplate LoadTemplate(string templatePath)
+ {
+ if (string.IsNullOrWhiteSpace(templatePath))
+ {
+ _logger.Warn("模板文件路径为空 | Template file path is null or empty");
+ return null;
+ }
+
+ if (!File.Exists(templatePath))
+ {
+ _logger.Warn("模板文件未找到: {Path} | Template file not found: {Path}", templatePath);
+ return null;
+ }
+
+ _logger.Info("开始加载模板文件: {Path} | Loading template file: {Path}", templatePath);
+
+ try
+ {
+ var json = File.ReadAllText(templatePath);
+ var template = JsonConvert.DeserializeObject(json);
+
+ _logger.Info("模板文件加载成功 | Template file loaded successfully");
+ return template;
+ }
+ catch (JsonReaderException ex)
+ {
+ // JSON 语法错误,包含错误位置信息 | JSON syntax error with position info
+ var message = $"模板 JSON 语法错误,行 {ex.LineNumber},位置 {ex.LinePosition}: {ex.Message} | " +
+ $"Template JSON syntax error at line {ex.LineNumber}, position {ex.LinePosition}: {ex.Message}";
+ _logger.Error(ex, message);
+ throw new InvalidOperationException(message, ex);
+ }
+ catch (JsonSerializationException ex)
+ {
+ // JSON 反序列化错误 | JSON deserialization error
+ var message = $"模板 JSON 反序列化失败: {ex.Message} | Template JSON deserialization failed: {ex.Message}";
+ _logger.Error(ex, message);
+ throw new InvalidOperationException(message, ex);
+ }
+ }
+
+ ///
+ /// 验证模板结构完整性 | Validate template structure integrity
+ /// 检查 document、pages、styles 必需字段是否存在
+ /// Checks if required fields (document, pages, styles) are present
+ ///
+ /// 待验证的模板 | Template to validate
+ /// 验证结果 | Validation result
+ public TemplateValidationResult Validate(ReportTemplate template)
+ {
+ if (template == null)
+ {
+ return TemplateValidationResult.Invalid("模板对象为 null | Template object is null");
+ }
+
+ var missingFields = new List();
+
+ if (template.Document == null)
+ {
+ missingFields.Add("document");
+ }
+
+ if (template.Pages == null || template.Pages.Count == 0)
+ {
+ missingFields.Add("pages");
+ }
+
+ if (template.Styles == null)
+ {
+ missingFields.Add("styles");
+ }
+
+ if (missingFields.Count > 0)
+ {
+ var fieldList = string.Join(", ", missingFields);
+ var message = $"模板缺少必需字段: {fieldList} | Template missing required fields: {fieldList}";
+ _logger.Warn(message);
+ return TemplateValidationResult.Invalid(message);
+ }
+
+ _logger.Info("模板验证通过 | Template validation passed");
+ return TemplateValidationResult.Valid();
+ }
+
+ ///
+ /// 解析样式名称,未定义时回退为默认样式 | Resolve style name, fallback to default if undefined
+ ///
+ /// 报告模板 | Report template
+ /// 样式名称 | Style name
+ /// 解析后的样式定义 | Resolved style definition
+ public StyleDefinition ResolveStyle(ReportTemplate template, string styleName)
+ {
+ // 样式名称为空时直接返回默认样式 | Return default style if style name is empty
+ if (string.IsNullOrWhiteSpace(styleName))
+ {
+ return DefaultStyle;
+ }
+
+ // 模板或样式字典为空时返回默认样式 | Return default style if template or styles dictionary is null
+ if (template?.Styles == null)
+ {
+ _logger.Warn("模板样式字典为空,使用默认样式: {StyleName} | Template styles dictionary is null, using default style: {StyleName}", styleName);
+ return DefaultStyle;
+ }
+
+ // 查找样式,找到则返回,否则回退为默认样式并记录警告
+ // Look up style, return if found, otherwise fallback to default and log warning
+ if (template.Styles.TryGetValue(styleName, out var style))
+ {
+ return style;
+ }
+
+ _logger.Warn("未定义的样式名称 '{StyleName}',使用默认样式 | Undefined style name '{StyleName}', using default style", styleName);
+ return DefaultStyle;
+ }
+ }
+}
diff --git a/XP.ReportEngine/Services/PageLayoutEngine.cs b/XP.ReportEngine/Services/PageLayoutEngine.cs
new file mode 100644
index 0000000..5e2257d
--- /dev/null
+++ b/XP.ReportEngine/Services/PageLayoutEngine.cs
@@ -0,0 +1,469 @@
+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,
+ 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);
+
+ // 检查是否需要分页 | 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);
+ }
+ else
+ {
+ // 普通元素分页检查 | Normal element pagination check
+ if (currentY + elementHeight > margins.Top + availableHeight)
+ {
+ // 创建新页面 | Create new page
+ currentPageNumber++;
+ currentPage = new LayoutPage
+ {
+ PageNumber = currentPageNumber,
+ 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)
+ {
+ 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,
+ 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,
+ 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,
+ 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,
+ "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);
+ }
+
+ ///
+ /// 计算元素宽度 | 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;
+ }
+ }
+}
diff --git a/XP.ReportEngine/Templates/.gitkeep b/XP.ReportEngine/Templates/.gitkeep
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/XP.ReportEngine/Templates/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/XP.ReportEngine/XP.ReportEngine.csproj b/XP.ReportEngine/XP.ReportEngine.csproj
index ad1209d..f26f2a6 100644
--- a/XP.ReportEngine/XP.ReportEngine.csproj
+++ b/XP.ReportEngine/XP.ReportEngine.csproj
@@ -4,6 +4,9 @@
true
+
+
+
@@ -13,4 +16,10 @@
-
\ No newline at end of file
+
+
+
+
+
+
+
diff --git a/XplorePlane.sln b/XplorePlane.sln
index ece241e..326513c 100644
--- a/XplorePlane.sln
+++ b/XplorePlane.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 18
-VisualStudioVersion = 18.5.11723.231 stable
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.37203.1 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XplorePlane", "XplorePlane\XplorePlane.csproj", "{07978DB9-4B88-4F42-9054-73992742BD6A}"
EndProject