Implement Chromium-based PDF generation service; add fetchAndDownloadPdf utility and update contract generation endpoint

This commit is contained in:
masoodafar-web
2025-11-14 15:20:46 +03:30
parent 230ba41113
commit 680ef3a7e2
9 changed files with 297 additions and 97 deletions
@@ -0,0 +1,148 @@
using PuppeteerSharp;
using PuppeteerSharp.Media;
using Microsoft.Extensions.Configuration;
namespace FrontOffice.Main.Utilities.Pdf;
public interface IChromiumPdfService
{
Task<byte[]> GeneratePdfAsync(string html, string? fileName = null, CancellationToken ct = default);
}
public class ChromiumPdfService : IChromiumPdfService
{
private static Task _browserPreparationTask = Task.CompletedTask;
private static bool _browserReady;
private readonly IConfiguration _config;
private static string? _executablePath; // path to system Chrome/Chromium if available
private static IBrowser? _browser; // reused headless browser instance
private static readonly SemaphoreSlim BrowserLock = new(1, 1);
public ChromiumPdfService(IConfiguration config)
{
_config = config;
PrepareBrowserAsync();
}
private void PrepareBrowserAsync()
{
if (_browserReady) return;
_browserPreparationTask = PrepareBrowserCoreAsync();
}
private Task PrepareBrowserCoreAsync()
{
try
{
// Config / env override
_executablePath = _config["Pdf:ChromiumExecutablePath"] ?? Environment.GetEnvironmentVariable("CHROMIUM_EXECUTABLE_PATH");
if (!string.IsNullOrWhiteSpace(_executablePath) && File.Exists(_executablePath))
{
#if DEBUG
Console.WriteLine($"[PDF] Using configured Chromium executable: {_executablePath}");
#endif
_browserReady = true; return Task.CompletedTask;
}
// Common system locations
var candidates = new[]{"/usr/bin/google-chrome","/usr/bin/google-chrome-stable","/usr/bin/chromium","/usr/bin/chromium-browser","/snap/bin/chromium"};
_executablePath = candidates.FirstOrDefault(File.Exists);
if (_executablePath != null)
{
#if DEBUG
Console.WriteLine($"[PDF] Found system Chrome/Chromium: {_executablePath}");
#endif
_browserReady = true; return Task.CompletedTask;
}
// Fall back to PuppeteerSharp auto-download on first launch (no explicit path).
#if DEBUG
Console.WriteLine("[PDF] No local Chromium path resolved. Will rely on PuppeteerSharp auto-download at first launch.");
#endif
_browserReady = true; // allow launch to proceed; it will fetch if needed
}
catch (Exception ex)
{
#if DEBUG
Console.WriteLine($"[PDF] Unexpected preparation error: {ex.Message}");
#endif
}
return Task.CompletedTask;
}
private async Task EnsureBrowserAsync()
{
await _browserPreparationTask;
if (!_browserReady)
{
throw new InvalidOperationException("Chromium not ready for launch.");
}
if (_browser != null && !_browser.IsClosed) return;
await BrowserLock.WaitAsync();
try
{
if (_browser != null && !_browser.IsClosed) return; // double-check after lock
var launchOptions = new LaunchOptions
{
Headless = true,
Args = new[] {"--no-sandbox","--disable-setuid-sandbox"},
ExecutablePath = _executablePath // may be null; PuppeteerSharp will download if needed
};
_browser = await Puppeteer.LaunchAsync(launchOptions);
#if DEBUG
Console.WriteLine("[PDF] Chromium browser instance launched.");
#endif
}
finally
{
BrowserLock.Release();
}
}
public async Task<byte[]> GeneratePdfAsync(string html, string? fileName = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(html)) throw new ArgumentException("Html content is empty", nameof(html));
// Ensure browser available (download may happen here if not done yet)
await EnsureBrowserAsync();
ct.ThrowIfCancellationRequested();
// Inject default styles (RTL + font-face) if not present
if (!html.Contains("<html", StringComparison.OrdinalIgnoreCase))
{
html = $"<html><head><meta charset=\"utf-8\"><style>{GetDefaultStyles()}</style></head><body>{html}</body></html>";
}
else if (!html.Contains("<style", StringComparison.OrdinalIgnoreCase))
{
html = html.Replace("<head>", "<head><style>" + GetDefaultStyles() + "</style>");
}
if (_browser == null) throw new InvalidOperationException("Browser not initialized");
await using var page = await _browser.NewPageAsync();
await page.SetContentAsync(html, new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
var pdfOptions = new PdfOptions
{
Format = PaperFormat.A4,
PrintBackground = true,
DisplayHeaderFooter = false,
MarginOptions = new MarginOptions { Top = "20mm", Bottom = "20mm", Left = "15mm", Right = "15mm" }
};
ct.ThrowIfCancellationRequested();
var data = await page.PdfDataAsync(pdfOptions);
return data;
}
private string GetDefaultStyles()
{
// Relative paths work when using SetContent; ensure fonts are served or embedded
return "@font-face{font-family:'Vazir';src:url('fonts/Vazir-Regular.ttf') format('truetype');font-weight:400;}" +
"@font-face{font-family:'Vazir';src:url('fonts/Vazir-Medium.ttf') format('truetype');font-weight:500;}" +
"@font-face{font-family:'Vazir';src:url('fonts/Vazir-Bold.ttf') format('truetype');font-weight:700;}" +
"body{font-family:'Vazir','Arial';direction:rtl;text-align:right;font-size:12px;line-height:1.6;margin:0;padding:25px;}";
}
}
@@ -1,36 +1,2 @@
using PdfSharpCore.Pdf;
//using HtmlRendererCore.PdfSharpCore;
// (removed) legacy HtmlToPdfService no longer used
namespace FrontOffice.Main.Utilities.Pdf;
public interface IHtmlToPdfService
{
byte[] GeneratePdf(string html, string? fileName = null);
}
public class HtmlToPdfService : IHtmlToPdfService
{
public byte[] GeneratePdf(string html, string? fileName = null)
{
if (string.IsNullOrWhiteSpace(html))
throw new ArgumentException("Html content is empty", nameof(html));
if (!html.Contains("<html", StringComparison.OrdinalIgnoreCase))
{
html = $"<html><head><meta charset='utf-8'><style>{GetDefaultStyles()}</style></head><body>{html}</body></html>";
}
else if (!html.Contains("<style", StringComparison.OrdinalIgnoreCase))
{
// Inject default styles if none provided
html = html.Replace("<head>", "<head><style>" + GetDefaultStyles() + "</style>");
}
using var document = new PdfDocument();
//PdfGenerator.AddPdfPages(document, html, PdfSharpCore.PageSize.A4);
using var ms = new MemoryStream();
document.Save(ms);
return ms.ToArray();
}
private static string GetDefaultStyles() => @"body{font-family:'Vazir','Arial';direction:rtl;text-align:right;font-size:12px;line-height:1.6;margin:25px;} h1,h2,h3{margin:0 0 12px;} ul{margin:0 0 12px 0;padding:0 18px;} .small{font-size:10px;color:#555;}";
}