All checks were successful
Build and Deploy / deploy (push) Successful in 1m13s
支持按 box/sku 模板生成 PDF;待支付前可改产品并自动重排货重算费。 Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.8 KiB
C#
190 lines
5.8 KiB
C#
using QuestPDF.Fluent;
|
|
using QuestPDF.Helpers;
|
|
using QuestPDF.Infrastructure;
|
|
|
|
namespace Oms.Application.Labeling.Hiprint;
|
|
|
|
/// <summary>
|
|
/// QuestPDF 实现的面单渲染器。
|
|
/// </summary>
|
|
public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabelPdfRenderer
|
|
{
|
|
private static int _licenseConfigured;
|
|
|
|
public Task<byte[]> RenderAsync(
|
|
string templateJson,
|
|
IReadOnlyDictionary<string, object?> printData,
|
|
CancellationToken cancellationToken = default) =>
|
|
RenderManyAsync(templateJson, [printData], cancellationToken);
|
|
|
|
public Task<byte[]> RenderManyAsync(
|
|
string templateJson,
|
|
IReadOnlyList<IReadOnlyDictionary<string, object?>> 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;
|
|
}
|
|
}
|
|
}
|