Implement Chromium-based PDF generation service; add fetchAndDownloadPdf utility and update contract generation endpoint
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.DataProtection.KeyManagement;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
@@ -10,21 +9,28 @@ public class MobileNumberEncryptor
|
||||
private readonly string _iv;
|
||||
public MobileNumberEncryptor(IConfiguration configuration)
|
||||
{
|
||||
var encryptionSettings = configuration.GetSection("EncryptionSettings").Get<EncryptionSettings>();
|
||||
var encryptionSettings = configuration
|
||||
.GetSection("EncryptionSettings")
|
||||
.Get<EncryptionSettings>()
|
||||
?? throw new InvalidOperationException("EncryptionSettings section not found in configuration");
|
||||
|
||||
_key = encryptionSettings?.Key ?? throw new ArgumentNullException("Encryption Key not found in configuration");
|
||||
_iv = encryptionSettings?.IV ?? throw new ArgumentNullException("Encryption IV not found in configuration");
|
||||
_key = !string.IsNullOrWhiteSpace(encryptionSettings.Key)
|
||||
? encryptionSettings.Key
|
||||
: throw new InvalidOperationException("Encryption Key not found in configuration");
|
||||
_iv = !string.IsNullOrWhiteSpace(encryptionSettings.IV)
|
||||
? encryptionSettings.IV
|
||||
: throw new InvalidOperationException("Encryption IV not found in configuration");
|
||||
|
||||
// اعتبارسنجی سایز
|
||||
ValidateKeyAndIV();
|
||||
ValidateKeyAndIv();
|
||||
}
|
||||
public MobileNumberEncryptor(string key, string iv)
|
||||
{
|
||||
_key = key;
|
||||
_iv = iv;
|
||||
ValidateKeyAndIV();
|
||||
ValidateKeyAndIv();
|
||||
}
|
||||
private void ValidateKeyAndIV()
|
||||
private void ValidateKeyAndIv()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -32,10 +38,10 @@ public class MobileNumberEncryptor
|
||||
byte[] ivBytes = Convert.FromBase64String(_iv);
|
||||
|
||||
if (keyBytes.Length != 32)
|
||||
throw new ArgumentException("Key must be 32 bytes in Base64 format");
|
||||
throw new ArgumentException("Key must be 32 bytes in Base64 format", nameof(_key));
|
||||
|
||||
if (ivBytes.Length != 16)
|
||||
throw new ArgumentException("IV must be 16 bytes in Base64 format");
|
||||
throw new ArgumentException("IV must be 16 bytes in Base64 format", nameof(_iv));
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
@@ -46,7 +52,7 @@ public class MobileNumberEncryptor
|
||||
public string EncryptMobileNumber(string mobileNumber)
|
||||
{
|
||||
if (string.IsNullOrEmpty(mobileNumber))
|
||||
throw new ArgumentException("Mobile number cannot be null or empty");
|
||||
throw new ArgumentException("Mobile number cannot be null or empty", nameof(mobileNumber));
|
||||
|
||||
try
|
||||
{
|
||||
@@ -58,7 +64,7 @@ public class MobileNumberEncryptor
|
||||
{
|
||||
aesAlg.Key = key;
|
||||
aesAlg.IV = iv;
|
||||
aesAlg.Mode = CipherMode.ECB;
|
||||
aesAlg.Mode = CipherMode.CBC; // ECB ignores IV; CBC is standard with IV
|
||||
aesAlg.Padding = PaddingMode.PKCS7;
|
||||
|
||||
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
|
||||
@@ -85,7 +91,7 @@ public class MobileNumberEncryptor
|
||||
public string DecryptMobileNumber(string encryptedMobileNumber)
|
||||
{
|
||||
if (string.IsNullOrEmpty(encryptedMobileNumber))
|
||||
throw new ArgumentException("Encrypted mobile number cannot be null or empty");
|
||||
throw new ArgumentException("Encrypted mobile number cannot be null or empty", nameof(encryptedMobileNumber));
|
||||
|
||||
try
|
||||
{
|
||||
@@ -97,7 +103,7 @@ public class MobileNumberEncryptor
|
||||
{
|
||||
aesAlg.Key = key;
|
||||
aesAlg.IV = iv;
|
||||
aesAlg.Mode = CipherMode.ECB;
|
||||
aesAlg.Mode = CipherMode.CBC;
|
||||
aesAlg.Padding = PaddingMode.PKCS7;
|
||||
|
||||
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
|
||||
|
||||
148
src/FrontOffice.Main/Utilities/Pdf/ChromiumPdfService.cs
Normal file
148
src/FrontOffice.Main/Utilities/Pdf/ChromiumPdfService.cs
Normal file
@@ -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;}";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user