-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReceiptRenderer.cs
More file actions
167 lines (158 loc) · 10.2 KB
/
Copy pathReceiptRenderer.cs
File metadata and controls
167 lines (158 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
using System.Drawing.Drawing2D;
namespace FMPReceiptPrinter;
public static class ReceiptRenderer
{
public const float PageWidth = 850f; // hundredths of an inch, approximately 216 mm
public const float PageHeight = 1098f; // approximately 279 mm
private const float PageSafeMargin = 36f;
private const float ReceiptPadding = 6f;
private const float TitleWidthRatio = .55f;
private const float QuantityColumnRatio = .12f;
private const float DescriptionColumnRatio = .60f;
private const float UnitPriceColumnRatio = .80f;
private const float PreparedSectionRatio = .48f;
private const float ReceivedSectionStartRatio = .52f;
private const float LogoOffsetX = 7f;
private const float LogoSize = 54f;
private const float HeaderIndent = 64f;
public static void DrawPage(Graphics g, RectangleF target, ReceiptData receipt, ReceiptTemplate template, IReadOnlySet<ReceiptSlot> slots, bool showCutGuides = true)
{
var state = g.Save();
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.TranslateTransform(target.Left, target.Top);
g.ScaleTransform(target.Width / PageWidth, target.Height / PageHeight);
g.Clear(Color.White);
// Keep receipt content inside a roughly 9 mm safe area. This small,
// symmetric inset clears the tested printer's right-hand hardware edge
// without changing the four-up proportions or header/logo alignment.
// The print path separately compensates for the printer's hard margin.
var inner = new RectangleF(PageSafeMargin, PageSafeMargin, PageWidth - PageSafeMargin * 2, PageHeight - PageSafeMargin * 2);
var halfW = inner.Width / 2f;
var halfH = inner.Height / 2f;
var rects = new Dictionary<ReceiptSlot, RectangleF>
{
[ReceiptSlot.TopLeft] = new(inner.Left, inner.Top, halfW, halfH),
[ReceiptSlot.TopRight] = new(inner.Left + halfW, inner.Top, halfW, halfH),
[ReceiptSlot.BottomLeft] = new(inner.Left, inner.Top + halfH, halfW, halfH),
[ReceiptSlot.BottomRight] = new(inner.Left + halfW, inner.Top + halfH, halfW, halfH)
};
if (showCutGuides)
{
using var guide = new Pen(Color.FromArgb(170, 170, 170), 0.6f) { DashStyle = DashStyle.Dash };
g.DrawLine(guide, inner.Left + halfW, inner.Top, inner.Left + halfW, inner.Bottom);
g.DrawLine(guide, inner.Left, inner.Top + halfH, inner.Right, inner.Top + halfH);
}
foreach (var slot in slots) DrawReceipt(g, RectangleF.Inflate(rects[slot], -ReceiptPadding, -ReceiptPadding), receipt, template);
g.Restore(state);
}
private static void DrawReceipt(Graphics g, RectangleF r, ReceiptData data, ReceiptTemplate t)
{
var accent = ParseColor(t.AccentColor, Color.FromArgb(8, 126, 175));
using var black = new Pen(Color.Black, 0.9f);
using var lightFill = new SolidBrush(Color.FromArgb(234, 245, 249));
using var dark = new SolidBrush(Color.Black);
using var gray = new SolidBrush(Color.DimGray);
using var accentBrush = new SolidBrush(accent);
using var red = new SolidBrush(Color.FromArgb(210, 25, 32));
using var businessFont = new Font("Arial", 11.2f, FontStyle.Bold);
using var smallFont = new Font("Arial", 6.2f);
using var labelFont = new Font("Arial", 6.8f, FontStyle.Bold);
using var bodyFont = new Font("Arial", 6.7f);
using var titleFont = new Font("Arial", 8.5f, FontStyle.Bold);
using var numberFont = new Font("Arial", 8.7f, FontStyle.Bold);
using var headerFont = new Font("Arial", 6.2f, FontStyle.Bold);
using var totalFont = new Font("Arial", 6.9f, FontStyle.Bold);
var center = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
var right = new StringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Center };
var left = new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center };
var y = r.Top;
var logoRect = new RectangleF(r.Left + LogoOffsetX, y - 2, LogoSize, LogoSize);
var logoPath = Path.Combine(AppContext.BaseDirectory, "Assets", t.LogoFile);
if (File.Exists(logoPath))
{
using var logo = Image.FromFile(logoPath);
g.DrawImage(logo, logoRect);
}
var headerX = r.Left + HeaderIndent;
var headerW = r.Width - HeaderIndent;
g.DrawString(t.BusinessName, businessFont, accentBrush, new RectangleF(headerX, y, headerW, 17), center);
g.DrawString(t.Proprietor, smallFont, gray, new RectangleF(headerX, y + 17, headerW, 9), center);
g.DrawString(t.Address, smallFont, gray, new RectangleF(headerX, y + 26, headerW, 9), center);
g.DrawString(t.Contact, smallFont, gray, new RectangleF(headerX, y + 35, headerW, 9), center);
y += 56;
g.DrawString("DELIVERY RECEIPT", titleFont, dark, new RectangleF(r.Left, y, r.Width * TitleWidthRatio, 18), left);
g.DrawString("NO.", labelFont, dark, new RectangleF(r.Right - 95, y, 30, 18), right);
g.DrawString(data.ReceiptNumber.ToString("00000"), numberFont, red, new RectangleF(r.Right - 64, y, 64, 18), right);
y += 23;
DrawLabelLine(g, labelFont, bodyFont, "Customer:", data.Customer, r.Left, y, r.Width - 115);
DrawLabelLine(g, labelFont, bodyFont, "Date:", data.Date.ToString("yyyy-MM-dd"), r.Right - 105, y, 105);
y += 20;
DrawLabelLine(g, labelFont, bodyFont, "Address:", data.Address, r.Left, y, r.Width);
y += 20;
g.DrawString($"Terms: {data.Terms}", bodyFont, dark, new RectangleF(r.Left, y, 160, 15), left);
g.DrawString($"Due: {data.DueDate:yyyy-MM-dd}", bodyFont, dark, new RectangleF(r.Right - 115, y, 115, 15), right);
y += 20;
var tableTop = y;
var col = new[] { r.Left, r.Left + r.Width * QuantityColumnRatio, r.Left + r.Width * DescriptionColumnRatio, r.Left + r.Width * UnitPriceColumnRatio, r.Right };
const float headH = 26f;
var itemRows = Math.Max(4, t.ItemRows);
const float rowH = 35f;
const float totalH = 22f;
g.FillRectangle(lightFill, r.Left, tableTop, r.Width, headH);
var tableBottom = tableTop + headH + itemRows * rowH + totalH * 2;
for (var i = 0; i < col.Length; i++) g.DrawLine(black, col[i], tableTop, col[i], tableTop + headH + itemRows * rowH);
g.DrawRectangle(black, r.Left, tableTop, r.Width, tableBottom - tableTop);
g.DrawString("QTY", headerFont, dark, new RectangleF(col[0], tableTop, col[1]-col[0], headH), center);
g.DrawString("DESCRIPTION", headerFont, dark, new RectangleF(col[1], tableTop, col[2]-col[1], headH), center);
g.DrawString("UNIT PRICE", headerFont, dark, new RectangleF(col[2], tableTop, col[3]-col[2], headH), center);
g.DrawString("AMOUNT", headerFont, dark, new RectangleF(col[3], tableTop, col[4]-col[3], headH), center);
y = tableTop + headH;
for (var i = 0; i < itemRows; i++)
{
g.DrawLine(black, r.Left, y, r.Right, y);
var item = i < data.Items.Count ? data.Items[i] : null;
if (item is not null)
{
g.DrawString(FormatNumber(item.Quantity), bodyFont, dark, new RectangleF(col[0]+2, y, col[1]-col[0]-4, rowH), center);
g.DrawString(item.Description, bodyFont, dark, new RectangleF(col[1]+3, y, col[2]-col[1]-6, rowH), left);
g.DrawString(FormatMoney(item.UnitPrice), bodyFont, dark, new RectangleF(col[2]+2, y, col[3]-col[2]-4, rowH), right);
g.DrawString(FormatMoney(item.Amount), bodyFont, dark, new RectangleF(col[3]+2, y, col[4]-col[3]-4, rowH), right);
}
y += rowH;
}
g.DrawLine(black, r.Left, y, r.Right, y);
g.DrawLine(black, col[3], y, col[3], tableBottom);
g.DrawString("DELIVERY FEE", totalFont, dark, new RectangleF(r.Left, y, col[3]-r.Left-6, totalH), right);
g.DrawString(FormatMoney(data.DeliveryFee), bodyFont, dark, new RectangleF(col[3]+2, y, r.Right-col[3]-4, totalH), right);
y += totalH; g.DrawLine(black, r.Left, y, r.Right, y);
g.DrawString("TOTAL", totalFont, dark, new RectangleF(r.Left, y, col[3]-r.Left-6, totalH), right);
g.DrawString(FormatMoney(data.Total), totalFont, dark, new RectangleF(col[3]+2, y, r.Right-col[3]-4, totalH), right);
y += totalH + 8;
g.DrawString($"Prepared by: {data.PreparedBy}", labelFont, dark, new RectangleF(r.Left, y, r.Width * PreparedSectionRatio, 18), left);
if (data.UseESignature && File.Exists(data.SignatureFile))
{
try
{
using var signature = Image.FromFile(data.SignatureFile);
// Keep the signature on the prepared-by side, larger and farther left.
var signatureRect = new RectangleF(r.Left + 34, y - 28, 118, 43);
g.DrawImage(signature, signatureRect);
}
catch { /* Keep printing usable if a signature image is corrupt. */ }
}
g.DrawString("Received by: __________________", labelFont, dark, new RectangleF(r.Left + r.Width * ReceivedSectionStartRatio, y, r.Width * PreparedSectionRatio, 18), right);
}
private static void DrawLabelLine(Graphics g, Font labelFont, Font valueFont, string label, string value, float x, float y, float width)
{
using var brush = new SolidBrush(Color.Black);
using var pen = new Pen(Color.Black, .6f);
var labelWidth = g.MeasureString(label, labelFont).Width + 3;
g.DrawString(label, labelFont, brush, new RectangleF(x, y, labelWidth, 16));
g.DrawString(value, valueFont, brush, new RectangleF(x + labelWidth, y, width - labelWidth, 16));
g.DrawLine(pen, x + labelWidth, y + 15, x + width, y + 15);
}
private static string FormatMoney(decimal value) => value == 0 ? "" : value.ToString("N2");
private static string FormatNumber(decimal value) => value == 0 ? "" : value.ToString("0.##");
private static Color ParseColor(string value, Color fallback) { try { return ColorTranslator.FromHtml(value); } catch { return fallback; } }
}