using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace Oms.Application.Labeling.Hiprint; /// /// QuestPDF 实现的面单渲染器。 /// public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabelPdfRenderer { private static int _licenseConfigured; public Task RenderAsync( string templateJson, IReadOnlyDictionary printData, CancellationToken cancellationToken = default) => RenderManyAsync(templateJson, [printData], cancellationToken); public Task RenderManyAsync( string templateJson, IReadOnlyList> printDataPages, CancellationToken cancellationToken = default) { EnsureLicense(); cancellationToken.ThrowIfCancellationRequested(); if (printDataPages == null || printDataPages.Count == 0) { throw new ArgumentException("printDataPages 不能为空", nameof(printDataPages)); } if (string.IsNullOrWhiteSpace(templateJson)) { throw new ArgumentException("templateJson 不能为空", nameof(templateJson)); } var bytes = Document.Create(container => { for (var i = 0; i < printDataPages.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); var pageIr = parser.Parse(templateJson, printDataPages[i]); var pageWidthPt = MmToPt(pageIr.WidthMm); var pageHeightPt = MmToPt(pageIr.HeightMm); container.Page(page => { page.Size(pageWidthPt, pageHeightPt); page.Margin(0); page.Content().Layers(layers => { layers.PrimaryLayer().Background(Colors.White); foreach (var op in pageIr.Ops) { DrawOp(layers, op); } }); }); } }).GeneratePdf(); return Task.FromResult(bytes); } private static void DrawOp(LayersDescriptor layers, LabelDrawOp op) { switch (op) { case LabelTextOp text: { var box = layers.Layer() .Unconstrained() .TranslateX(text.LeftPt) .TranslateY(text.TopPt) .Width(text.WidthPt) .Height(text.HeightPt); var aligned = text.Align switch { "center" => box.AlignCenter(), "right" or "end" => box.AlignRight(), _ => box.AlignLeft() }; var span = aligned .AlignTop() .Text(text.Text) .FontSize(text.FontSizePt) .FontColor(ParseColor(text.ColorHex)); if (text.Bold) { span.Bold(); } break; } case LabelBarcodeOp barcode: { var pxW = Math.Max(40, (int)(barcode.WidthPt * 2)); var pxH = Math.Max(40, (int)(barcode.HeightPt * 2)); var png = BarcodeImageGenerator.CreateBarcodePng(barcode.Content, pxW, pxH, barcode.QrCode); layers.Layer() .Unconstrained() .TranslateX(barcode.LeftPt) .TranslateY(barcode.TopPt) .Width(barcode.WidthPt) .Height(barcode.HeightPt) .Image(png) .FitArea(); break; } case LabelLineOp line: { var w = line.Vertical ? Math.Max(line.ThicknessPt, 0.5f) : line.WidthPt; var h = line.Vertical ? line.HeightPt : Math.Max(line.ThicknessPt, 0.5f); layers.Layer() .Unconstrained() .TranslateX(line.LeftPt) .TranslateY(line.TopPt) .Width(w) .Height(h) .Background(ParseColor(line.ColorHex)); break; } case LabelRectOp rect: layers.Layer() .Unconstrained() .TranslateX(rect.LeftPt) .TranslateY(rect.TopPt) .Width(rect.WidthPt) .Height(rect.HeightPt) .BorderColor(ParseColor(rect.BorderColorHex)) .Border(rect.BorderWidthPt); break; case LabelImageOp image when !string.IsNullOrWhiteSpace(image.Src) && File.Exists(image.Src): layers.Layer() .Unconstrained() .TranslateX(image.LeftPt) .TranslateY(image.TopPt) .Width(image.WidthPt) .Height(image.HeightPt) .Image(image.Src) .FitArea(); break; } } private static void EnsureLicense() { if (Interlocked.Exchange(ref _licenseConfigured, 1) == 1) { return; } QuestPDF.Settings.License = LicenseType.Community; } private static float MmToPt(float mm) => mm * 72f / 25.4f; private static Color ParseColor(string? hex) { if (string.IsNullOrWhiteSpace(hex)) { return Colors.Black; } var value = hex.Trim(); if (!value.StartsWith('#')) { value = "#" + value; } try { return Color.FromHex(value); } catch { return Colors.Black; } } }