using System.Drawing.Drawing2D; using System.Drawing.Printing; using System.Drawing.Text; using QRCoder; namespace FixedAssetPrintAgent; internal sealed class LabelPrintService { private const string CompanyName = "常州简尚家具有限公司"; private const int CanvasWidth = 480; private const int CanvasHeight = 320; private const int PaperWidthHundredthsOfInch = 236; private const int PaperHeightHundredthsOfInch = 157; public List ListPrinters() { var defaultPrinter = string.Empty; try { var settings = new PrinterSettings(); defaultPrinter = settings.PrinterName; } catch { defaultPrinter = string.Empty; } return PrinterSettings.InstalledPrinters .Cast() .Select(name => new PrinterInfo { Name = name, IsDefault = string.Equals(name, defaultPrinter, StringComparison.OrdinalIgnoreCase), IsAvailable = true }) .OrderByDescending(item => item.IsDefault) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase) .ToList(); } public Task PrintAssetLabelsAsync(string printerName, IReadOnlyList items) { return Task.Run(() => { var pages = items.Select(RenderAssetLabel).ToList(); try { PrintBitmaps(printerName, pages, "固定资产标签打印"); return pages.Count; } finally { foreach (var page in pages) { page.Dispose(); } } }); } public Task PrintTestLabelAsync(string printerName) { return Task.Run(() => { using var bitmap = RenderAssetLabel(new AssetLabelItem { EquipmentCode = "TEST-60X40", EquipmentName = "测试标签", EquipmentType = "测试标签", PurchaseDate = DateTimeOffset.Now.ToString("yyyy-MM-dd"), Department = new DepartmentInfo { Name = $"测试打印 · {printerName}" } }); PrintBitmaps(printerName, [bitmap], "固定资产测试标签"); }); } private static Bitmap RenderAssetLabel(AssetLabelItem item) { var bitmap = new Bitmap(CanvasWidth, CanvasHeight); bitmap.SetResolution(203, 203); using var graphics = Graphics.FromImage(bitmap); graphics.Clear(Color.White); graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; using var outerPen = new Pen(Color.Black, 4); graphics.DrawRectangle(outerPen, 2, 2, CanvasWidth - 4, CanvasHeight - 4); DrawHeader(graphics); DrawQrSection(graphics, item); DrawInfoSection(graphics, item); return bitmap; } private static void DrawHeader(Graphics graphics) { var headerRect = new Rectangle(0, 0, CanvasWidth, 54); using var fillBrush = new SolidBrush(Color.Black); graphics.FillRectangle(fillBrush, headerRect); using var companyFont = new Font("Microsoft YaHei", 22, FontStyle.Bold, GraphicsUnit.Pixel); using var whiteBrush = new SolidBrush(Color.White); using var companyFormat = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }; graphics.DrawString(CompanyName, companyFont, whiteBrush, new RectangleF(18, 0, 350, 54), companyFormat); graphics.FillRectangle(whiteBrush, new Rectangle(414, 16, 18, 18)); graphics.FillRectangle(whiteBrush, new Rectangle(440, 16, 5, 18)); } private static void DrawQrSection(Graphics graphics, AssetLabelItem item) { var panelRect = new Rectangle(14, 66, 184, 184); using var framePen = new Pen(Color.Black, 2); graphics.DrawRectangle(framePen, panelRect); var innerRect = Rectangle.Inflate(panelRect, -10, -10); graphics.DrawRectangle(framePen, innerRect); using var qrBitmap = BuildQrBitmap(item.EquipmentCode); var qrRect = Rectangle.Inflate(innerRect, -10, -10); graphics.DrawImage(qrBitmap, qrRect); } private static void DrawInfoSection(Graphics graphics, AssetLabelItem item) { var startX = 212; var contentWidth = 252; var typeText = NormalizeText(item.EquipmentType, "未分类资产"); var codeLines = SplitCodeForDisplay(item.EquipmentCode); var purchaseDate = FormatLabelDate(item.PurchaseDate); var organization = NormalizeText(item.Department?.Name, "未分配组织"); using var blackBrush = new SolidBrush(Color.Black); using var whiteBrush = new SolidBrush(Color.White); using var typeFont = new Font("Bahnschrift", 18, FontStyle.Bold, GraphicsUnit.Pixel); using var codeFont = new Font("Bahnschrift", 34, FontStyle.Bold, GraphicsUnit.Pixel); using var captionFont = new Font("Bahnschrift", 13, FontStyle.Bold, GraphicsUnit.Pixel); using var valueFont = new Font("Microsoft YaHei", 18, FontStyle.Bold, GraphicsUnit.Pixel); using var orgFont = new Font("Microsoft YaHei", 17, FontStyle.Bold, GraphicsUnit.Pixel); using var noWrapFormat = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }; var typeWidth = Math.Min(contentWidth, Math.Max(92, (int)Math.Ceiling(graphics.MeasureString(typeText, typeFont).Width) + 24)); var typeRect = new Rectangle(startX, 70, typeWidth, 34); graphics.FillRectangle(blackBrush, typeRect); graphics.DrawString(typeText, typeFont, whiteBrush, typeRect, noWrapFormat); var codeY = 114; foreach (var line in codeLines.Take(2)) { graphics.DrawString(line, codeFont, blackBrush, new RectangleF(startX, codeY, contentWidth, 38), noWrapFormat); codeY += 34; } graphics.FillRectangle(blackBrush, new Rectangle(startX, 184, contentWidth - 4, 3)); DrawCaptionWithValue( graphics, startX, 198, "采购日期", string.IsNullOrWhiteSpace(purchaseDate) ? "-" : purchaseDate, captionFont, valueFont); DrawCaptionWithMultilineValue( graphics, startX, 242, "所属组织", organization, captionFont, orgFont, contentWidth, 44); graphics.FillRectangle(blackBrush, new Rectangle(startX, 296, 180, 3)); graphics.FillRectangle(blackBrush, new Rectangle(startX + 188, 287, 26, 12)); } private static void DrawCaptionWithValue( Graphics graphics, int x, int y, string caption, string value, Font captionFont, Font valueFont) { using var blackBrush = new SolidBrush(Color.Black); using var whiteBrush = new SolidBrush(Color.White); using var captionFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, FormatFlags = StringFormatFlags.NoWrap }; using var valueFormat = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisCharacter, FormatFlags = StringFormatFlags.NoWrap }; var captionRect = new Rectangle(x, y, 82, 24); graphics.FillRectangle(blackBrush, captionRect); graphics.DrawString(caption, captionFont, whiteBrush, captionRect, captionFormat); graphics.DrawString(value, valueFont, blackBrush, new RectangleF(x, y + 24, 210, 28), valueFormat); } private static void DrawCaptionWithMultilineValue( Graphics graphics, int x, int y, string caption, string value, Font captionFont, Font valueFont, int maxWidth, int maxHeight) { using var blackBrush = new SolidBrush(Color.Black); using var whiteBrush = new SolidBrush(Color.White); using var captionFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center, FormatFlags = StringFormatFlags.NoWrap }; using var valueFormat = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Near, Trimming = StringTrimming.EllipsisCharacter }; var captionRect = new Rectangle(x, y, 82, 24); graphics.FillRectangle(blackBrush, captionRect); graphics.DrawString(caption, captionFont, whiteBrush, captionRect, captionFormat); graphics.DrawString(value, valueFont, blackBrush, new RectangleF(x, y + 24, maxWidth, maxHeight), valueFormat); } private static Bitmap BuildQrBitmap(string text) { using var generator = new QRCodeGenerator(); using var data = generator.CreateQrCode(NormalizeText(text, "UNKNOWN"), QRCodeGenerator.ECCLevel.Q); using var qrCode = new QRCode(data); return qrCode.GetGraphic(20, Color.Black, Color.White, true); } private static void PrintBitmaps(string printerName, IReadOnlyList pages, string documentName) { if (string.IsNullOrWhiteSpace(printerName)) { throw new InvalidOperationException("尚未选择标签打印机,请先在助手里保存打印机。"); } using var document = new PrintDocument(); document.DocumentName = documentName; document.PrinterSettings.PrinterName = printerName; if (!document.PrinterSettings.IsValid) { throw new InvalidOperationException($"找不到打印机:{printerName}"); } document.DefaultPageSettings.Margins = new Margins(0, 0, 0, 0); document.DefaultPageSettings.PaperSize = new PaperSize("AssetLabel60x40", PaperWidthHundredthsOfInch, PaperHeightHundredthsOfInch); document.DefaultPageSettings.Landscape = false; document.OriginAtMargins = false; document.PrintController = new StandardPrintController(); var pageIndex = 0; document.PrintPage += (_, eventArgs) => { var page = pages[pageIndex]; var graphics = eventArgs.Graphics; if (graphics is null) { throw new InvalidOperationException("当前打印驱动没有返回可用的绘图上下文。"); } graphics.PageUnit = GraphicsUnit.Display; graphics.Clear(Color.White); graphics.DrawImage(page, eventArgs.PageBounds); pageIndex += 1; eventArgs.HasMorePages = pageIndex < pages.Count; }; document.Print(); } private static string NormalizeText(string? value, string fallback) { var normalized = string.IsNullOrWhiteSpace(value) ? string.Empty : string.Join(" ", value.Split(['\r', '\n', '\t', ' '], StringSplitOptions.RemoveEmptyEntries)); return string.IsNullOrWhiteSpace(normalized) ? fallback : normalized; } private static string FormatLabelDate(string? value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } var normalized = value.Trim().Replace('/', '-'); return normalized.Length >= 10 ? normalized[..10] : normalized; } private static List SplitCodeForDisplay(string? value) { var normalized = NormalizeText(value, "-"); if (normalized.Length <= 14) { return [normalized]; } var separatorIndexes = normalized .Select((character, index) => (character, index)) .Where(item => item.character is '-' or '/') .Select(item => item.index) .ToList(); if (separatorIndexes.Count > 0) { var middleIndex = normalized.Length / 2.0; var bestIndex = separatorIndexes .OrderBy(index => Math.Abs(index - middleIndex)) .First(); var first = normalized[..(bestIndex + 1)].Trim(); var second = normalized[(bestIndex + 1)..].Trim(); if (!string.IsNullOrWhiteSpace(first) && !string.IsNullOrWhiteSpace(second)) { return [first, second]; } } var midpoint = (int)Math.Ceiling(normalized.Length / 2.0); return [normalized[..midpoint], normalized[midpoint..]]; } }