using System.Collections.Generic; using System.Data; using FastReport; using FastReport.Export.Pdf; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PdfSharpCore.Pdf; using PdfSharpCore.Pdf.IO; var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); var app = builder.Build(); var templateRoot = Path.Combine( AppContext.BaseDirectory, builder.Configuration["TemplateRoot"] ?? "wwwroot"); app.Logger.LogInformation("Template root: {TemplateRoot}", templateRoot); app.MapGet("/health", () => Results.Ok(new { status = "ok", time = DateTime.UtcNow })); // Render a single template + dataset to a PDF. // Body shape: // { // "template": "report_6", // resolves to wwwroot/reports/report_6.frx // "dataSourceName": "abnormal_meter_not_send_report", // "rows": [ { "field": "value", ... }, ... ], // "fileName": "report.pdf" // optional, response Content-Disposition // } app.MapPost("/render", async (HttpRequest req) => { using var sr = new StreamReader(req.Body); var bodyText = await sr.ReadToEndAsync(); var body = JsonConvert.DeserializeObject(bodyText) ?? throw new ArgumentException("Empty body"); var pdf = RenderSingle(templateRoot, body); return Results.File(pdf, "application/pdf", body.FileName ?? $"{body.Template}.pdf"); }); // Render multiple sections and merge them into a single PDF. // Body shape: { "fileName": "merged.pdf", "sections": [ , ... ] } app.MapPost("/render/multi", async (HttpRequest req) => { using var sr = new StreamReader(req.Body); var bodyText = await sr.ReadToEndAsync(); var body = JsonConvert.DeserializeObject(bodyText) ?? throw new ArgumentException("Empty body"); if (body.Sections == null || body.Sections.Count == 0) return Results.BadRequest(new { error = "sections must contain at least one entry" }); var streams = body.Sections.Select(s => new MemoryStream(RenderSingle(templateRoot, s))).ToList(); var merged = MergePdfs(streams); foreach (var s in streams) s.Dispose(); return Results.File(merged, "application/pdf", body.FileName ?? "merged.pdf"); }); app.Run(); return; // ---- helpers -------------------------------------------------------------- static byte[] RenderSingle(string templateRoot, RenderRequest req) { if (string.IsNullOrWhiteSpace(req.Template)) throw new ArgumentException("template is required"); var path = ResolveTemplatePath(templateRoot, req.Template); if (!File.Exists(path)) throw new FileNotFoundException($"Template not found: {req.Template}", path); using var report = new Report(); report.Load(path); if (req.Rows != null && req.Rows.Count > 0) { var dt = JArrayToDataTable(req.Rows, req.DataSourceName ?? "data"); report.RegisterData(dt, dt.TableName); report.GetDataSource(dt.TableName).Enabled = true; } else if (!string.IsNullOrEmpty(req.DataSourceName)) { // No rows but a name was given — register an empty table so the template // renders its bands without the "data source not found" error. var dt = new DataTable(req.DataSourceName); report.RegisterData(dt, dt.TableName); } report.Prepare(); using var ms = new MemoryStream(); var pdfExport = new PDFExport(); report.Export(pdfExport, ms); return ms.ToArray(); } // Resolves "report_6" → wwwroot/reports/report_6.frx, with "documents/uuid" // supported for the empty-report templates that live under wwwroot/documents. static string ResolveTemplatePath(string root, string name) { if (name.EndsWith(".frx", StringComparison.OrdinalIgnoreCase)) { var direct = Path.Combine(root, name); if (File.Exists(direct)) return direct; } var withExt = name.EndsWith(".frx") ? name : name + ".frx"; var inReports = Path.Combine(root, "reports", withExt); if (File.Exists(inReports)) return inReports; var inDocuments = Path.Combine(root, "documents", withExt); if (File.Exists(inDocuments)) return inDocuments; return inReports; // let RenderSingle throw a clean FileNotFoundException } // Build a DataTable whose columns are the union of keys present across the // JArray rows. FastReport binds .frx columns to DataTable columns by name. static DataTable JArrayToDataTable(JArray rows, string tableName) { var dt = new DataTable(tableName); foreach (var token in rows) { if (token is not JObject obj) continue; foreach (var prop in obj.Properties()) { if (dt.Columns.Contains(prop.Name)) continue; dt.Columns.Add(prop.Name, InferColumnType(prop.Value)); } } foreach (var token in rows) { if (token is not JObject obj) continue; var row = dt.NewRow(); foreach (DataColumn col in dt.Columns) { var jt = obj[col.ColumnName]; row[col.ColumnName] = JTokenToCell(jt, col.DataType); } dt.Rows.Add(row); } return dt; } static Type InferColumnType(JToken? value) => value?.Type switch { JTokenType.Integer => typeof(long), JTokenType.Float => typeof(decimal), JTokenType.Boolean => typeof(bool), JTokenType.Date => typeof(DateTime), _ => typeof(string), }; static object JTokenToCell(JToken? jt, Type targetType) { if (jt == null || jt.Type == JTokenType.Null || jt.Type == JTokenType.Undefined) return DBNull.Value; try { if (targetType == typeof(string)) return jt.ToString(); return jt.ToObject(targetType) ?? (object)DBNull.Value; } catch { return jt.ToString(); } } static byte[] MergePdfs(IEnumerable sources) { using var output = new PdfDocument(); foreach (var src in sources) { src.Position = 0; using var input = PdfReader.Open(src, PdfDocumentOpenMode.Import); for (var i = 0; i < input.PageCount; i++) output.AddPage(input.Pages[i]); } using var ms = new MemoryStream(); output.Save(ms, false); return ms.ToArray(); } // ---- DTOs ----------------------------------------------------------------- public class RenderRequest { public string Template { get; set; } = ""; public string? DataSourceName { get; set; } public JArray? Rows { get; set; } public string? FileName { get; set; } } public class MultiRenderRequest { public string? FileName { get; set; } public List? Sections { get; set; } }