QrRapido/Services/QRRapidoService.cs
Ricardo Carneiro 00d924ce3b
Some checks failed
Deploy QR Rapido / test (push) Successful in 51s
Deploy QR Rapido / build-and-push (push) Failing after 6s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
feat: ajustes qrcode, estilos e cores.
2025-07-30 21:35:28 -03:00

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; }
}
}
}