diff --git a/roles/lib/files/FWO.Report/FWO.Report.csproj b/roles/lib/files/FWO.Report/FWO.Report.csproj index 07862d5fb..15886d15b 100644 --- a/roles/lib/files/FWO.Report/FWO.Report.csproj +++ b/roles/lib/files/FWO.Report/FWO.Report.csproj @@ -7,7 +7,7 @@ - + diff --git a/roles/lib/files/FWO.Report/PaperFormat.cs b/roles/lib/files/FWO.Report/PaperFormat.cs new file mode 100644 index 000000000..75d86d0d7 --- /dev/null +++ b/roles/lib/files/FWO.Report/PaperFormat.cs @@ -0,0 +1,18 @@ +namespace FWO.Report +{ + public enum PaperFormat + { + A0, + A1, + A2, + A3, + A4, + A5, + A6, + Letter, + Legal, + Tabloid, + Ledger, + Custom + } +} diff --git a/roles/lib/files/FWO.Report/ReportBase.cs b/roles/lib/files/FWO.Report/ReportBase.cs index 95f02068b..4672a4f4f 100644 --- a/roles/lib/files/FWO.Report/ReportBase.cs +++ b/roles/lib/files/FWO.Report/ReportBase.cs @@ -4,14 +4,18 @@ using FWO.Report.Filter; using FWO.Config.Api; using System.Text; -using WkHtmlToPdfDotNet; +using PuppeteerSharp.Media; +using PuppeteerSharp; +using System.Reflection; +using System.IO; +using PuppeteerSharp.BrowserData; namespace FWO.Report { public enum RsbTab { - all = 10, - report = 20, + all = 10, + report = 20, rule = 30, usedObj = 40, @@ -21,8 +25,8 @@ public enum RsbTab public enum ObjCategory { all = 0, - nobj = 1, - nsrv = 2, + nobj = 1, + nsrv = 2, user = 3 } @@ -42,7 +46,7 @@ public enum OutputLocation public abstract class ReportBase { - protected StringBuilder HtmlTemplate = new ($@" + protected StringBuilder HtmlTemplate = new($@" @@ -86,11 +90,13 @@ public abstract class ReportBase protected UserConfig userConfig; public ReportType ReportType; public ReportData ReportData = new(); + public int CustomWidth = 0; + public int CustomHeight = 0; protected string htmlExport = ""; - // Pdf converter - protected static readonly SynchronizedConverter converter = new (new PdfTools()); + private const string ChromeBinPathLinux = "/usr/local/bin"; + public bool GotObjectsInReport { get; protected set; } = false; @@ -160,14 +166,14 @@ protected string GenerateHtmlFrameBase(string title, string filter, DateTime dat HtmlTemplate = HtmlTemplate.Replace("##Filter##", userConfig.GetText("filter") + ": " + filter); HtmlTemplate = HtmlTemplate.Replace("##GeneratedOn##", userConfig.GetText("generated_on")); HtmlTemplate = HtmlTemplate.Replace("##Date##", date.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssK")); - if(ReportType.IsChangeReport()) + if (ReportType.IsChangeReport()) { string timeRange = $"{userConfig.GetText("change_time")}: " + $"{userConfig.GetText("from")}: {ToUtcString(Query.QueryVariables["start"]?.ToString())}, " + $"{userConfig.GetText("until")}: {ToUtcString(Query.QueryVariables["stop"]?.ToString())}"; HtmlTemplate = HtmlTemplate.Replace("##Date-of-Config##: ##GeneratedFor##", timeRange); } - else if(ReportType.IsRuleReport() || ReportType == ReportType.Statistics) + else if (ReportType.IsRuleReport() || ReportType == ReportType.Statistics) { HtmlTemplate = HtmlTemplate.Replace("##Date-of-Config##", userConfig.GetText("date_of_config")); HtmlTemplate = HtmlTemplate.Replace("##GeneratedFor##", ToUtcString(Query.ReportTimeString)); @@ -186,7 +192,7 @@ protected string GenerateHtmlFrameBase(string title, string filter, DateTime dat HtmlTemplate = HtmlTemplate.Replace("

##OwnerFilters##

", ""); } - if(deviceFilter != null) + if (deviceFilter != null) { HtmlTemplate = HtmlTemplate.Replace("##OtherFilters##", userConfig.GetText("devices") + ": " + deviceFilter); } @@ -206,58 +212,96 @@ public static string ToUtcString(string? timestring) { return timestring != null ? DateTime.Parse(timestring).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssK") : ""; } - catch(Exception) + catch (Exception) { return timestring ?? ""; } } - public virtual byte[] ToPdf(PaperKind paperKind, int width = -1, int height = -1) + private async Task CreatePDFViaPuppeteer(string html, PaperFormat format) { - // HTML - if (string.IsNullOrEmpty(htmlExport)) + OperatingSystem? os = Environment.OSVersion; + + string path = ""; + BrowserFetcher? browserFetcher; + + switch (os.Platform) { - htmlExport = ExportToHtml(); + case PlatformID.Win32NT: + browserFetcher = new(); + break; + case PlatformID.Unix: + path = ChromeBinPathLinux; + browserFetcher = new(new BrowserFetcherOptions { Path = path, Platform = Platform.Linux, Browser = SupportedBrowser.Chrome }); + break; + default: + return default; } - GlobalSettings globalSettings = new () + InstalledBrowser? brw = await browserFetcher.DownloadAsync(BrowserTag.Stable); + + using IBrowser? browser = await Puppeteer.LaunchAsync(new LaunchOptions { - ColorMode = ColorMode.Color, - Orientation = Orientation.Landscape, - }; + ExecutablePath = brw.GetExecutablePath(), + Headless = true, + Args = ["--no-sandbox"] + }); - if (paperKind == PaperKind.Custom) + try { - if (width > 0 && height > 0) - { - globalSettings.PaperSize = new PechkinPaperSize(width + "mm", height + "mm"); - } - else - { - throw new Exception("Custom paper size: width or height <= 0"); - } + using IPage page = await browser.NewPageAsync(); + await page.SetContentAsync(html); + + PuppeteerSharp.Media.PaperFormat? pupformat = GetPuppeteerPaperFormat(format) ?? throw new Exception(); + + PdfOptions pdfOptions = new() { DisplayHeaderFooter = true, Landscape = true, PrintBackground = true, Format = pupformat, MarginOptions = new MarginOptions { Top = "1cm", Bottom = "1cm", Left = "1cm", Right = "1cm" } }; + byte[] pdfData = await page.PdfDataAsync(pdfOptions); + + return Convert.ToBase64String(pdfData); } - else + catch (Exception) { - globalSettings.PaperSize = paperKind; + throw new Exception("This paper kind is currently not supported. Please choose another one or \"Custom\" for a custom size."); } - - HtmlToPdfDocument doc = new () + finally { - GlobalSettings = globalSettings, - Objects = - { - new ObjectSettings() - { - PagesCount = true, - HtmlContent = htmlExport, - WebSettings = { DefaultEncoding = "utf-8" }, - HeaderSettings = { FontSize = 9, Right = "Page [page] of [toPage]", Line = true, Spacing = 2.812 } - } - } - }; + await browser.CloseAsync(); + } + } + + private PuppeteerSharp.Media.PaperFormat? GetPuppeteerPaperFormat(PaperFormat format) + { + if (format == PaperFormat.Custom) + return new PuppeteerSharp.Media.PaperFormat(CustomWidth, CustomHeight); + + PropertyInfo[] propertyInfos = typeof(PuppeteerSharp.Media.PaperFormat).GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic); + + PropertyInfo? prop = propertyInfos.SingleOrDefault(_ => _.Name == format.ToString()); + + if (prop == null) + return default; + + PuppeteerSharp.Media.PaperFormat? propFormat = (PuppeteerSharp.Media.PaperFormat)prop.GetValue(null); + + if (propFormat is null) + return default; - return converter.Convert(doc); + return propFormat; + } + + public virtual async Task ToPdf(string html, PaperFormat format) + { + return await CreatePDFViaPuppeteer(html, format); + } + + public virtual async Task ToPdf(string html) + { + return await CreatePDFViaPuppeteer(html, PaperFormat.A4); + } + + public virtual async Task ToPdf(PaperFormat format) + { + return await CreatePDFViaPuppeteer(htmlExport, format); } public static string GetIconClass(ObjCategory? objCategory, string? objType) diff --git a/roles/middleware/files/FWO.Middleware.Server/FWO.Middleware.Server.csproj b/roles/middleware/files/FWO.Middleware.Server/FWO.Middleware.Server.csproj index 6e2c4a531..6d1ea1c91 100644 --- a/roles/middleware/files/FWO.Middleware.Server/FWO.Middleware.Server.csproj +++ b/roles/middleware/files/FWO.Middleware.Server/FWO.Middleware.Server.csproj @@ -10,6 +10,7 @@ + diff --git a/roles/middleware/files/FWO.Middleware.Server/ImportChangeNotifier.cs b/roles/middleware/files/FWO.Middleware.Server/ImportChangeNotifier.cs index 616349517..85a28a31b 100644 --- a/roles/middleware/files/FWO.Middleware.Server/ImportChangeNotifier.cs +++ b/roles/middleware/files/FWO.Middleware.Server/ImportChangeNotifier.cs @@ -11,7 +11,7 @@ using Newtonsoft.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using WkHtmlToPdfDotNet; +using PuppeteerSharp.Media; namespace FWO.Middleware.Server { @@ -175,11 +175,14 @@ private async Task SendEmail() EmailConnection emailConnection = new(globalConfig.EmailServerAddress, globalConfig.EmailPort, globalConfig.EmailTls, globalConfig.EmailUser, decryptedSecret, globalConfig.EmailSenderAddress); MailKitMailer mailer = new(emailConnection); - await mailer.SendAsync(PrepareEmail(), emailConnection, new CancellationToken(), + + MailData? mail = await PrepareEmail(); + + await mailer.SendAsync(mail, emailConnection, new CancellationToken(), globalConfig.ImpChangeNotifyType == (int)ImpChangeNotificationType.HtmlInBody); } - private MailData PrepareEmail() + private async Task PrepareEmail() { string subject = globalConfig.ImpChangeNotifySubject; string body = CreateBody(); @@ -192,7 +195,12 @@ private MailData PrepareEmail() body += changeReport?.ExportToHtml(); break; case (int)ImpChangeNotificationType.PdfAsAttachment: - attachment = CreateAttachment(Convert.ToBase64String(changeReport?.ToPdf(PaperKind.A4) ?? throw new Exception("No Pdf generated.")), GlobalConst.kPdf); + string? pdfData = await changeReport.ToPdf(Report.PaperFormat.A4); + + if (string.IsNullOrWhiteSpace(pdfData)) + throw new Exception("No Pdf generated."); + + attachment = CreateAttachment(pdfData, GlobalConst.kPdf); break; case (int)ImpChangeNotificationType.HtmlAsAttachment: attachment = CreateAttachment(changeReport?.ExportToHtml(), GlobalConst.kHtml); diff --git a/roles/middleware/files/FWO.Middleware.Server/ReportScheduler.cs b/roles/middleware/files/FWO.Middleware.Server/ReportScheduler.cs index bc1078ce8..8d5d76624 100644 --- a/roles/middleware/files/FWO.Middleware.Server/ReportScheduler.cs +++ b/roles/middleware/files/FWO.Middleware.Server/ReportScheduler.cs @@ -8,8 +8,8 @@ using FWO.Report; using FWO.Report.Filter; using System.Timers; -using WkHtmlToPdfDotNet; using FWO.Config.File; +using PuppeteerSharp.Media; using FWO.Services; namespace FWO.Middleware.Server @@ -260,7 +260,7 @@ private static async Task AdaptDeviceFilter(ReportParams reportParams, ApiConnec } } - private static void WriteReportFile(ReportBase report, List fileFormats, ReportFile reportFile) + private static async Task WriteReportFile(ReportBase report, List fileFormats, ReportFile reportFile) { reportFile.Json = report.ExportToJson(); foreach (FileFormat format in fileFormats) @@ -276,7 +276,7 @@ private static void WriteReportFile(ReportBase report, List fileForm break; case GlobalConst.kPdf: - reportFile.Pdf = Convert.ToBase64String(report.ToPdf(PaperKind.A4)); + reportFile.Pdf = await report.ToPdf(Report.PaperFormat.A4); break; case GlobalConst.kJson: diff --git a/roles/test/files/FWO.Test/FWO.Test.csproj b/roles/test/files/FWO.Test/FWO.Test.csproj index 07da59862..253d9bd63 100644 --- a/roles/test/files/FWO.Test/FWO.Test.csproj +++ b/roles/test/files/FWO.Test/FWO.Test.csproj @@ -8,7 +8,7 @@ - + diff --git a/roles/test/files/FWO.Test/HtmlToPdfTest.cs b/roles/test/files/FWO.Test/HtmlToPdfTest.cs index 0271485fe..8e9b00db9 100644 --- a/roles/test/files/FWO.Test/HtmlToPdfTest.cs +++ b/roles/test/files/FWO.Test/HtmlToPdfTest.cs @@ -1,71 +1,110 @@ using NUnit.Framework; using NUnit.Framework.Legacy; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using WkHtmlToPdfDotNet; using FWO.Logging; +using PuppeteerSharp.Media; +using PuppeteerSharp; +using PuppeteerSharp.BrowserData; namespace FWO.Test { [TestFixture] - [Parallelizable] internal class HtmlToPdfTest { - // Pdf converter - protected readonly SynchronizedConverter converter; + private const string FilePath = "pdffile.pdf"; + private const string Html = "

test

test mit puppteer "; + private const string ChromeBinPathLinux = "/usr/local/bin"; - public HtmlToPdfTest() + [Test] + public async Task GeneratePdf() { - converter = new SynchronizedConverter(new PdfTools()); + if (File.Exists(FilePath)) + File.Delete(FilePath); + + OperatingSystem? os = Environment.OSVersion; + + Log.WriteInfo("Test Log", $"OS: {os}"); + + string path = ""; + BrowserFetcher? browserFetcher = default; + + switch (os.Platform) + { + case PlatformID.Win32NT: + browserFetcher = new(); + break; + case PlatformID.Unix: + path = ChromeBinPathLinux; + browserFetcher = new(new BrowserFetcherOptions { Path = path, Platform = Platform.Linux, Browser = SupportedBrowser.Chrome }); + break; + default: + break; + } + + InstalledBrowser? brw = await browserFetcher.DownloadAsync(BrowserTag.Stable); + + using IBrowser? browser = await Puppeteer.LaunchAsync(new LaunchOptions + { + ExecutablePath = brw.GetExecutablePath(), + Headless = true, + Args = ["--no-sandbox"] + }); + + try + { + await TryCreatePDF(browser, PaperFormat.A0); + await TryCreatePDF(browser, PaperFormat.A1); + await TryCreatePDF(browser, PaperFormat.A2); + await TryCreatePDF(browser, PaperFormat.A3); + await TryCreatePDF(browser, PaperFormat.A4); + await TryCreatePDF(browser, PaperFormat.A5); + await TryCreatePDF(browser, PaperFormat.A6); + + await TryCreatePDF(browser, PaperFormat.Ledger); + await TryCreatePDF(browser, PaperFormat.Legal); + await TryCreatePDF(browser, PaperFormat.Letter); + await TryCreatePDF(browser, PaperFormat.Tabloid); + } + catch (Exception) + { + throw; + } + finally + { + await browser.CloseAsync(); + } } - [Test] - [Parallelizable] - public void GeneratePdf() + private async Task TryCreatePDF(IBrowser browser, PaperFormat paperFormat) { - Log.WriteInfo("Test Log", "starting PDF generation"); - // HTML - string html = "

test

test "; + Log.WriteInfo("Test Log", $"Test creating PDF {paperFormat}"); - GlobalSettings globalSettings = new GlobalSettings + try { - ColorMode = ColorMode.Color, - Orientation = Orientation.Landscape, - PaperSize = PaperKind.A4 - }; + using IPage page = await browser.NewPageAsync(); + await page.SetContentAsync(Html); - HtmlToPdfDocument doc = new HtmlToPdfDocument() - { - GlobalSettings = globalSettings, - Objects = - { - new ObjectSettings() - { - PagesCount = true, - HtmlContent = html, - WebSettings = { DefaultEncoding = "utf-8" }, - HeaderSettings = { FontSize = 9, Right = "Page [page] of [toPage]", Line = true, Spacing = 2.812 } - } - } - }; - - byte[] pdf = converter.Convert(doc); - string filePath = "test.pdf"; - using (var s = File.OpenWrite(filePath)) { - var bw = new BinaryWriter(s); - bw.Write(pdf); + PdfOptions pdfOptions = new() { DisplayHeaderFooter = true, Landscape = true, PrintBackground = true, Format = paperFormat, MarginOptions = new MarginOptions { Top = "1cm", Bottom = "1cm", Left = "1cm", Right = "1cm" } }; + byte[] pdfData = await page.PdfDataAsync(pdfOptions); + + await File.WriteAllBytesAsync(FilePath, pdfData); + + Assert.That(FilePath, Does.Exist); + FileAssert.Exists(FilePath); + ClassicAssert.AreEqual(new FileInfo(FilePath).Length, pdfData.Length); } - Assert.That(filePath, Does.Exist); - ClassicAssert.Greater(new System.IO.FileInfo(filePath).Length, 5000); + catch (Exception) + { + throw new Exception("This paper kind is currently not supported. Please choose another one or \"Custom\" for a custom size."); + } } [OneTimeTearDown] public void OnFinished() { - File.Delete("test.pdf"); + if (File.Exists(FilePath)) + { + File.Delete(FilePath); + } } } } diff --git a/roles/ui/files/FWO.UI/FWO.Ui.csproj b/roles/ui/files/FWO.UI/FWO.Ui.csproj index bbcd2b389..124bdd0cc 100644 --- a/roles/ui/files/FWO.UI/FWO.Ui.csproj +++ b/roles/ui/files/FWO.UI/FWO.Ui.csproj @@ -9,6 +9,7 @@ + diff --git a/roles/ui/files/FWO.UI/Pages/Reporting/ReportExport.razor b/roles/ui/files/FWO.UI/Pages/Reporting/ReportExport.razor index 2973f09e2..a5c28e9e5 100644 --- a/roles/ui/files/FWO.UI/Pages/Reporting/ReportExport.razor +++ b/roles/ui/files/FWO.UI/Pages/Reporting/ReportExport.razor @@ -1,16 +1,19 @@ @using System.Text @using FWO.Report @using FWO.Config.Api -@using WkHtmlToPdfDotNet +@using System.Reflection @using System.Linq @using FWO.Report.Filter +@using System.IO @using FWO.Services @inject ApiConnection apiConnection @inject UserConfig userConfig - + @@ -34,20 +37,20 @@ {
@(userConfig.GetText("page_format")): - - @if (SelectedPaperKind == PaperKind.Custom) + + @if (SelectedPaperFormat == PaperFormat.Custom) {
-
- @(userConfig.GetText("width")) -
- +
+ @(userConfig.GetText("width")) +
+
-
- @(userConfig.GetText("height")) -
- +
+ @(userConfig.GetText("height")) +
+
}
@@ -64,7 +67,7 @@ { } - else + else { ExportCsv = false; @@ -93,11 +96,13 @@