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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
| public static class UpiUriBuilder
{
/// <summary>
/// Builds a properly formatted UPI URI string from payment information.
/// The URI follows the UPI specification for mobile payments in India.
/// </summary>
/// <param name="info">Payment information containing VPA, amount, and other details</param>
/// <returns>A formatted UPI URI string (e.g., upi://pay?pa=...&pn=...)</returns>
public static string Build(UpiPaymentInfo info)
{
// Build query parameters with percent-encoding for special characters
var query = new List<string>();
if (!string.IsNullOrWhiteSpace(info.Upid))
query.Add($"pa={UrlEncode(info.Upid)}");
if (!string.IsNullOrWhiteSpace(info.PayeeName))
query.Add($"pn={UrlEncode(info.PayeeName)}");
if (info.Amount.HasValue)
query.Add($"am={info.Amount.Value.ToString("0.00")}");
if (!string.IsNullOrWhiteSpace(info.Note))
query.Add($"tn={UrlEncode(info.Note)}");
if (!string.IsNullOrWhiteSpace(info.TransactionId))
query.Add($"tr={UrlEncode(info.TransactionId)}");
if (!string.IsNullOrWhiteSpace(info.MerchantCode))
query.Add($"mc={UrlEncode(info.MerchantCode)}");
// Currency defaults to INR as per UPI specification
query.Add("cu=INR");
string q = string.Join("&", query);
return $"upi://pay?{q}";
}
/// <summary>
/// URL encodes a string using percent-encoding for safe transmission in query parameters.
/// </summary>
/// <param name="value">The string to encode</param>
/// <returns>The percent-encoded string</returns>
private static string UrlEncode(string value)
{
// Uri.EscapeDataString is the safe method for encoding query string components
return Uri.EscapeDataString(value ?? string.Empty);
}
}
public class UpiPaymentInfo
{
/// <summary>
/// Virtual Payment Address (VPA) - unique identifier for UPI transactions
/// </summary>
public string Upid { get; set; }
/// <summary>
/// Name of the payment recipient
/// </summary>
public string PayeeName { get; set; }
/// <summary>
/// Transaction amount in INR (optional)
/// </summary>
public decimal? Amount { get; set; }
/// <summary>
/// Transaction note or description (optional)
/// </summary>
public string Note { get; set; }
/// <summary>
/// Unique transaction reference ID (optional)
/// </summary>
public string TransactionId { get; set; }
/// <summary>
/// Merchant category code (optional)
/// </summary>
public string MerchantCode { get; set; }
/// <summary>
/// Payment due date (optional)
/// </summary>
public DateTime? DueDate { get; set; }
}
public static class FancyQrGenerator
{
/// <summary>
/// Generates a fancy QR code with gradient coloring, rounded dots, optional center logo, and footer text.
/// </summary>
/// <param name="qrText">The text or URI to encode in the QR code</param>
/// <param name="logoStream">Optional image stream for center logo (PNG, JPG supported)</param>
/// <param name="footerText">Optional text to display below the QR code</param>
/// <param name="size">Canvas size in pixels (default 600px)</param>
/// <returns>PNG image as Base64-encoded string for easy embedding</returns>
public static string GenerateBase64(
string qrText,
Stream logoStream = null,
string footerText = null,
int size = 600)
{
int paddingForText = 80;
int canvasHeight = size + paddingForText;
// Generate QR code pixel data with high error correction level
// This allows up to 30% of the QR code to be obscured and still readable
var writer = new BarcodeWriterPixelData
{
Format = BarcodeFormat.QR_CODE,
Options = new QrCodeEncodingOptions
{
Width = size,
Height = size,
Margin = 4, // quiet zone in modules (standard is 1-4 modules)
ErrorCorrection = ZXing.QrCode.Internal.ErrorCorrectionLevel.H
}
};
PixelData pixelData = writer.Write(qrText);
// Convert pixel bitmap to module matrix for precise rendering
var moduleMatrix = PixelDataToModuleMatrix(pixelData, out int moduleCount, out int modulePixelSize);
// Create Skia surface for drawing
using var surface = SKSurface.Create(new SKImageInfo(size, canvasHeight));
var canvas = surface.Canvas;
canvas.Clear(SKColors.White);
// Create gradient shader (purple to cyan) for visual appeal
var shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
new SKPoint(size, size),
new SKColor[] { SKColor.Parse("#5A00FF"), SKColor.Parse("#00C2FF") },
null,
SKShaderTileMode.Clamp);
var gradientPaint = new SKPaint
{
IsAntialias = true,
Shader = shader
};
float cellSize = size / (float)moduleCount;
float radius = cellSize * 0.45f; // radius of rounded dots
// Detect margin modules (quiet zone) by finding first dark module
int minX = moduleCount, minY = moduleCount;
for (int y = 0; y < moduleCount; y++)
for (int x = 0; x < moduleCount; x++)
if (moduleMatrix[x, y])
{
if (x < minX) minX = x;
if (y < minY) minY = y;
}
int marginModules = Math.Min(minX, minY);
// QR code finder patterns are 7x7 modules in the three corners
int finderSize = 7;
var finders = new (int fx, int fy)[]
{
(marginModules, marginModules), // top-left
(moduleCount - marginModules - finderSize, marginModules), // top-right
(marginModules, moduleCount - marginModules - finderSize) // bottom-left
};
// Paint for crisp finder squares (no antialiasing for clean edges)
var squarePaint = new SKPaint { IsAntialias = false, Shader = shader };
// Draw all modules - finder patterns as squares, data modules as circles
for (int my = 0; my < moduleCount; my++)
{
for (int mx = 0; mx < moduleCount; mx++)
{
if (!moduleMatrix[mx, my]) continue;
// Check if module is part of a finder pattern
bool inFinder = finders.Any(f => mx >= f.fx && mx < f.fx + finderSize && my >= f.fy && my < f.fy + finderSize);
float left = mx * cellSize;
float top = my * cellSize;
if (inFinder)
{
// Finder patterns remain as squares for scanning accuracy
canvas.DrawRect(new SKRect(left, top, left + cellSize, top + cellSize), squarePaint);
}
else
{
// Regular data modules drawn as rounded circles
float cx = left + cellSize / 2f;
float cy = top + cellSize / 2f;
canvas.DrawCircle(cx, cy, radius, gradientPaint);
}
}
}
// Draw center logo if provided
if (logoStream != null)
{
if (logoStream.CanSeek) logoStream.Seek(0, SeekOrigin.Begin);
SKBitmap logoBitmap = null;
try
{
logoBitmap = SKBitmap.Decode(logoStream);
}
catch
{
// Silently ignore decode errors; logoBitmap remains null
}
if (logoBitmap != null)
{
int logoSize = Math.Max(16, size / 5); // approximately 20% of QR width
using var resized = logoBitmap.Resize(new SKImageInfo(logoSize, logoSize), SKSamplingOptions.Default);
float lx = (size - logoSize) / 2f;
float ly = (size - logoSize) / 2f;
// Draw white rounded background for logo contrast
var bgPaint = new SKPaint { Color = SKColors.White, IsAntialias = true };
canvas.DrawRoundRect(new SKRect(lx - 8, ly - 8, lx + logoSize + 8, ly + logoSize + 8), 20, 20, bgPaint);
if (resized != null)
canvas.DrawBitmap(resized, lx, ly);
else
canvas.DrawBitmap(logoBitmap, (size - logoBitmap.Width) / 2f, (size - logoBitmap.Height) / 2f);
}
}
// Draw footer text below QR code
if (!string.IsNullOrWhiteSpace(footerText))
{
var font = new SKFont(SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold), 36);
var textPaint = new SKPaint
{
IsAntialias = true,
Color = SKColors.Black,
};
textPaint.TextAlign = SKTextAlign.Center;
float textY = size + (paddingForText / 2f) + (font.Size / 2f) - 6;
canvas.DrawText(footerText, size / 2f, textY, font, textPaint);
}
// Encode to PNG and return as Base64
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
byte[] bytes = data.ToArray();
return Convert.ToBase64String(bytes);
}
/// <summary>
/// Converts ZXing PixelData bitmap to a boolean module matrix for rendering.
/// Detects the module count and pixel size automatically.
/// </summary>
/// <param name="pd">PixelData from ZXing barcode writer</param>
/// <param name="moduleCount">Output: number of modules per row/column</param>
/// <param name="modulePixelSize">Output: pixels per module</param>
/// <returns>2D boolean array where true = dark module, false = light module</returns>
private static bool[,] PixelDataToModuleMatrix(PixelData pd, out int moduleCount, out int modulePixelSize)
{
int w = pd.Width;
int h = pd.Height;
var pixels = pd.Pixels;
// Helper function to determine if a pixel is dark (luminance < 128)
bool IsDark(int x, int y)
{
int idx = (y * w + x) * 4;
if (idx + 2 >= pixels.Length) return false;
byte r = pixels[idx], g = pixels[idx + 1], b = pixels[idx + 2];
int lum = (r * 299 + g * 587 + b * 114) / 1000;
return lum < 128;
}
// Analyze central row to determine module pixel size by finding run lengths
int row = h / 2;
var rowB = new bool[w];
for (int x = 0; x < w; x++) rowB[x] = IsDark(x, row);
var runs = new List<int>();
int run = 1;
for (int x = 1; x < w; x++)
{
if (rowB[x] == rowB[x - 1]) run++;
else { runs.Add(run); run = 1; }
}
runs.Add(run);
modulePixelSize = runs.Count > 0 ? Math.Max(1, runs.Min()) : 1;
moduleCount = Math.Max(1, w / modulePixelSize);
// Create module matrix by sampling center of each module
var matrix = new bool[moduleCount, moduleCount];
for (int my = 0; my < moduleCount; my++)
{
for (int mx = 0; mx < moduleCount; mx++)
{
int sx = mx * modulePixelSize + modulePixelSize / 2;
int sy = my * modulePixelSize + modulePixelSize / 2;
if (sx >= w) sx = Math.Max(0, mx * modulePixelSize);
if (sy >= h) sy = Math.Max(0, my * modulePixelSize);
matrix[mx, my] = IsDark(sx, sy);
}
}
return matrix;
}
}
|