492 lines
20 KiB
C#
492 lines
20 KiB
C#
using Microsoft.Extensions.Caching.Distributed;
|
|
using QRCoder;
|
|
using QRRapidoApp.Models.ViewModels;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Drawing.Imaging;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace QRRapidoApp.Services
|
|
{
|
|
public class QRRapidoService : IQRCodeService
|
|
{
|
|
private readonly IDistributedCache _cache;
|
|
private readonly IConfiguration _config;
|
|
private readonly ILogger<QRRapidoService> _logger;
|
|
private readonly SemaphoreSlim _semaphore;
|
|
|
|
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger)
|
|
{
|
|
_cache = cache;
|
|
_config = config;
|
|
_logger = logger;
|
|
|
|
// Limit simultaneous generations to maintain performance
|
|
var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100);
|
|
_semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
|
|
}
|
|
|
|
public async Task<QRGenerationResult> GenerateRapidAsync(QRGenerationRequest request)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
await _semaphore.WaitAsync();
|
|
|
|
// Cache key based on content and settings
|
|
var cacheKey = GenerateCacheKey(request);
|
|
var cached = await _cache.GetStringAsync(cacheKey);
|
|
|
|
if (!string.IsNullOrEmpty(cached))
|
|
{
|
|
stopwatch.Stop();
|
|
_logger.LogInformation($"QR code served from cache in {stopwatch.ElapsedMilliseconds}ms");
|
|
|
|
var cachedResult = JsonSerializer.Deserialize<QRGenerationResult>(cached);
|
|
if (cachedResult != null)
|
|
{
|
|
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
|
|
cachedResult.FromCache = true;
|
|
return cachedResult;
|
|
}
|
|
}
|
|
|
|
// Optimized generation
|
|
var qrCode = await GenerateQRCodeOptimizedAsync(request);
|
|
var base64 = Convert.ToBase64String(qrCode);
|
|
|
|
var result = new QRGenerationResult
|
|
{
|
|
QRCodeBase64 = base64,
|
|
QRId = Guid.NewGuid().ToString(),
|
|
GenerationTimeMs = stopwatch.ElapsedMilliseconds,
|
|
FromCache = false,
|
|
Size = qrCode.Length,
|
|
RequestSettings = request,
|
|
Success = true
|
|
};
|
|
|
|
// Cache for configurable time
|
|
var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60));
|
|
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
|
|
{
|
|
AbsoluteExpirationRelativeToNow = cacheExpiration
|
|
});
|
|
|
|
stopwatch.Stop();
|
|
_logger.LogInformation($"QR code generated in {stopwatch.ElapsedMilliseconds}ms for type {request.Type}");
|
|
|
|
return result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Error in rapid QR code generation: {ex.Message}");
|
|
return new QRGenerationResult
|
|
{
|
|
Success = false,
|
|
ErrorMessage = ex.Message,
|
|
GenerationTimeMs = stopwatch.ElapsedMilliseconds
|
|
};
|
|
}
|
|
finally
|
|
{
|
|
_semaphore.Release();
|
|
}
|
|
}
|
|
|
|
private async Task<byte[]> GenerateQRCodeOptimizedAsync(QRGenerationRequest request)
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
using var qrGenerator = new QRCodeGenerator();
|
|
using var qrCodeData = qrGenerator.CreateQrCode(request.Content, GetErrorCorrectionLevel(request));
|
|
|
|
// Optimized settings for speed
|
|
using var qrCode = new PngByteQRCode(qrCodeData);
|
|
|
|
// Apply optimizations based on user type
|
|
var pixelsPerModule = request.IsPremium ?
|
|
GetOptimalPixelsPerModule(request.Size) :
|
|
Math.Max(8, request.Size / 40); // Lower quality for free users, but faster
|
|
|
|
var primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor));
|
|
var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor));
|
|
|
|
var qrBytes = qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes);
|
|
|
|
// Apply custom corner styles for premium users
|
|
if (request.IsPremium && !string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square")
|
|
{
|
|
qrBytes = ApplyCornerStyle(qrBytes, request.CornerStyle, request.Size);
|
|
}
|
|
|
|
// Apply logo overlay if provided
|
|
if (request.HasLogo && request.Logo != null)
|
|
{
|
|
return ApplyLogoOverlay(qrBytes, request.Logo, request.Size);
|
|
}
|
|
|
|
return qrBytes;
|
|
});
|
|
}
|
|
|
|
private QRCodeGenerator.ECCLevel GetErrorCorrectionLevel(QRGenerationRequest request)
|
|
{
|
|
// Lower error correction = faster generation
|
|
if (request.OptimizeForSpeed)
|
|
{
|
|
return QRCodeGenerator.ECCLevel.L; // ~7% correction
|
|
}
|
|
|
|
return request.HasLogo ?
|
|
QRCodeGenerator.ECCLevel.H : // ~30% correction for logos
|
|
QRCodeGenerator.ECCLevel.M; // ~15% correction default
|
|
}
|
|
|
|
private int GetOptimalPixelsPerModule(int targetSize)
|
|
{
|
|
// Optimized algorithm for best quality/speed ratio
|
|
return targetSize switch
|
|
{
|
|
<= 200 => 8,
|
|
<= 300 => 12,
|
|
<= 500 => 16,
|
|
<= 800 => 20,
|
|
_ => 24
|
|
};
|
|
}
|
|
|
|
private string GenerateCacheKey(QRGenerationRequest request)
|
|
{
|
|
var keyData = $"{request.Content}|{request.Type}|{request.Size}|{request.PrimaryColor}|{request.BackgroundColor}|{request.QuickStyle}|{request.CornerStyle}|{request.Margin}";
|
|
using var sha256 = SHA256.Create();
|
|
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
|
|
return $"qr_rapid_{Convert.ToBase64String(hash)[..16]}";
|
|
}
|
|
|
|
public async Task<byte[]> ConvertToSvgAsync(string qrCodeBase64)
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
// Convert PNG to SVG (simplified implementation)
|
|
var svgContent = $@"<?xml version=""1.0"" encoding=""UTF-8""?>
|
|
<svg xmlns=""http://www.w3.org/2000/svg"" viewBox=""0 0 300 300"">
|
|
<rect width=""300"" height=""300"" fill=""white""/>
|
|
<image href=""data:image/png;base64,{qrCodeBase64}"" width=""300"" height=""300""/>
|
|
</svg>";
|
|
return Encoding.UTF8.GetBytes(svgContent);
|
|
});
|
|
}
|
|
|
|
public async Task<byte[]> ConvertToPdfAsync(string qrCodeBase64, int size = 300)
|
|
{
|
|
return await Task.Run(() =>
|
|
{
|
|
// Simplified PDF generation - in real implementation, use iTextSharp or similar
|
|
var pdfHeader = "%PDF-1.4\n";
|
|
var pdfBody = $"1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n" +
|
|
$"2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n" +
|
|
$"3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 {size} {size}]>>endobj\n" +
|
|
$"xref\n0 4\n0000000000 65535 f \n0000000010 00000 n \n0000000053 00000 n \n0000000125 00000 n \n";
|
|
|
|
var pdfContent = pdfBody + $"trailer<</Size 4/Root 1 0 R>>\nstartxref\n{pdfHeader.Length + pdfBody.Length}\n%%EOF";
|
|
|
|
return Encoding.UTF8.GetBytes(pdfHeader + pdfContent);
|
|
});
|
|
}
|
|
|
|
public async Task<string> GenerateDynamicQRAsync(QRGenerationRequest request, string userId)
|
|
{
|
|
// For premium users only - dynamic QR codes that can be edited
|
|
var dynamicId = Guid.NewGuid().ToString();
|
|
var dynamicUrl = $"https://qrrapido.site/d/{dynamicId}";
|
|
|
|
// Store mapping in cache/database
|
|
var cacheKey = $"dynamic_qr_{dynamicId}";
|
|
await _cache.SetStringAsync(cacheKey, request.Content, new DistributedCacheEntryOptions
|
|
{
|
|
SlidingExpiration = TimeSpan.FromDays(365) // Long-lived for premium users
|
|
});
|
|
|
|
return dynamicId;
|
|
}
|
|
|
|
public async Task<bool> UpdateDynamicQRAsync(string qrId, string newContent)
|
|
{
|
|
try
|
|
{
|
|
var cacheKey = $"dynamic_qr_{qrId}";
|
|
await _cache.SetStringAsync(cacheKey, newContent, new DistributedCacheEntryOptions
|
|
{
|
|
SlidingExpiration = TimeSpan.FromDays(365)
|
|
});
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, $"Failed to update dynamic QR {qrId}: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private System.Drawing.Color ParseHtmlColor(string htmlColor)
|
|
{
|
|
if (string.IsNullOrEmpty(htmlColor) || !htmlColor.StartsWith("#"))
|
|
{
|
|
return System.Drawing.Color.Black;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (htmlColor.Length == 7) // #RRGGBB
|
|
{
|
|
var r = Convert.ToByte(htmlColor.Substring(1, 2), 16);
|
|
var g = Convert.ToByte(htmlColor.Substring(3, 2), 16);
|
|
var b = Convert.ToByte(htmlColor.Substring(5, 2), 16);
|
|
return System.Drawing.Color.FromArgb(r, g, b);
|
|
}
|
|
else if (htmlColor.Length == 4) // #RGB
|
|
{
|
|
var r = Convert.ToByte(htmlColor.Substring(1, 1) + htmlColor.Substring(1, 1), 16);
|
|
var g = Convert.ToByte(htmlColor.Substring(2, 1) + htmlColor.Substring(2, 1), 16);
|
|
var b = Convert.ToByte(htmlColor.Substring(3, 1) + htmlColor.Substring(3, 1), 16);
|
|
return System.Drawing.Color.FromArgb(r, g, b);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
return System.Drawing.Color.Black;
|
|
}
|
|
|
|
return System.Drawing.Color.Black;
|
|
}
|
|
|
|
private byte[] ColorToBytes(System.Drawing.Color color)
|
|
{
|
|
return new byte[] { color.R, color.G, color.B };
|
|
}
|
|
|
|
private byte[] ApplyLogoOverlay(byte[] qrBytes, byte[] logoBytes, int qrSize)
|
|
{
|
|
try
|
|
{
|
|
using var qrStream = new MemoryStream(qrBytes);
|
|
using var logoStream = new MemoryStream(logoBytes);
|
|
using var qrImage = new Bitmap(qrStream);
|
|
using var logoImage = new Bitmap(logoStream);
|
|
|
|
// Create a new bitmap to draw on (to avoid modifying the original)
|
|
using var finalImage = new Bitmap(qrImage.Width, qrImage.Height);
|
|
using var graphics = Graphics.FromImage(finalImage);
|
|
|
|
// Set high quality rendering
|
|
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
|
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
|
|
|
// Draw the QR code as base
|
|
graphics.DrawImage(qrImage, 0, 0, qrImage.Width, qrImage.Height);
|
|
|
|
// Calculate logo size (20% of QR code size)
|
|
var logoSize = Math.Min(qrImage.Width, qrImage.Height) / 5;
|
|
|
|
// Calculate center position
|
|
var logoX = (qrImage.Width - logoSize) / 2;
|
|
var logoY = (qrImage.Height - logoSize) / 2;
|
|
|
|
// Create a white background circle for better contrast
|
|
var backgroundSize = logoSize + 10; // Slightly larger than logo
|
|
var backgroundX = (qrImage.Width - backgroundSize) / 2;
|
|
var backgroundY = (qrImage.Height - backgroundSize) / 2;
|
|
|
|
using var whiteBrush = new SolidBrush(System.Drawing.Color.White);
|
|
graphics.FillEllipse(whiteBrush, backgroundX, backgroundY, backgroundSize, backgroundSize);
|
|
|
|
// Draw the logo
|
|
graphics.DrawImage(logoImage, logoX, logoY, logoSize, logoSize);
|
|
|
|
// Convert back to byte array
|
|
using var outputStream = new MemoryStream();
|
|
finalImage.Save(outputStream, ImageFormat.Png);
|
|
return outputStream.ToArray();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error applying logo overlay, returning original QR code");
|
|
// Return original QR code if logo overlay fails
|
|
return qrBytes;
|
|
}
|
|
}
|
|
|
|
private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize)
|
|
{
|
|
try
|
|
{
|
|
using var originalStream = new MemoryStream(qrBytes);
|
|
using var originalImage = new Bitmap(originalStream);
|
|
using var styledImage = new Bitmap(originalImage.Width, originalImage.Height);
|
|
using var graphics = Graphics.FromImage(styledImage);
|
|
|
|
// Set high quality rendering
|
|
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
|
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
|
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
|
|
|
// Fill with background color first
|
|
graphics.Clear(System.Drawing.Color.White);
|
|
|
|
// Analyze the QR code to identify modules
|
|
var moduleSize = DetectModuleSize(originalImage);
|
|
var modules = ExtractQRModules(originalImage, moduleSize);
|
|
|
|
// Draw modules with custom style
|
|
foreach (var module in modules)
|
|
{
|
|
if (module.IsBlack)
|
|
{
|
|
DrawStyledModule(graphics, module, cornerStyle, moduleSize);
|
|
}
|
|
}
|
|
|
|
// Convert back to byte array
|
|
using var outputStream = new MemoryStream();
|
|
styledImage.Save(outputStream, ImageFormat.Png);
|
|
return outputStream.ToArray();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error applying corner style {CornerStyle}, returning original QR code", cornerStyle);
|
|
// Return original QR code if styling fails
|
|
return qrBytes;
|
|
}
|
|
}
|
|
|
|
private int DetectModuleSize(Bitmap qrImage)
|
|
{
|
|
// Simple detection: scan from top-left to find first module boundaries
|
|
// QR codes typically have quiet zones, so we look for the first black pixel pattern
|
|
for (int size = 4; size <= 20; size++)
|
|
{
|
|
if (IsValidModuleSize(qrImage, size))
|
|
{
|
|
return size;
|
|
}
|
|
}
|
|
return 8; // Fallback
|
|
}
|
|
|
|
private bool IsValidModuleSize(Bitmap image, int moduleSize)
|
|
{
|
|
// Simple validation: check if the suspected module size creates a reasonable grid
|
|
var expectedModules = image.Width / moduleSize;
|
|
return expectedModules >= 21 && expectedModules <= 177 && (expectedModules % 4 == 1);
|
|
}
|
|
|
|
private List<QRModule> ExtractQRModules(Bitmap image, int moduleSize)
|
|
{
|
|
var modules = new List<QRModule>();
|
|
var modulesPerRow = image.Width / moduleSize;
|
|
|
|
for (int row = 0; row < modulesPerRow; row++)
|
|
{
|
|
for (int col = 0; col < modulesPerRow; col++)
|
|
{
|
|
var x = col * moduleSize + moduleSize / 2;
|
|
var y = row * moduleSize + moduleSize / 2;
|
|
|
|
if (x < image.Width && y < image.Height)
|
|
{
|
|
var pixel = image.GetPixel(x, y);
|
|
var isBlack = pixel.R < 128; // Simple threshold
|
|
|
|
modules.Add(new QRModule
|
|
{
|
|
X = col * moduleSize,
|
|
Y = row * moduleSize,
|
|
Size = moduleSize,
|
|
IsBlack = isBlack
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return modules;
|
|
}
|
|
|
|
private void DrawStyledModule(Graphics graphics, QRModule module, string style, int moduleSize)
|
|
{
|
|
using var brush = new SolidBrush(System.Drawing.Color.Black);
|
|
|
|
switch (style.ToLower())
|
|
{
|
|
case "rounded":
|
|
// Draw rounded rectangles
|
|
var roundedRect = new Rectangle(module.X, module.Y, moduleSize, moduleSize);
|
|
using (var path = CreateRoundedRectPath(roundedRect, moduleSize / 4))
|
|
{
|
|
graphics.FillPath(brush, path);
|
|
}
|
|
break;
|
|
|
|
case "circle":
|
|
case "dots":
|
|
// Draw circles/dots
|
|
var margin = moduleSize / 6;
|
|
graphics.FillEllipse(brush,
|
|
module.X + margin, module.Y + margin,
|
|
moduleSize - 2 * margin, moduleSize - 2 * margin);
|
|
break;
|
|
|
|
case "leaf":
|
|
// Draw leaf-shaped modules (rounded on one side)
|
|
DrawLeafModule(graphics, brush, module, moduleSize);
|
|
break;
|
|
|
|
default:
|
|
// Square (fallback)
|
|
graphics.FillRectangle(brush, module.X, module.Y, moduleSize, moduleSize);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private System.Drawing.Drawing2D.GraphicsPath CreateRoundedRectPath(Rectangle rect, int radius)
|
|
{
|
|
var path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90);
|
|
path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90);
|
|
path.AddArc(rect.Right - radius * 2, rect.Bottom - radius * 2, radius * 2, radius * 2, 0, 90);
|
|
path.AddArc(rect.X, rect.Bottom - radius * 2, radius * 2, radius * 2, 90, 90);
|
|
path.CloseFigure();
|
|
return path;
|
|
}
|
|
|
|
private void DrawLeafModule(Graphics graphics, SolidBrush brush, QRModule module, int moduleSize)
|
|
{
|
|
// Create a path that looks like a leaf (rounded on top-right, square elsewhere)
|
|
using var path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
var rect = new Rectangle(module.X, module.Y, moduleSize, moduleSize);
|
|
var radius = moduleSize / 3;
|
|
|
|
// Start from top-left, go clockwise
|
|
path.AddLine(rect.X, rect.Y, rect.Right - radius, rect.Y);
|
|
path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90);
|
|
path.AddLine(rect.Right, rect.Y + radius, rect.Right, rect.Bottom);
|
|
path.AddLine(rect.Right, rect.Bottom, rect.X, rect.Bottom);
|
|
path.AddLine(rect.X, rect.Bottom, rect.X, rect.Y);
|
|
path.CloseFigure();
|
|
|
|
graphics.FillPath(brush, path);
|
|
}
|
|
|
|
private class QRModule
|
|
{
|
|
public int X { get; set; }
|
|
public int Y { get; set; }
|
|
public int Size { get; set; }
|
|
public bool IsBlack { get; set; }
|
|
}
|
|
}
|
|
} |