QrRapido/Services/QRRapidoService.cs
Ricardo Carneiro 2ccd35bb7d
Some checks failed
Deploy QR Rapido / test (push) Successful in 4m58s
Deploy QR Rapido / build-and-push (push) Failing after 1m39s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
first commit
2025-07-28 11:46:48 -03:00

257 lines
10 KiB
C#

using Microsoft.Extensions.Caching.Distributed;
using QRCoder;
using QRRapidoApp.Models.ViewModels;
using System.Diagnostics;
using System.Drawing;
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));
return qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes);
});
}
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 };
}
}
}