报告ReportEngineBase修改语言资源文件,修改模板和报告输出功能。

This commit is contained in:
QI Mingxuan
2026-05-13 17:36:54 +08:00
parent 1d3cacea75
commit 11c69d03fb
8 changed files with 1166 additions and 119 deletions
+62 -15
View File
@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@@ -58,15 +117,14 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- Report metadata keys -->
<data name="Report_Title" xml:space="preserve">
<value>Inspection Report</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>Inspection Date</value>
<value>Inspection Time</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>Sample Name</value>
<value>Product Name</value>
</data>
<data name="Report_Operator" xml:space="preserve">
<value>Operator</value>
@@ -80,14 +138,12 @@
<data name="Report_Description" xml:space="preserve">
<value>Description</value>
</data>
<!-- Classification keys -->
<data name="Classification_Pass" xml:space="preserve">
<value>PASS</value>
</data>
<data name="Classification_Fail" xml:space="preserve">
<value>FAIL</value>
</data>
<!-- Inspection type keys -->
<data name="Inspection_LineMeasurement" xml:space="preserve">
<value>Line Measurement</value>
</data>
@@ -100,7 +156,6 @@
<data name="Inspection_FillRate" xml:space="preserve">
<value>Via Fill Rate</value>
</data>
<!-- Table header keys -->
<data name="Table_Index" xml:space="preserve">
<value>No.</value>
</data>
@@ -122,7 +177,6 @@
<data name="Table_CenterY" xml:space="preserve">
<value>Center Y</value>
</data>
<!-- Measurement keys -->
<data name="Measurement_Type" xml:space="preserve">
<value>Measurement Type</value>
</data>
@@ -144,7 +198,6 @@
<data name="Measurement_Limit" xml:space="preserve">
<value>Limit</value>
</data>
<!-- Summary keys -->
<data name="Summary_TotalDefects" xml:space="preserve">
<value>Total Defects</value>
</data>
@@ -157,11 +210,9 @@
<data name="Summary_OverallResult" xml:space="preserve">
<value>Overall Result</value>
</data>
<!-- Image placeholder key -->
<data name="Image_NoImage" xml:space="preserve">
<value>No Image</value>
</data>
<!-- Page title keys -->
<data name="Page_Homepage" xml:space="preserve">
<value>Inspection Report</value>
</data>
@@ -177,7 +228,6 @@
<data name="Page_ViaFillInspection" xml:space="preserve">
<value>Via Fill Rate Inspection</value>
</data>
<!-- BGA inspection keys -->
<data name="Bga_Count" xml:space="preserve">
<value>Ball Count</value>
</data>
@@ -193,7 +243,6 @@
<data name="Bga_BallIndex" xml:space="preserve">
<value>Ball No.</value>
</data>
<!-- Void inspection keys -->
<data name="Void_RoiArea" xml:space="preserve">
<value>ROI Area</value>
</data>
@@ -209,7 +258,6 @@
<data name="Void_Limit" xml:space="preserve">
<value>Void Rate Limit</value>
</data>
<!-- Via fill rate keys -->
<data name="Fill_Rate" xml:space="preserve">
<value>Fill Rate</value>
</data>
@@ -222,7 +270,6 @@
<data name="Fill_THTLimit" xml:space="preserve">
<value>THT Limit</value>
</data>
<!-- Common field keys -->
<data name="Field_Point1" xml:space="preserve">
<value>Point 1</value>
</data>
@@ -238,4 +285,4 @@
<data name="Field_Status" xml:space="preserve">
<value>Status</value>
</data>
</root>
</root>
+62 -15
View File
@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@@ -58,15 +117,14 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- 报告元数据键 | Report metadata keys -->
<data name="Report_Title" xml:space="preserve">
<value>检测报告</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>检测日期</value>
<value>检测时间</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>品名称</value>
<value>品名称</value>
</data>
<data name="Report_Operator" xml:space="preserve">
<value>操作员</value>
@@ -80,14 +138,12 @@
<data name="Report_Description" xml:space="preserve">
<value>描述</value>
</data>
<!-- 分类结果键 | Classification keys -->
<data name="Classification_Pass" xml:space="preserve">
<value>通过</value>
</data>
<data name="Classification_Fail" xml:space="preserve">
<value>不通过</value>
</data>
<!-- 检测类型键 | Inspection type keys -->
<data name="Inspection_LineMeasurement" xml:space="preserve">
<value>距离测量</value>
</data>
@@ -100,7 +156,6 @@
<data name="Inspection_FillRate" xml:space="preserve">
<value>通孔填锡率</value>
</data>
<!-- 表格列头键 | Table header keys -->
<data name="Table_Index" xml:space="preserve">
<value>序号</value>
</data>
@@ -122,7 +177,6 @@
<data name="Table_CenterY" xml:space="preserve">
<value>中心 Y</value>
</data>
<!-- 测量键 | Measurement keys -->
<data name="Measurement_Type" xml:space="preserve">
<value>测量类型</value>
</data>
@@ -144,7 +198,6 @@
<data name="Measurement_Limit" xml:space="preserve">
<value>限值</value>
</data>
<!-- 摘要键 | Summary keys -->
<data name="Summary_TotalDefects" xml:space="preserve">
<value>总缺陷数</value>
</data>
@@ -157,11 +210,9 @@
<data name="Summary_OverallResult" xml:space="preserve">
<value>总体结果</value>
</data>
<!-- 图像占位符键 | Image placeholder key -->
<data name="Image_NoImage" xml:space="preserve">
<value>无图像</value>
</data>
<!-- 页面标题键 | Page title keys -->
<data name="Page_Homepage" xml:space="preserve">
<value>检测报告首页</value>
</data>
@@ -177,7 +228,6 @@
<data name="Page_ViaFillInspection" xml:space="preserve">
<value>通孔填锡检测</value>
</data>
<!-- BGA 检测键 | BGA inspection keys -->
<data name="Bga_Count" xml:space="preserve">
<value>焊球数量</value>
</data>
@@ -193,7 +243,6 @@
<data name="Bga_BallIndex" xml:space="preserve">
<value>焊球序号</value>
</data>
<!-- 空隙检测键 | Void inspection keys -->
<data name="Void_RoiArea" xml:space="preserve">
<value>ROI 面积</value>
</data>
@@ -209,7 +258,6 @@
<data name="Void_Limit" xml:space="preserve">
<value>空隙率限值</value>
</data>
<!-- 通孔填锡键 | Via fill rate keys -->
<data name="Fill_Rate" xml:space="preserve">
<value>填锡率</value>
</data>
@@ -222,7 +270,6 @@
<data name="Fill_THTLimit" xml:space="preserve">
<value>THT 限值</value>
</data>
<!-- 通用字段键 | Common field keys -->
<data name="Field_Point1" xml:space="preserve">
<value>起点</value>
</data>
@@ -238,4 +285,4 @@
<data name="Field_Status" xml:space="preserve">
<value>状态</value>
</data>
</root>
</root>
+62 -15
View File
@@ -1,5 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@@ -58,15 +117,14 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- 报告元数据键 | Report metadata keys -->
<data name="Report_Title" xml:space="preserve">
<value>检测报告</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>检测日期</value>
<value>检测时间</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>品名称</value>
<value>品名称</value>
</data>
<data name="Report_Operator" xml:space="preserve">
<value>操作员</value>
@@ -80,14 +138,12 @@
<data name="Report_Description" xml:space="preserve">
<value>描述</value>
</data>
<!-- 分类结果键 | Classification keys -->
<data name="Classification_Pass" xml:space="preserve">
<value>通过</value>
</data>
<data name="Classification_Fail" xml:space="preserve">
<value>不通过</value>
</data>
<!-- 检测类型键 | Inspection type keys -->
<data name="Inspection_LineMeasurement" xml:space="preserve">
<value>距离测量</value>
</data>
@@ -100,7 +156,6 @@
<data name="Inspection_FillRate" xml:space="preserve">
<value>通孔填锡率</value>
</data>
<!-- 表格列头键 | Table header keys -->
<data name="Table_Index" xml:space="preserve">
<value>序号</value>
</data>
@@ -122,7 +177,6 @@
<data name="Table_CenterY" xml:space="preserve">
<value>中心 Y</value>
</data>
<!-- 测量键 | Measurement keys -->
<data name="Measurement_Type" xml:space="preserve">
<value>测量类型</value>
</data>
@@ -144,7 +198,6 @@
<data name="Measurement_Limit" xml:space="preserve">
<value>限值</value>
</data>
<!-- 摘要键 | Summary keys -->
<data name="Summary_TotalDefects" xml:space="preserve">
<value>总缺陷数</value>
</data>
@@ -157,11 +210,9 @@
<data name="Summary_OverallResult" xml:space="preserve">
<value>总体结果</value>
</data>
<!-- 图像占位符键 | Image placeholder key -->
<data name="Image_NoImage" xml:space="preserve">
<value>无图像</value>
</data>
<!-- 页面标题键 | Page title keys -->
<data name="Page_Homepage" xml:space="preserve">
<value>检测报告首页</value>
</data>
@@ -177,7 +228,6 @@
<data name="Page_ViaFillInspection" xml:space="preserve">
<value>通孔填锡检测</value>
</data>
<!-- BGA 检测键 | BGA inspection keys -->
<data name="Bga_Count" xml:space="preserve">
<value>焊球数量</value>
</data>
@@ -193,7 +243,6 @@
<data name="Bga_BallIndex" xml:space="preserve">
<value>焊球序号</value>
</data>
<!-- 空隙检测键 | Void inspection keys -->
<data name="Void_RoiArea" xml:space="preserve">
<value>ROI 面积</value>
</data>
@@ -209,7 +258,6 @@
<data name="Void_Limit" xml:space="preserve">
<value>空隙率限值</value>
</data>
<!-- 通孔填锡键 | Via fill rate keys -->
<data name="Fill_Rate" xml:space="preserve">
<value>填锡率</value>
</data>
@@ -222,7 +270,6 @@
<data name="Fill_THTLimit" xml:space="preserve">
<value>THT 限值</value>
</data>
<!-- 通用字段键 | Common field keys -->
<data name="Field_Point1" xml:space="preserve">
<value>起点</value>
</data>
@@ -238,4 +285,4 @@
<data name="Field_Status" xml:space="preserve">
<value>状态</value>
</data>
</root>
</root>
@@ -63,7 +63,7 @@
<value>檢測報告</value>
</data>
<data name="Report_Date" xml:space="preserve">
<value>檢測日期</value>
<value>檢測時間</value>
</data>
<data name="Report_Sample" xml:space="preserve">
<value>樣品名稱</value>
@@ -54,6 +54,9 @@ namespace XP.ReportEngine.Services
}
}
// 绑定页眉页脚中的表达式 | Bind expressions in header/footer
BindHeaderFooter(clonedTemplate, context);
_logger.Info("数据绑定完成 | Data binding completed");
return clonedTemplate;
}
@@ -99,6 +102,62 @@ namespace XP.ReportEngine.Services
}
}
}
// Row 子元素递归绑定 | Recursively bind row child elements
if (string.Equals(element.Type, "row", StringComparison.OrdinalIgnoreCase)
&& element.Children != null)
{
foreach (var child in element.Children)
{
BindElement(child, context);
// Column 子元素递归绑定 | Recursively bind column child elements
if (string.Equals(child.Type, "column", StringComparison.OrdinalIgnoreCase)
&& child.Children != null)
{
foreach (var subChild in child.Children)
{
BindElement(subChild, context);
}
}
}
}
}
/// <summary>
/// 绑定页眉页脚中的 ${} 表达式 | Bind ${} expressions in header/footer
/// </summary>
private void BindHeaderFooter(ReportTemplate template, ReportContext context)
{
if (template?.Document == null) return;
// 绑定页眉文本 | Bind header text
if (template.Document.Header != null && template.Document.Header.Enabled)
{
BindStringList(template.Document.Header.Left, context);
BindStringList(template.Document.Header.Right, context);
}
// 绑定页脚文本 | Bind footer text
if (template.Document.Footer != null && template.Document.Footer.Enabled)
{
BindStringList(template.Document.Footer.Left, context);
BindStringList(template.Document.Footer.Right, context);
}
}
/// <summary>
/// 绑定字符串列表中的表达式 | Bind expressions in string list
/// </summary>
private void BindStringList(List<string> lines, ReportContext context)
{
if (lines == null) return;
for (int i = 0; i < lines.Count; i++)
{
if (!string.IsNullOrEmpty(lines[i]))
{
lines[i] = ResolveAllExpressions(lines[i], context);
}
}
}
private string ResolveAllExpressions(string input, ReportContext context)
@@ -313,10 +372,10 @@ namespace XP.ReportEngine.Services
var format = _localizationService.CurrentLanguage switch
{
SupportedLanguage.ZhCN => "yyyy年MM月dd日",
SupportedLanguage.ZhTW => "yyyy年MM月dd日",
SupportedLanguage.EnUS => "MM/dd/yyyy",
_ => "yyyy-MM-dd"
SupportedLanguage.ZhCN => "yyyy年MM月dd日 HH:mm:ss",
SupportedLanguage.ZhTW => "yyyy年MM月dd日 HH:mm:ss",
SupportedLanguage.EnUS => "MM/dd/yyyy HH:mm:ss",
_ => "yyyy-MM-dd HH:mm:ss"
};
return dateTime.ToString(format);
+680 -1
View File
@@ -62,6 +62,7 @@ namespace XP.ReportEngine.Services
private PdfFont _westernFont;
private bool _fontsInitialized;
private readonly object _fontLock = new();
private ReportTemplate _currentTemplate;
public ITextPdfRenderer(ILoggerService logger, ILocalizationService localizationService)
{
@@ -77,10 +78,12 @@ namespace XP.ReportEngine.Services
/// </summary>
/// <param name="pages">排版后的页面列表 | Laid-out pages</param>
/// <param name="options">生成选项 | Generation options</param>
/// <param name="template">绑定后的模板(用于页眉页脚配置)| Bound template (for header/footer config)</param>
/// <returns>PDF 内存流 | PDF memory stream</returns>
public MemoryStream Render(List<LayoutPage> pages, ReportGenerationOptions options)
public MemoryStream Render(List<LayoutPage> pages, ReportGenerationOptions options, ReportTemplate template = null)
{
_logger.Info("开始 PDF 渲染,共 {PageCount} 页 | Starting PDF rendering, {PageCount} pages", pages?.Count ?? 0);
_currentTemplate = template;
var memoryStream = new MemoryStream();
@@ -102,8 +105,27 @@ namespace XP.ReportEngine.Services
float marginLeft = 20f * MmToPoints;
float marginRight = 20f * MmToPoints;
// 如果有页眉页脚配置,为内容页增加边距空间 | Increase margins for header/footer on content pages
var headerConfig = template?.Document?.Header;
var footerConfig = template?.Document?.Footer;
bool hasHeader = headerConfig != null && headerConfig.Enabled;
bool hasFooter = footerConfig != null && footerConfig.Enabled;
// 页眉页脚占用的额外空间(mm → points| Extra space for header/footer
float headerAreaHeight = hasHeader ? 15f * MmToPoints : 0f;
float footerAreaHeight = hasFooter ? 12f * MmToPoints : 0f;
document.SetMargins(marginTop, marginRight, marginBottom, marginLeft);
// 注册页眉页脚事件处理器 | Register header/footer event handler
HeaderFooterEventHandler headerFooterHandler = null;
if (hasHeader || hasFooter)
{
headerFooterHandler = new HeaderFooterEventHandler(
this, template, pages, _logger);
pdfDocument.AddEventHandler(iText.Kernel.Events.PdfDocumentEvent.END_PAGE, headerFooterHandler);
}
if (pages != null && pages.Count > 0)
{
for (int i = 0; i < pages.Count; i++)
@@ -114,10 +136,27 @@ namespace XP.ReportEngine.Services
document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
}
// 非首页增加页眉页脚边距 | Add header/footer margins for non-homepage
bool isHomepage = string.Equals(pages[i].PageType, "homepage", StringComparison.OrdinalIgnoreCase);
if (!isHomepage)
{
// 通过添加顶部间距为页眉留出空间 | Add top spacing for header area
if (hasHeader)
{
document.Add(new Paragraph("").SetMarginBottom(headerAreaHeight).SetFontSize(1));
}
}
RenderPage(document, pages[i]);
}
}
// 文档关闭前回填总页数占位符 | Fill total page count placeholder before closing
if (headerFooterHandler != null)
{
headerFooterHandler.WriteTotal(pdfDocument);
}
// 仅关闭 Document(它会级联关闭 PdfDocument 和 PdfWriter
// Only close Document (it cascades to PdfDocument and PdfWriter)
document.Close();
@@ -178,6 +217,15 @@ namespace XP.ReportEngine.Services
case "divider":
RenderDividerElement(document, element);
break;
case "spacer":
RenderSpacerElement(document, element);
break;
case "row":
RenderRowElement(document, element);
break;
case "pagebreak":
RenderPageBreakElement(document);
break;
default:
_logger.Warn("未知的元素类型:{Type},跳过渲染 | Unknown element type: {Type}, skipping", elementType);
break;
@@ -198,6 +246,10 @@ namespace XP.ReportEngine.Services
var paragraph = new Paragraph(content);
// 设置紧凑的默认段落间距 | Set compact default paragraph spacing
paragraph.SetMarginTop(0);
paragraph.SetMarginBottom(2f);
// 应用字体 | Apply font
var font = GetFontForCurrentLanguage();
paragraph.SetFont(font);
@@ -224,6 +276,24 @@ namespace XP.ReportEngine.Services
paragraph.SetFontColor(color);
}
// 应用条件颜色规则(根据内容关键词覆盖颜色)| Apply conditional color rules (override color by content keywords)
if (element.Source?.ColorRules != null && !string.IsNullOrEmpty(content))
{
foreach (var rule in element.Source.ColorRules)
{
if (content.Contains(rule.Key, StringComparison.OrdinalIgnoreCase))
{
var ruleColor = ParseColor(rule.Value);
if (ruleColor != null)
{
paragraph.SetFontColor(ruleColor);
paragraph.SetBold();
}
break;
}
}
}
// 应用对齐方式 | Apply text alignment
paragraph.SetTextAlignment(ParseTextAlignment(style.Align));
@@ -243,6 +313,16 @@ namespace XP.ReportEngine.Services
paragraph.SetWidth(element.Width * MmToPoints);
}
// 应用边距和缩进 | Apply margins and indent
if (style.MarginTop > 0)
paragraph.SetMarginTop(style.MarginTop * MmToPoints);
if (style.MarginBottom > 0)
paragraph.SetMarginBottom(style.MarginBottom * MmToPoints);
if (style.PaddingLeft > 0)
paragraph.SetPaddingLeft(style.PaddingLeft * MmToPoints);
if (style.LineHeight > 0)
paragraph.SetMultipliedLeading(style.LineHeight);
document.Add(paragraph);
}
@@ -419,6 +499,27 @@ namespace XP.ReportEngine.Services
image.SetBorder(new SolidBorder(ColorConstants.BLACK, 1f));
}
// 应用对齐方式 | Apply alignment
var align = element.Source?.Align?.ToLowerInvariant();
if (align == "center")
{
image.SetHorizontalAlignment(HorizontalAlignment.CENTER);
}
else if (align == "right")
{
image.SetHorizontalAlignment(HorizontalAlignment.RIGHT);
}
// 应用样式中的边距 | Apply margins from style
var style = element.ResolvedStyle;
if (style != null)
{
if (style.MarginTop > 0)
image.SetMarginTop(style.MarginTop * MmToPoints);
if (style.MarginBottom > 0)
image.SetMarginBottom(style.MarginBottom * MmToPoints);
}
document.Add(image);
}
catch (Exception ex)
@@ -596,6 +697,25 @@ namespace XP.ReportEngine.Services
cellParagraph.SetFontSize(9f);
cellParagraph.SetTextAlignment(ParseTextAlignment(column.Align));
// 应用条件颜色规则 | Apply conditional color rules
if (column.ColorRules != null && !string.IsNullOrEmpty(cellValue))
{
foreach (var rule in column.ColorRules)
{
if (string.Equals(cellValue, rule.Key, StringComparison.OrdinalIgnoreCase)
|| cellValue.Contains(rule.Key, StringComparison.OrdinalIgnoreCase))
{
var ruleColor = ParseColor(rule.Value);
if (ruleColor != null)
{
cellParagraph.SetFontColor(ruleColor);
cellParagraph.SetBold();
}
break;
}
}
}
dataCell.Add(cellParagraph);
dataCell.SetBackgroundColor(rowBgColor);
dataCell.SetBorder(new SolidBorder(ColorConstants.LIGHT_GRAY, 0.5f));
@@ -605,6 +725,16 @@ namespace XP.ReportEngine.Services
}
}
// 应用样式中的边距 | Apply margins from style
var style = element.ResolvedStyle;
if (style != null)
{
if (style.MarginTop > 0)
table.SetMarginTop(style.MarginTop * MmToPoints);
if (style.MarginBottom > 0)
table.SetMarginBottom(style.MarginBottom * MmToPoints);
}
document.Add(table);
}
@@ -647,6 +777,555 @@ namespace XP.ReportEngine.Services
document.Add(lineSeparator);
}
/// <summary>
/// 渲染空白间距元素 | Render spacer element
/// 通过 Size[1](高度,mm)控制垂直空白大小
/// Controls vertical whitespace via Size[1] (height in mm)
/// </summary>
private void RenderSpacerElement(Document document, LayoutElement element)
{
// 从 Size[1] 获取高度,默认 10mm | Get height from Size[1], default 10mm
float heightMm = 10f;
if (element.Source?.Size is { Length: >= 2 })
{
heightMm = element.Source.Size[1];
}
// 使用空段落撑出指定高度的空白 | Use empty paragraph to create specified height whitespace
var spacer = new Paragraph("")
.SetFontSize(1)
.SetMarginTop(0)
.SetMarginBottom(heightMm * MmToPoints);
document.Add(spacer);
}
/// <summary>
/// 渲染强制分页元素 | Render forced page break element
/// </summary>
private void RenderPageBreakElement(Document document)
{
document.Add(new AreaBreak(AreaBreakType.NEXT_PAGE));
}
/// <summary>
/// 渲染行容器元素(水平布局)| Render row container element (horizontal layout)
/// 使用无边框表格实现子元素的水平排列,支持 left/center/right 对齐
/// Uses borderless table to arrange child elements horizontally, supports left/center/right alignment
/// </summary>
private void RenderRowElement(Document document, LayoutElement element)
{
var children = element.Source?.Children;
if (children == null || children.Count == 0) return;
// 创建表格,支持自定义列宽比例 | Create table with custom column width ratios
var columnCount = children.Count;
Table table;
if (element.Source.Widths != null && element.Source.Widths.Length == columnCount)
{
// 使用指定的列宽比例 | Use specified column width ratios
var totalRatio = 0f;
foreach (var w in element.Source.Widths) totalRatio += w;
var columnWidths = new float[columnCount];
for (int i = 0; i < columnCount; i++)
{
columnWidths[i] = element.Source.Widths[i] / totalRatio;
}
table = new Table(UnitValue.CreatePercentArray(columnWidths));
}
else
{
// 均分列宽 | Equal column widths
table = new Table(columnCount);
}
table.UseAllAvailableWidth();
table.SetBorder(iText.Layout.Borders.Border.NO_BORDER);
var font = GetFontForCurrentLanguage();
foreach (var child in children)
{
var cell = new Cell();
cell.SetBorder(iText.Layout.Borders.Border.NO_BORDER);
cell.SetPadding(0);
// 确定子元素对齐方式 | Determine child element alignment
var align = child.Align?.ToLowerInvariant() ?? "left";
cell.SetTextAlignment(ParseTextAlignment(align));
var childType = child.Type?.ToLowerInvariant();
if (childType == "column")
{
// 渲染列容器子元素(垂直堆叠多个元素在同一单元格内)
// Render column container child (stack multiple elements vertically in same cell)
if (child.Children != null)
{
foreach (var subChild in child.Children)
{
RenderRowChildIntoCell(cell, subChild, align, font);
}
}
}
else
{
RenderRowChildIntoCell(cell, child, align, font);
}
table.AddCell(cell);
}
document.Add(table);
}
/// <summary>
/// 将单个子元素渲染到单元格中 | Render a single child element into a cell
/// </summary>
private void RenderRowChildIntoCell(Cell cell, TemplateElement child, string align, PdfFont font)
{
var childType = child.Type?.ToLowerInvariant();
// 子元素可以覆盖父级对齐 | Child can override parent alignment
var childAlign = child.Align?.ToLowerInvariant() ?? align;
if (childType == "image")
{
// 渲染图像子元素 | Render image child element
var imageData = child.ImageData;
if (imageData != null)
{
try
{
byte[] imageBytes = GetImageBytes(imageData);
if (imageBytes != null && imageBytes.Length > 0)
{
var iTextImageData = iText.IO.Image.ImageDataFactory.Create(imageBytes);
var image = new Image(iTextImageData);
// 应用尺寸 | Apply size
float targetWidthPt = child.Size != null && child.Size.Length > 0 ? child.Size[0] * MmToPoints : 0;
float targetHeightPt = child.Size != null && child.Size.Length > 1 ? child.Size[1] * MmToPoints : 0;
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);
}
// 设置图像水平对齐 | Set image horizontal alignment
if (childAlign == "right")
image.SetHorizontalAlignment(HorizontalAlignment.RIGHT);
else if (childAlign == "center")
image.SetHorizontalAlignment(HorizontalAlignment.CENTER);
else
image.SetHorizontalAlignment(HorizontalAlignment.LEFT);
cell.Add(image);
}
}
catch (Exception ex)
{
_logger.Warn("Row 子元素图像渲染失败 | Row child image rendering failed: {Message}", ex.Message);
}
}
}
else if (childType == "text")
{
// 渲染文本子元素 | Render text child element
var content = child.Content ?? string.Empty;
var style = ResolveStyleFromTemplate(child.Style);
var paragraph = new Paragraph(content);
paragraph.SetFont(font);
paragraph.SetFontSize(style.Size);
if (style.Bold) paragraph.SetBold();
if (style.Italic) paragraph.SetItalic();
var color = ParseColor(style.Color);
if (color != null) paragraph.SetFontColor(color);
// 应用条件颜色规则 | Apply conditional color rules
if (child.ColorRules != null && !string.IsNullOrEmpty(content))
{
foreach (var rule in child.ColorRules)
{
if (content.Contains(rule.Key, StringComparison.OrdinalIgnoreCase))
{
var ruleColor = ParseColor(rule.Value);
if (ruleColor != null)
{
paragraph.SetFontColor(ruleColor);
paragraph.SetBold();
}
break;
}
}
}
paragraph.SetTextAlignment(ParseTextAlignment(childAlign));
paragraph.SetMargin(0);
cell.Add(paragraph);
}
}
/// <summary>
/// 从当前模板中解析样式定义 | Resolve style definition from current template
/// </summary>
private StyleDefinition ResolveStyleFromTemplate(string styleName)
{
if (string.IsNullOrWhiteSpace(styleName))
return new StyleDefinition();
if (_currentTemplate?.Styles != null
&& _currentTemplate.Styles.TryGetValue(styleName, out var style))
{
return style;
}
return new StyleDefinition();
}
#endregion
#region 7.85 | Header/Footer rendering (event-driven)
/// <summary>
/// 页眉页脚事件处理器 | Header/Footer event handler
/// 在 END_PAGE 事件中绘制页眉页脚,使用 PdfFormXObject 占位符实现总页数回填
/// Draws header/footer in END_PAGE event, uses PdfFormXObject placeholder for total page count
/// </summary>
private class HeaderFooterEventHandler : iText.Kernel.Events.IEventHandler
{
private readonly ITextPdfRenderer _renderer;
private readonly ReportTemplate _template;
private readonly List<LayoutPage> _pages;
private readonly ILoggerService _logger;
private readonly HeaderFooterSettings _headerConfig;
private readonly HeaderFooterSettings _footerConfig;
private readonly MarginSettings _margins;
private readonly PdfFont _font;
// 首页数量 | Homepage count
private readonly int _homepageCount;
// 总页数占位符模板(用于回填)| Total page count placeholder template (for backfill)
private readonly iText.Kernel.Pdf.Xobject.PdfFormXObject _totalPagePlaceholder;
private readonly List<(iText.Kernel.Pdf.Canvas.PdfCanvas canvas, float x, float y)> _totalPagePositions = new();
// 当前页面索引(从 0 开始)| Current page index (0-based)
private int _currentPageIndex = -1;
public HeaderFooterEventHandler(
ITextPdfRenderer renderer,
ReportTemplate template,
List<LayoutPage> pages,
ILoggerService logger)
{
_renderer = renderer;
_template = template;
_pages = pages;
_logger = logger;
_headerConfig = template?.Document?.Header;
_footerConfig = template?.Document?.Footer;
_margins = template?.Document?.Margins ?? new MarginSettings();
_font = renderer.GetFontForCurrentLanguage();
// 计算首页数量 | Calculate homepage count
_homepageCount = 0;
if (pages != null)
{
for (int i = 0; i < pages.Count; i++)
{
if (string.Equals(pages[i].PageType, "homepage", StringComparison.OrdinalIgnoreCase))
_homepageCount++;
else
break;
}
}
// 创建总页数占位符(固定宽度区域)| Create total page count placeholder (fixed width area)
_totalPagePlaceholder = new iText.Kernel.Pdf.Xobject.PdfFormXObject(new Rectangle(0, 0, 30, 12));
}
public void HandleEvent(iText.Kernel.Events.Event @event)
{
if (@event is not iText.Kernel.Events.PdfDocumentEvent docEvent) return;
_currentPageIndex++;
var pdfDoc = docEvent.GetDocument();
var pdfPage = docEvent.GetPage();
var pageSize = pdfPage.GetPageSize();
// 跳过首页 | Skip homepage
if (_currentPageIndex < _homepageCount) return;
int currentContentPageNum = _currentPageIndex - _homepageCount + 1;
try
{
var canvas = new iText.Kernel.Pdf.Canvas.PdfCanvas(pdfPage.NewContentStreamBefore(), pdfPage.GetResources(), pdfDoc);
// 绘制页眉 | Draw header
if (_headerConfig != null && _headerConfig.Enabled)
{
DrawHeader(canvas, pageSize);
}
// 绘制页脚 | Draw footer
if (_footerConfig != null && _footerConfig.Enabled)
{
DrawFooter(canvas, pageSize, pdfDoc, currentContentPageNum);
}
canvas.Release();
}
catch (Exception ex)
{
_logger.Warn("页眉页脚绘制异常 | Header/footer drawing exception: {Message}", ex.Message);
}
}
/// <summary>
/// 绘制页眉 | Draw header
/// </summary>
private void DrawHeader(iText.Kernel.Pdf.Canvas.PdfCanvas canvas, Rectangle pageSize)
{
float leftX = _margins.Left * MmToPoints;
float rightX = pageSize.GetWidth() - _margins.Right * MmToPoints;
float topY = pageSize.GetHeight() - (_margins.Top * MmToPoints * 0.3f);
float fontSize = _headerConfig.FontSize > 0 ? _headerConfig.FontSize : 8f;
var fontColor = _renderer.ParseColor(_headerConfig.Color ?? "#666666");
// 绘制左侧文本行 | Draw left-side text lines
if (_headerConfig.Left != null && _headerConfig.Left.Count > 0)
{
float lineY = topY;
float lineSpacing = (fontSize + 2f) * 1.2f;
foreach (var line in _headerConfig.Left)
{
if (string.IsNullOrEmpty(line)) continue;
canvas.BeginText()
.SetFontAndSize(_font, fontSize)
.MoveText(leftX, lineY)
.ShowText(line)
.EndText();
lineY -= lineSpacing;
}
}
// 绘制右上角 Logo | Draw right-side logo
if (!string.IsNullOrEmpty(_headerConfig.RightImageKey))
{
try
{
ImageData logoImageData = _renderer.FindBoundImage(_template, _headerConfig.RightImageKey);
if (logoImageData != null)
{
byte[] imageBytes = _renderer.GetImageBytes(logoImageData);
if (imageBytes != null && imageBytes.Length > 0)
{
var iTextImageData = iText.IO.Image.ImageDataFactory.Create(imageBytes);
float logoHeight = 10f * MmToPoints;
float logoWidth = logoHeight * (iTextImageData.GetWidth() / iTextImageData.GetHeight());
float logoX = rightX - logoWidth;
float logoY = topY - logoHeight + fontSize;
canvas.AddImageFittedIntoRectangle(iTextImageData,
new Rectangle(logoX, logoY, logoWidth, logoHeight), false);
}
}
}
catch (Exception ex)
{
_logger.Warn("页眉 Logo 渲染失败 | Header logo rendering failed: {Message}", ex.Message);
}
}
// 绘制页眉分隔线 | Draw header separator line
if (_headerConfig.ShowLine)
{
float lineY = topY - (_headerConfig.Left?.Count ?? 1) * ((fontSize + 2f) * 1.2f) - 3f;
canvas.SetStrokeColor(fontColor)
.SetLineWidth(0.5f)
.MoveTo(leftX, lineY)
.LineTo(rightX, lineY)
.Stroke();
}
}
/// <summary>
/// 绘制页脚 | Draw footer
/// </summary>
private void DrawFooter(iText.Kernel.Pdf.Canvas.PdfCanvas canvas, Rectangle pageSize, PdfDocument pdfDoc, int currentPage)
{
float leftX = _margins.Left * MmToPoints;
float rightX = pageSize.GetWidth() - _margins.Right * MmToPoints;
float bottomY = _margins.Bottom * MmToPoints * 0.5f;
float fontSize = _footerConfig.FontSize > 0 ? _footerConfig.FontSize : 8f;
var fontColor = _renderer.ParseColor(_footerConfig.Color ?? "#666666");
// 绘制页脚分隔线 | Draw footer separator line
if (_footerConfig.ShowLine)
{
float lineY = bottomY + fontSize + 5f;
canvas.SetStrokeColor(fontColor)
.SetLineWidth(0.5f)
.MoveTo(leftX, lineY)
.LineTo(rightX, lineY)
.Stroke();
}
// 绘制左侧文本(公司名称)| Draw left-side text (company name)
if (_footerConfig.Left != null && _footerConfig.Left.Count > 0)
{
var leftText = _footerConfig.Left[0] ?? string.Empty;
canvas.BeginText()
.SetFontAndSize(_font, fontSize)
.MoveText(leftX, bottomY)
.ShowText(leftText)
.EndText();
}
// 绘制右侧页码(当前页 / 总页数占位符)| Draw right-side page number (current / total placeholder)
if (_footerConfig.Right != null && _footerConfig.Right.Count > 0)
{
var pageNumTemplate = _footerConfig.Right[0] ?? string.Empty;
// 先写当前页码部分 | Write current page number part
var currentPageText = pageNumTemplate.Replace("{currentPage}", currentPage.ToString()).Replace("{totalPages}", "");
// 分离出 totalPages 前后的文本 | Separate text around totalPages
var parts = pageNumTemplate.Split(new[] { "{totalPages}" }, StringSplitOptions.None);
if (parts.Length == 2)
{
// 有总页数占位符:写前缀 + 当前页码 + 占位符 XObject + 后缀
var prefix = parts[0].Replace("{currentPage}", currentPage.ToString());
var suffix = parts[1];
float prefixWidth = _font.GetWidth(prefix, fontSize);
float suffixWidth = _font.GetWidth(suffix, fontSize);
float placeholderWidth = 15f; // 预留总页数宽度 | Reserve width for total pages
float totalWidth = prefixWidth + placeholderWidth + suffixWidth;
float startX = rightX - totalWidth;
// 写前缀文本 | Write prefix text
canvas.BeginText()
.SetFontAndSize(_font, fontSize)
.MoveText(startX, bottomY)
.ShowText(prefix)
.EndText();
// 添加总页数占位符 XObject | Add total page count placeholder XObject
float placeholderX = startX + prefixWidth;
canvas.AddXObjectAt(_totalPagePlaceholder, placeholderX, bottomY - 2f);
_totalPagePositions.Add((canvas, placeholderX, bottomY));
// 写后缀文本 | Write suffix text
if (!string.IsNullOrEmpty(suffix))
{
canvas.BeginText()
.SetFontAndSize(_font, fontSize)
.MoveText(placeholderX + placeholderWidth, bottomY)
.ShowText(suffix)
.EndText();
}
}
else
{
// 无总页数占位符,直接写文本 | No total pages placeholder, write text directly
var text = pageNumTemplate.Replace("{currentPage}", currentPage.ToString());
float textWidth = _font.GetWidth(text, fontSize);
float textX = rightX - textWidth;
canvas.BeginText()
.SetFontAndSize(_font, fontSize)
.MoveText(textX, bottomY)
.ShowText(text)
.EndText();
}
}
}
/// <summary>
/// 文档关闭前回填总页数到所有占位符 | Write total page count to all placeholders before document close
/// </summary>
public void WriteTotal(PdfDocument pdfDoc)
{
int totalContentPages = pdfDoc.GetNumberOfPages() - _homepageCount;
var totalText = totalContentPages.ToString();
// 在占位符 XObject 上绘制总页数 | Draw total page count on placeholder XObject
var canvas = new iText.Kernel.Pdf.Canvas.PdfCanvas(_totalPagePlaceholder, pdfDoc);
canvas.BeginText()
.SetFontAndSize(_font, _footerConfig?.FontSize > 0 ? _footerConfig.FontSize : 8f)
.MoveText(0, 2f)
.ShowText(totalText)
.EndText();
canvas.Release();
}
}
/// <summary>
/// 从模板中查找已绑定的图像数据 | Find bound image data from template
/// </summary>
internal ImageData FindBoundImage(ReportTemplate template, string dataKey)
{
if (template?.Pages == null || string.IsNullOrEmpty(dataKey)) return null;
foreach (var page in template.Pages)
{
if (page.Elements == null) continue;
foreach (var element in page.Elements)
{
var found = FindImageInElement(element, dataKey);
if (found != null) return found;
}
}
return null;
}
/// <summary>
/// 递归搜索元素及其子元素中的图像数据 | Recursively search for image data in element and its children
/// </summary>
private ImageData FindImageInElement(TemplateElement element, string dataKey)
{
if (element == null) return null;
// 检查当前元素 | Check current element
if (string.Equals(element.Type, "image", StringComparison.OrdinalIgnoreCase)
&& string.Equals(element.DataKey, dataKey, StringComparison.OrdinalIgnoreCase)
&& element.ImageData != null)
{
return element.ImageData;
}
// 递归搜索子元素 | Recursively search children
if (element.Children != null)
{
foreach (var child in element.Children)
{
var found = FindImageInElement(child, dataKey);
if (found != null) return found;
}
}
return null;
}
#endregion
#region 7.9 PDF | PDF save to file
@@ -2,29 +2,71 @@
"document": {
"pageSize": "A4",
"orientation": "Portrait",
"margins": { "top": 20, "bottom": 20, "left": 20, "right": 20 }
"margins": { "top": 40, "bottom": 20, "left": 20, "right": 20 },
"header": {
"enabled": true,
"left": [
"${loc:Report_Title}",
"${loc:Report_Id}${metadata.reportId} | ${loc:Report_Date}${formatDate(metadata.inspectionDate)} | ${loc:Report_Sample}${metadata.sampleName}"
],
"rightImageKey": "companyLogo",
"fontSize": 7,
"color": "#666666",
"showLine": true
},
"footer": {
"enabled": true,
"left": ["${CompanyName}"],
"right": ["{currentPage} / {totalPages}"],
"fontSize": 8,
"color": "#666666",
"showLine": true
}
},
"pages": [
{
"type": "homepage",
"elements": [
{ "type": "image", "dataKey": "companyLogo", "position": [0, 0], "size": [30, 15], "positioning": "absolute" },
{ "type": "text", "content": "${CompanyName}", "style": "body", "position": [32, 3], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Title}", "style": "title", "position": [60, 40], "positioning": "absolute" },
{ "type": "divider", "position": [0, 60], "size": [170, 1], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Id}${metadata.reportId}", "style": "body", "position": [0, 75], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Date}${formatDate(metadata.inspectionDate)}", "style": "body", "position": [0, 85], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Sample}${metadata.sampleName}", "style": "body", "position": [0, 95], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Operator}${metadata.operatorName}", "style": "body", "position": [0, 105], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Description}${metadata.description}", "style": "body", "position": [0, 115], "positioning": "absolute" },
{ "type": "divider", "position": [0, 130], "size": [170, 1], "positioning": "absolute" },
{ "type": "text", "content": "${loc:Report_Summary}", "style": "heading", "position": [0, 140], "positioning": "absolute" },
{
"type": "table", "dataKey": "summaryTable", "position": [0, 150], "size": [170, 60], "positioning": "absolute",
"type": "row", "size": [170, 40], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "companyLogo", "size": [50, 30], "align": "left" },
{ "type": "text", "content": "${CompanyName}", "style": "companyName", "align": "left" }
]
},
{
"type": "column", "align": "right",
"children": [
{ "type": "image", "dataKey": "softwareLogo", "size": [15, 20], "align": "right" },
{ "type": "text", "content": "${SoftwareName}", "style": "companyName", "align": "right" }
]
}
]
},
{ "type": "spacer", "size": [170, 15], "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Title}", "style": "title", "positioning": "flow" },
{ "type": "spacer", "size": [170, 10], "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Id}${metadata.reportId}", "style": "homepageInfo", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Sample}${metadata.sampleName}", "style": "homepageInfo", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Description}${metadata.description}", "style": "homepageInfo", "positioning": "flow" },
{ "type": "image", "dataKey": "workpieceImage", "size": [160, 110], "border": true, "align": "center", "style": "imageDefault", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Operator}${metadata.operatorName}", "style": "homepageFooter", "positioning": "flow" },
{ "type": "text", "content": "${loc:Report_Date}${formatDate(metadata.inspectionDate)}", "style": "homepageFooter", "positioning": "flow" }
]
},
{
"type": "summary",
"elements": [
{ "type": "text", "content": "${loc:Report_Summary}", "style": "homepageHeading", "positioning": "flow" },
{
"type": "table", "dataKey": "summaryTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
"columns": [
{ "header": "${loc:Field_InspectionType}", "field": "inspectionType", "width": 50, "align": "left" },
{ "header": "${loc:Field_Result}", "field": "classification", "width": 30, "align": "center" },
{ "header": "${loc:Field_Status}", "field": "status", "width": 30, "align": "center" }
{ "header": "${loc:Field_Result}", "field": "classification", "width": 30, "align": "center", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } },
{ "header": "${loc:Field_Status}", "field": "status", "width": 30, "align": "center", "colorRules": { "合格": "#008000", "PASS": "#008000", "不合格": "#FF0000", "FAIL": "#FF0000" } }
]
}
]
@@ -33,35 +75,63 @@
"type": "metricData",
"elements": [
{ "type": "text", "content": "${loc:Page_MetricData}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_Type}${measurementType}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Field_Point1}${point1}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Field_Point2}${point2}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_Distance}${actualDistance} ${unit}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_Angle}${angle}°", "style": "body", "positioning": "flow" },
{ "type": "image", "dataKey": "lineMeasurementImage", "size": [150, 100], "border": true, "positioning": "flow" }
{
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "lineMeasurementImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Measurement_Type}${measurementType}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Field_Point1}${point1}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Field_Point2}${point2}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_Distance}${actualDistance} ${unit}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_Angle}${angle}°", "style": "body", "align": "left" }
]
}
]
}
]
},
{
"type": "bgaInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_BgaInspection}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_Count}${bgaCount}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_FillRate}${formatPercent(fillRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_TotalArea}${formatNumber(totalBgaArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_TotalVoidArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Bga_VoidLimit}${formatPercent(voidLimit)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "bodyBold", "positioning": "flow" },
{ "type": "image", "dataKey": "bgaInspectionImage", "size": [150, 100], "border": true, "positioning": "flow" },
{
"type": "table", "dataKey": "bgaBallsTable", "positioning": "flow", "size": [170, 0],
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "bgaInspectionImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Bga_Count}${bgaCount}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_FillRate}${formatPercent(fillRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Bga_TotalArea}${formatNumber(totalBgaArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Bga_TotalVoidArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Bga_VoidLimit}${formatPercent(voidLimit)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
},
{
"type": "table", "dataKey": "bgaBallsTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
"columns": [
{ "header": "${loc:Bga_BallIndex}", "field": "index", "width": 25, "align": "center" },
{ "header": "${loc:Table_VoidRate}", "field": "voidRate", "width": 40, "align": "center" },
{ "header": "${loc:Table_Area}", "field": "area", "width": 40, "align": "center" },
{ "header": "${loc:Table_Classification}", "field": "classification", "width": 35, "align": "center" }
{ "header": "${loc:Table_Classification}", "field": "classification", "width": 35, "align": "center", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
@@ -70,17 +140,31 @@
"type": "voidInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_VoidInspection}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_RoiArea}${formatNumber(roiArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_TotalArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_Limit}${formatPercent(voidLimit)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_Count}${voidCount}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Void_MaxArea}${formatNumber(maxVoidArea, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "bodyBold", "positioning": "flow" },
{ "type": "image", "dataKey": "voidInspectionImage", "size": [150, 100], "border": true, "positioning": "flow" },
{
"type": "table", "dataKey": "voidsTable", "positioning": "flow", "size": [170, 0],
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "voidInspectionImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Void_RoiArea}${formatNumber(roiArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_TotalArea}${formatNumber(totalVoidArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_Limit}${formatPercent(voidLimit)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_Count}${voidCount}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Void_MaxArea}${formatNumber(maxVoidArea, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
},
{
"type": "table", "dataKey": "voidsTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
"columns": [
{ "header": "${loc:Table_Index}", "field": "index", "width": 20, "align": "center" },
{ "header": "${loc:Table_Area}", "field": "area", "width": 35, "align": "center" },
@@ -95,23 +179,44 @@
"type": "viaFillInspection",
"elements": [
{ "type": "text", "content": "${loc:Page_ViaFillInspection}", "style": "heading", "positioning": "flow" },
{ "type": "divider", "size": [170, 1], "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_Rate}${formatPercent(fillRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_FullDistance}${formatNumber(fullDistance, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_FillDistance}${formatNumber(fillDistance, 2)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Fill_THTLimit}${formatPercent(thtLimit)}", "style": "body", "positioning": "flow" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "bodyBold", "positioning": "flow" },
{ "type": "image", "dataKey": "viaFillImage", "size": [150, 100], "border": true, "positioning": "flow" }
{
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
"children": [
{
"type": "column", "align": "left",
"children": [
{ "type": "image", "dataKey": "viaFillImage", "size": [110, 90], "border": true, "align": "left" }
]
},
{
"type": "column", "align": "left",
"children": [
{ "type": "text", "content": "${loc:Fill_Rate}${formatPercent(fillRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Measurement_VoidRate}${formatPercent(voidRate)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Fill_FullDistance}${formatNumber(fullDistance, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Fill_FillDistance}${formatNumber(fillDistance, 2)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Fill_THTLimit}${formatPercent(thtLimit)}", "style": "body", "align": "left" },
{ "type": "text", "content": "${loc:Table_Classification}${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
]
}
]
}
]
}
],
"styles": {
"title": { "font": "auto", "size": 24, "bold": true, "italic": false, "color": "#1a1a1a", "align": "center" },
"heading": { "font": "auto", "size": 16, "bold": true, "italic": false, "color": "#333333", "align": "left" },
"title": { "font": "auto", "size": 28, "bold": true, "italic": false, "color": "#1a1a1a", "align": "center" },
"companyName": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center" },
"homepageInfo": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "left", "paddingLeft": 10 },
"homepageFooter": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "center" },
"homepageHeading": { "font": "auto", "size": 14, "bold": true, "italic": false, "color": "#333333", "align": "left" },
"heading": { "font": "auto", "size": 16, "bold": true, "italic": false, "color": "#333333", "align": "left", "marginBottom": 3 },
"body": { "font": "auto", "size": 11, "bold": false, "italic": false, "color": "#333333", "align": "left" },
"bodyBold": { "font": "auto", "size": 11, "bold": true, "italic": false, "color": "#1a1a1a", "align": "left" },
"conclusion": { "font": "auto", "size": 12, "bold": true, "italic": false, "color": "#333333", "align": "left", "marginTop": 3 },
"imageDefault": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "center", "marginTop": 5, "marginBottom": 5 },
"tableHeader": { "font": "auto", "size": 10, "bold": true, "italic": false, "color": "#ffffff", "align": "center", "backgroundColor": "#4472C4" },
"tableDefault": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center", "marginTop": 5 },
"tableCell": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center" }
}
}
@@ -216,7 +216,7 @@ namespace XP.ReportEngine.ViewModels
var defaultFileName = System.IO.Path.GetFileName(defaultOutputPath);
var result = MessageBox.Show(
$"是否将报告输出到默认位置?\n\n{defaultOutputPath}",
$"是否将报告输出到默认位置?\r\n{defaultOutputPath}",
"输出位置确认",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
@@ -317,6 +317,50 @@ namespace XP.ReportEngine.ViewModels
context.Properties["CompanyName"] = _reportConfig.CompanyName;
}
// 注入软件名称到上下文属性 | Inject software name into context properties
if (!string.IsNullOrEmpty(_reportConfig.SoftwareName))
{
context.Properties["SoftwareName"] = _reportConfig.SoftwareName;
}
// 注入软件 Logo 图像数据 | Inject software logo image data
if (!string.IsNullOrEmpty(_reportConfig.SoftwareLogo))
{
var softwareLogoPath = System.IO.Path.IsPathRooted(_reportConfig.SoftwareLogo)
? _reportConfig.SoftwareLogo
: System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _reportConfig.SoftwareLogo);
if (File.Exists(softwareLogoPath))
{
context.Images["softwareLogo"] = new ImageData
{
SourceType = ImageSourceType.FilePath,
FilePath = softwareLogoPath
};
_logger.Info("软件 Logo 已加载:{Path} | Software logo loaded: {Path}", softwareLogoPath);
}
else
{
_logger.Warn("软件 Logo 文件不存在:{Path} | Software logo file not found: {Path}", softwareLogoPath);
}
}
// 注入工件整体图片 | Inject workpiece overview image
var workpieceImagePath = System.IO.Path.Combine(MockImageDirectory, "OverView.png");
if (File.Exists(workpieceImagePath))
{
context.Images["workpieceImage"] = new ImageData
{
SourceType = ImageSourceType.FilePath,
FilePath = workpieceImagePath
};
_logger.Info("工件整体图片已加载:{Path} | Workpiece image loaded: {Path}", workpieceImagePath);
}
else
{
_logger.Warn("工件整体图片不存在:{Path} | Workpiece image not found: {Path}", workpieceImagePath);
}
// 注入首页汇总表数据 | Inject homepage summary table data
context.Properties["summaryTable"] = CreateSummaryTableData(context);
@@ -500,6 +544,37 @@ namespace XP.ReportEngine.ViewModels
/// </summary>
private ProcessorOutput CreateBgaVoidRateOutput()
{
// 生成 64 组 BGA 球模拟数据 | Generate 64 BGA ball mock data
var bgaBalls = new List<Dictionary<string, object>>();
var random = new Random(42); // 固定种子确保可重复 | Fixed seed for reproducibility
var voidLimit = 0.05;
for (int i = 1; i <= 64; i++)
{
// 大部分合格,少数不合格 | Most pass, a few fail
double voidRate;
if (i == 12 || i == 27 || i == 41 || i == 58)
{
// 这几个球不合格 | These balls fail
voidRate = Math.Round(0.05 + random.NextDouble() * 0.03, 4);
}
else
{
voidRate = Math.Round(random.NextDouble() * 0.045, 4);
}
var area = Math.Round(190.0 + random.NextDouble() * 15.0, 1);
var classification = voidRate > voidLimit ? "Fail" : "Pass";
bgaBalls.Add(new Dictionary<string, object>
{
["Index"] = i,
["VoidRate"] = voidRate,
["Area"] = area,
["Classification"] = classification
});
}
return new ProcessorOutput
{
ProcessorType = "BgaVoidRateProcessor",
@@ -512,19 +587,7 @@ namespace XP.ReportEngine.ViewModels
["TotalVoidArea"] = 350.2,
["Classification"] = "Pass",
["VoidLimit"] = 0.05,
["BgaBalls"] = new List<Dictionary<string, object>>
{
new() { ["Index"] = 1, ["VoidRate"] = 0.012, ["Area"] = 195.3, ["Classification"] = "Pass" },
new() { ["Index"] = 2, ["VoidRate"] = 0.035, ["Area"] = 198.1, ["Classification"] = "Pass" },
new() { ["Index"] = 3, ["VoidRate"] = 0.008, ["Area"] = 192.7, ["Classification"] = "Pass" },
new() { ["Index"] = 4, ["VoidRate"] = 0.042, ["Area"] = 201.0, ["Classification"] = "Pass" },
new() { ["Index"] = 5, ["VoidRate"] = 0.015, ["Area"] = 196.5, ["Classification"] = "Pass" },
new() { ["Index"] = 6, ["VoidRate"] = 0.028, ["Area"] = 199.8, ["Classification"] = "Pass" },
new() { ["Index"] = 7, ["VoidRate"] = 0.005, ["Area"] = 194.2, ["Classification"] = "Pass" },
new() { ["Index"] = 8, ["VoidRate"] = 0.048, ["Area"] = 200.5, ["Classification"] = "Pass" },
new() { ["Index"] = 9, ["VoidRate"] = 0.019, ["Area"] = 197.0, ["Classification"] = "Pass" },
new() { ["Index"] = 10, ["VoidRate"] = 0.031, ["Area"] = 196.1, ["Classification"] = "Pass" }
}
["BgaBalls"] = bgaBalls
},
AnnotatedImage = LoadMockImage("BGA.png")
};
@@ -646,7 +709,7 @@ namespace XP.ReportEngine.ViewModels
};
var classification = string.IsNullOrEmpty(group.Classification) ? "N/A" : group.Classification;
var status = classification == "Pass" ? " 合格" : classification == "Fail" ? " 不合格" : "—";
var status = classification == "Pass" ? "[PASS] 合格" : classification == "Fail" ? "[FAIL] 不合格" : "—";
rows.Add(new Dictionary<string, object>
{