Files
trakqr/src/TrackApi/TrackQrApi/Features/QRCodes/Services/QRCodeGeneratorService.cs

261 lines
9.7 KiB
C#

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 xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {actualSize} {actualSize}\" width=\"{actualSize}\" height=\"{actualSize}\">");
svg.AppendLine($" <rect width=\"100%\" height=\"100%\" fill=\"{background}\"/>");
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(
$" <circle cx=\"{cx:F2}\" cy=\"{cy:F2}\" r=\"{radius:F2}\" fill=\"{foreground}\"/>");
break;
case "rounded":
var cornerRadius = moduleSize * 0.3f;
svg.AppendLine(
$" <rect x=\"{px + padding / 2:F2}\" y=\"{py + padding / 2:F2}\" width=\"{moduleSize:F2}\" height=\"{moduleSize:F2}\" rx=\"{cornerRadius:F2}\" fill=\"{foreground}\"/>");
break;
case "square":
default:
svg.AppendLine(
$" <rect x=\"{px + padding / 2:F2}\" y=\"{py + padding / 2:F2}\" width=\"{moduleSize:F2}\" height=\"{moduleSize:F2}\" fill=\"{foreground}\"/>");
break;
}
}
svg.AppendLine("</svg>");
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;
}
}