using System.Text; using QRCoder; using SkiaSharp; using TrackQrApi.Features.QRCodes.Common; namespace TrackQrApi.Features.QRCodes.Services; public interface IQrCodeGeneratorService { byte[] GeneratePng(string content, QRCodeStyle style, int size = 512, Stream? logoStream = null); string GenerateSvg(string content, QRCodeStyle style, int size = 512); string GenerateDataUrl(string content, QRCodeStyle style, int size = 256, Stream? logoStream = null); } public class QrCodeGeneratorService : IQrCodeGeneratorService { public byte[] GeneratePng(string content, QRCodeStyle style, int size = 512, Stream? logoStream = null) { using var qrGenerator = new QRCodeGenerator(); var eccLevel = ParseEccLevel(style.ErrorCorrectionLevel); using var qrCodeData = qrGenerator.CreateQrCode(content, eccLevel); var moduleMatrix = qrCodeData.ModuleMatrix; var moduleCount = moduleMatrix.Count; // Calculate pixels per module based on desired size (accounting for quiet zone) var totalModules = moduleCount + style.QuietZone * 2; var pixelsPerModule = Math.Max(4, size / totalModules); var actualSize = totalModules * pixelsPerModule; // Create bitmap with SkiaSharp for custom shapes var foregroundColor = ParseSkColor(style.ForegroundColor); var backgroundColor = ParseSkColor(style.BackgroundColor); using var surface = SKSurface.Create(new SKImageInfo(actualSize, actualSize)); var canvas = surface.Canvas; // Draw background canvas.Clear(backgroundColor); // Draw QR modules with custom shapes var modulePaint = new SKPaint { Color = foregroundColor, IsAntialias = true, Style = SKPaintStyle.Fill }; var quietZoneOffset = style.QuietZone * pixelsPerModule; for (var y = 0; y < moduleCount; y++) for (var x = 0; x < moduleCount; x++) if (moduleMatrix[y][x]) { var px = quietZoneOffset + x * pixelsPerModule; var py = quietZoneOffset + y * pixelsPerModule; // Check if this is part of a finder pattern (eyes) var isEye = IsFinderPattern(x, y, moduleCount); if (isEye) DrawModule(canvas, px, py, pixelsPerModule, modulePaint, style.EyeShape); else DrawModule(canvas, px, py, pixelsPerModule, modulePaint, style.ModuleShape); } // Encode to PNG using var image = surface.Snapshot(); using var data = image.Encode(SKEncodedImageFormat.Png, 100); var qrBytes = data.ToArray(); // If no logo, return the QR code as-is if (logoStream == null) return qrBytes; // Overlay logo on QR code return OverlayLogo(qrBytes, logoStream, actualSize); } public string GenerateSvg(string content, QRCodeStyle style, int size = 512) { using var qrGenerator = new QRCodeGenerator(); var eccLevel = ParseEccLevel(style.ErrorCorrectionLevel); using var qrCodeData = qrGenerator.CreateQrCode(content, eccLevel); var moduleMatrix = qrCodeData.ModuleMatrix; var moduleCount = moduleMatrix.Count; // Calculate pixels per module based on desired size (accounting for quiet zone) var totalModules = moduleCount + style.QuietZone * 2; var pixelsPerModule = (float)size / totalModules; var actualSize = size; var foreground = style.ForegroundColor; var background = style.BackgroundColor; var svg = new StringBuilder(); svg.AppendLine( $""); svg.AppendLine($" "); var quietZoneOffset = style.QuietZone * pixelsPerModule; for (var y = 0; y < moduleCount; y++) for (var x = 0; x < moduleCount; x++) if (moduleMatrix[y][x]) { var px = quietZoneOffset + x * pixelsPerModule; var py = quietZoneOffset + y * pixelsPerModule; var isEye = IsFinderPattern(x, y, moduleCount); var shape = isEye ? style.EyeShape : style.ModuleShape; var padding = pixelsPerModule * 0.1f; var moduleSize = pixelsPerModule - padding; switch (shape.ToLowerInvariant()) { case "circle": case "dots": var radius = moduleSize / 2; var cx = px + pixelsPerModule / 2; var cy = py + pixelsPerModule / 2; svg.AppendLine( $" "); break; case "rounded": var cornerRadius = moduleSize * 0.3f; svg.AppendLine( $" "); break; case "square": default: svg.AppendLine( $" "); break; } } svg.AppendLine(""); return svg.ToString(); } public string GenerateDataUrl(string content, QRCodeStyle style, int size = 256, Stream? logoStream = null) { var pngBytes = GeneratePng(content, style, size, logoStream); var base64 = Convert.ToBase64String(pngBytes); return $"data:image/png;base64,{base64}"; } private static bool IsFinderPattern(int x, int y, int moduleCount) { // Top-left finder pattern: 0-6, 0-6 if (x <= 6 && y <= 6) return true; // Top-right finder pattern: moduleCount-7 to moduleCount-1, 0-6 if (x >= moduleCount - 7 && y <= 6) return true; // Bottom-left finder pattern: 0-6, moduleCount-7 to moduleCount-1 if (x <= 6 && y >= moduleCount - 7) return true; return false; } private static void DrawModule(SKCanvas canvas, float x, float y, float size, SKPaint paint, string shape) { var padding = size * 0.1f; // 10% padding between modules var moduleSize = size - padding; switch (shape.ToLowerInvariant()) { case "circle": case "dots": var radius = moduleSize / 2; canvas.DrawCircle(x + size / 2, y + size / 2, radius, paint); break; case "rounded": var cornerRadius = moduleSize * 0.3f; var rect = new SKRoundRect( new SKRect(x + padding / 2, y + padding / 2, x + size - padding / 2, y + size - padding / 2), cornerRadius ); canvas.DrawRoundRect(rect, paint); break; case "square": default: canvas.DrawRect(x + padding / 2, y + padding / 2, moduleSize, moduleSize, paint); break; } } private static byte[] OverlayLogo(byte[] qrBytes, Stream logoStream, int qrSize) { using var qrBitmap = SKBitmap.Decode(qrBytes); using var logoBitmap = SKBitmap.Decode(logoStream); if (qrBitmap == null || logoBitmap == null) return qrBytes; // Logo should be about 20% of QR code size var logoSize = (int)(qrSize * 0.2); var logoX = (qrBitmap.Width - logoSize) / 2; var logoY = (qrBitmap.Height - logoSize) / 2; // Create a new surface to draw on using var surface = SKSurface.Create(new SKImageInfo(qrBitmap.Width, qrBitmap.Height)); var canvas = surface.Canvas; // Draw QR code canvas.DrawBitmap(qrBitmap, 0, 0); // Draw white background circle for logo var circlePaint = new SKPaint { Color = SKColors.White, IsAntialias = true, Style = SKPaintStyle.Fill }; var circleRadius = logoSize * 0.6f; canvas.DrawCircle(qrBitmap.Width / 2f, qrBitmap.Height / 2f, circleRadius, circlePaint); // Resize and draw logo using var resizedLogo = logoBitmap.Resize( new SKImageInfo(logoSize, logoSize), new SKSamplingOptions(SKCubicResampler.Mitchell)); if (resizedLogo != null) canvas.DrawBitmap(resizedLogo, logoX, logoY); // Encode to PNG using var image = surface.Snapshot(); using var data = image.Encode(SKEncodedImageFormat.Png, 100); return data.ToArray(); } private static QRCodeGenerator.ECCLevel ParseEccLevel(string level) { return level.ToUpperInvariant() switch { "L" => QRCodeGenerator.ECCLevel.L, "M" => QRCodeGenerator.ECCLevel.M, "Q" => QRCodeGenerator.ECCLevel.Q, "H" => QRCodeGenerator.ECCLevel.H, _ => QRCodeGenerator.ECCLevel.M }; } private static SKColor ParseSkColor(string hexColor) { // Remove # if present var hex = hexColor.TrimStart('#'); if (hex.Length == 6) { var r = Convert.ToByte(hex[..2], 16); var g = Convert.ToByte(hex[2..4], 16); var b = Convert.ToByte(hex[4..6], 16); return new SKColor(r, g, b); } // Default to black return SKColors.Black; } }