261 lines
9.7 KiB
C#
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;
|
|
}
|
|
} |