-
This commit is contained in:
@@ -57,12 +57,16 @@ public class ReportGeneratorService
|
||||
_logger.LogInformation("Elements pageIds: {PageIds}", string.Join(", ", aprt.Elements.Select(e => $"{e.Name ?? e.Id}->'{e.PageId ?? "NULL"}'")));
|
||||
|
||||
const float dpi = 300f;
|
||||
var totalPages = aprt.Pages.Count;
|
||||
|
||||
// Pre-render all pages to avoid closure issues in QuestPDF lambda
|
||||
var pageRenderData = new List<(float pageWidth, float pageHeight, string? bgColor, byte[] imageBytes)>();
|
||||
|
||||
foreach (var pageDefinition in aprt.Pages)
|
||||
for (int pageIndex = 0; pageIndex < aprt.Pages.Count; pageIndex++)
|
||||
{
|
||||
var pageDefinition = aprt.Pages[pageIndex];
|
||||
var currentPageNumber = pageIndex + 1;
|
||||
|
||||
// Determine page settings (use page override or template default)
|
||||
var pageSize = pageDefinition.PageSize ?? aprt.Meta.PageSize;
|
||||
var orientation = pageDefinition.Orientation ?? aprt.Meta.Orientation;
|
||||
@@ -76,16 +80,18 @@ public class ReportGeneratorService
|
||||
(string.IsNullOrEmpty(e.PageId) && pageDefinition.Id == aprt.Pages[0].Id)))
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation("Page '{PageName}' ({PageId}): {ElementCount} elements (of {TotalElements} total), Size: {Width}x{Height}mm",
|
||||
_logger.LogInformation("Page '{PageName}' ({PageId}): {ElementCount} elements (of {TotalElements} total), Size: {Width}x{Height}mm, Page {CurrentPage}/{TotalPages}",
|
||||
pageDefinition.Name, pageDefinition.Id, pageElements.Count, aprt.Elements.Count,
|
||||
pageWidth, pageHeight);
|
||||
pageWidth, pageHeight, currentPageNumber, totalPages);
|
||||
_logger.LogInformation(" Elements on this page: {Elements}",
|
||||
string.Join(", ", pageElements.Select(e => e.Name ?? e.Id)));
|
||||
|
||||
// Render full page to bitmap (element coordinates are relative to entire page)
|
||||
// Pass page context for special variables
|
||||
var pageContext = new PageContext { PageNumber = currentPageNumber, TotalPages = totalPages };
|
||||
var pageImageBytes = RenderContentToBitmap(
|
||||
pageElements, pageWidth, pageHeight, dataContext, resources, dpi,
|
||||
pageDefinition.BackgroundColor);
|
||||
pageDefinition.BackgroundColor, pageContext);
|
||||
|
||||
pageRenderData.Add((pageWidth, pageHeight, pageDefinition.BackgroundColor, pageImageBytes));
|
||||
}
|
||||
@@ -112,6 +118,15 @@ public class ReportGeneratorService
|
||||
return document.GeneratePdf();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for page-level special variables during rendering
|
||||
/// </summary>
|
||||
private class PageContext
|
||||
{
|
||||
public int PageNumber { get; set; }
|
||||
public int TotalPages { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Migrates legacy templates without pages to the new multi-page format.
|
||||
/// Creates a default page and assigns all orphan elements to it.
|
||||
@@ -141,7 +156,7 @@ public class ReportGeneratorService
|
||||
/// </summary>
|
||||
private byte[] RenderContentToBitmap(List<AprtElement> elements, float contentWidthMm, float contentHeightMm,
|
||||
Dictionary<string, object> dataContext, Dictionary<string, object> resources, float dpi,
|
||||
string? backgroundColor = null)
|
||||
string? backgroundColor = null, PageContext? pageContext = null)
|
||||
{
|
||||
// Use a SINGLE uniform scale for both axes - critical for correct element positioning
|
||||
// With FitArea(), the bitmap will fill the page maintaining proportions
|
||||
@@ -167,7 +182,7 @@ public class ReportGeneratorService
|
||||
// Render each element using consistent scale factors
|
||||
foreach (var element in elements.OrderBy(e => elements.IndexOf(e)))
|
||||
{
|
||||
RenderElementToCanvas(canvas, element, scaleX, scaleY, dataContext, resources);
|
||||
RenderElementToCanvas(canvas, element, scaleX, scaleY, dataContext, resources, pageContext);
|
||||
}
|
||||
|
||||
// Encode to PNG
|
||||
@@ -192,7 +207,7 @@ public class ReportGeneratorService
|
||||
/// 3. Draw the element centered on this point (offset by -width/2, -height/2)
|
||||
/// </summary>
|
||||
private void RenderElementToCanvas(SKCanvas canvas, AprtElement element, float scaleX, float scaleY,
|
||||
Dictionary<string, object> dataContext, Dictionary<string, object> resources)
|
||||
Dictionary<string, object> dataContext, Dictionary<string, object> resources, PageContext? pageContext = null)
|
||||
{
|
||||
// Convert mm to pixels
|
||||
var left = element.Position.X * scaleX;
|
||||
@@ -244,7 +259,7 @@ public class ReportGeneratorService
|
||||
break;
|
||||
|
||||
case "text":
|
||||
RenderTextToCanvas(canvas, element, drawX, drawY, width, height, scaleX, scaleY, dataContext);
|
||||
RenderTextToCanvas(canvas, element, drawX, drawY, width, height, scaleX, scaleY, dataContext, pageContext);
|
||||
break;
|
||||
|
||||
case "image":
|
||||
@@ -308,10 +323,10 @@ public class ReportGeneratorService
|
||||
}
|
||||
|
||||
private void RenderTextToCanvas(SKCanvas canvas, AprtElement element, float x, float y, float width, float height,
|
||||
float scaleX, float scaleY, Dictionary<string, object> dataContext)
|
||||
float scaleX, float scaleY, Dictionary<string, object> dataContext, PageContext? pageContext = null)
|
||||
{
|
||||
var style = element.Style;
|
||||
var text = ResolveContent(element.Content, dataContext);
|
||||
var text = ResolveContent(element.Content, dataContext, pageContext);
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
// Use average scale for font size to maintain proportions
|
||||
@@ -1478,27 +1493,27 @@ public class ReportGeneratorService
|
||||
return null;
|
||||
}
|
||||
|
||||
private string ResolveContent(AprtContent? content, Dictionary<string, object> dataContext)
|
||||
private string ResolveContent(AprtContent? content, Dictionary<string, object> dataContext, PageContext? pageContext = null)
|
||||
{
|
||||
if (content == null) return string.Empty;
|
||||
|
||||
var result = content.Type?.ToLower() switch
|
||||
{
|
||||
"static" => content.Value ?? string.Empty,
|
||||
"binding" => ResolveBindingWithFormat(content.Expression ?? content.Value ?? string.Empty, dataContext, content.Format),
|
||||
"expression" => ResolveExpression(content.Value ?? string.Empty, dataContext),
|
||||
"binding" => ResolveBindingWithFormat(content.Expression ?? content.Value ?? string.Empty, dataContext, content.Format, pageContext),
|
||||
"expression" => ResolveExpression(content.Value ?? string.Empty, dataContext, pageContext),
|
||||
_ => content.Value ?? string.Empty
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private string ResolveBindingWithFormat(string expression, Dictionary<string, object> dataContext, string? format)
|
||||
private string ResolveBindingWithFormat(string expression, Dictionary<string, object> dataContext, string? format, PageContext? pageContext = null)
|
||||
{
|
||||
return BindingRegex.Replace(expression, match =>
|
||||
{
|
||||
var path = match.Groups[1].Value.Trim();
|
||||
var value = ResolveBindingPath(path, dataContext);
|
||||
var value = ResolveBindingPath(path, dataContext, pageContext);
|
||||
|
||||
if (value != null && !string.IsNullOrEmpty(format))
|
||||
{
|
||||
@@ -1509,25 +1524,25 @@ public class ReportGeneratorService
|
||||
});
|
||||
}
|
||||
|
||||
private string ResolveBinding(string expression, Dictionary<string, object> dataContext)
|
||||
private string ResolveBinding(string expression, Dictionary<string, object> dataContext, PageContext? pageContext = null)
|
||||
{
|
||||
return BindingRegex.Replace(expression, match =>
|
||||
{
|
||||
var path = match.Groups[1].Value.Trim();
|
||||
var value = ResolveBindingPath(path, dataContext);
|
||||
var value = ResolveBindingPath(path, dataContext, pageContext);
|
||||
return value?.ToString() ?? string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
private object? ResolveBindingPath(string path, Dictionary<string, object> dataContext)
|
||||
private object? ResolveBindingPath(string path, Dictionary<string, object> dataContext, PageContext? pageContext = null)
|
||||
{
|
||||
// Handle special variables
|
||||
if (path.StartsWith("$"))
|
||||
{
|
||||
return path switch
|
||||
{
|
||||
"$pageNumber" => "{{PAGE}}",
|
||||
"$totalPages" => "{{TOTALPAGES}}",
|
||||
"$pageNumber" => pageContext?.PageNumber.ToString() ?? "1",
|
||||
"$totalPages" => pageContext?.TotalPages.ToString() ?? "1",
|
||||
"$date" => DateTime.Now.ToString("dd/MM/yyyy"),
|
||||
"$time" => DateTime.Now.ToString("HH:mm"),
|
||||
"$datetime" => DateTime.Now.ToString("dd/MM/yyyy HH:mm"),
|
||||
@@ -1566,9 +1581,9 @@ public class ReportGeneratorService
|
||||
return current;
|
||||
}
|
||||
|
||||
private string ResolveExpression(string expression, Dictionary<string, object> dataContext)
|
||||
private string ResolveExpression(string expression, Dictionary<string, object> dataContext, PageContext? pageContext = null)
|
||||
{
|
||||
return ResolveBinding(expression, dataContext);
|
||||
return ResolveBinding(expression, dataContext, pageContext);
|
||||
}
|
||||
|
||||
private object? GetPropertyValue(object obj, string propertyPath)
|
||||
|
||||
Reference in New Issue
Block a user