chore: correct namespaces and hiearchy
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user