WuYongJie 0dc3c09315 feat(资产与打印): 提交近期借还盘点与打印助手源码
- 提交借用单缺失异常处理、扫码借还搜索与站内消息快捷处理源码
- 提交盘点范围、多终端打印、组织同步与认证模式相关源码
- 提交 .NET/Node 打印助手及版本清单源码
2026-03-27 20:18:31 +08:00

437 lines
14 KiB
C#

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<PrinterInfo> ListPrinters()
{
var defaultPrinter = string.Empty;
try
{
var settings = new PrinterSettings();
defaultPrinter = settings.PrinterName;
}
catch
{
defaultPrinter = string.Empty;
}
return PrinterSettings.InstalledPrinters
.Cast<string>()
.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<int> PrintAssetLabelsAsync(string printerName, IReadOnlyList<AssetLabelItem> 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<int> PrintRenderedLabelImagesAsync(
string printerName,
IReadOnlyList<AssetLabelRenderedImage> renderedLabels)
{
return Task.Run(() =>
{
var pages = renderedLabels.Select(RenderRenderedLabelImage).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 RenderRenderedLabelImage(AssetLabelRenderedImage renderedLabel)
{
const string dataUrlPrefix = "data:image/png;base64,";
if (string.IsNullOrWhiteSpace(renderedLabel.ImageDataUrl) ||
!renderedLabel.ImageDataUrl.StartsWith(dataUrlPrefix, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("当前打印任务缺少有效的 PNG 成品标签图。");
}
try
{
var base64 = renderedLabel.ImageDataUrl[dataUrlPrefix.Length..];
var imageBytes = Convert.FromBase64String(base64);
using var stream = new MemoryStream(imageBytes);
using var image = Image.FromStream(stream, true, true);
return new Bitmap(image);
}
catch (Exception ex)
{
throw new InvalidOperationException($"成品标签图解析失败:{ex.Message}", ex);
}
}
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<Bitmap> 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.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.SmoothingMode = SmoothingMode.HighQuality;
var destinationRect = CalculateContainRectangle(eventArgs.PageBounds, page.Size);
graphics.DrawImage(page, destinationRect);
pageIndex += 1;
eventArgs.HasMorePages = pageIndex < pages.Count;
};
document.Print();
}
private static Rectangle CalculateContainRectangle(Rectangle bounds, Size imageSize)
{
if (imageSize.Width <= 0 || imageSize.Height <= 0)
{
return bounds;
}
var widthScale = bounds.Width / (double)imageSize.Width;
var heightScale = bounds.Height / (double)imageSize.Height;
var scale = Math.Min(widthScale, heightScale);
var drawWidth = Math.Max(1, (int)Math.Round(imageSize.Width * scale));
var drawHeight = Math.Max(1, (int)Math.Round(imageSize.Height * scale));
var offsetX = bounds.X + (bounds.Width - drawWidth) / 2;
var offsetY = bounds.Y + (bounds.Height - drawHeight) / 2;
return new Rectangle(offsetX, offsetY, drawWidth, drawHeight);
}
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<string> 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..]];
}
}