using Microsoft.Extensions.Caching.Distributed; using QRCoder; using QRRapidoApp.Models.ViewModels; using System.Diagnostics; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Drawing.Processing; using System.Numerics; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.Extensions.Localization; namespace QRRapidoApp.Services { public class QRRapidoService : IQRCodeService { private readonly IDistributedCache _cache; private readonly IConfiguration _config; private readonly ILogger _logger; private readonly SemaphoreSlim _semaphore; private readonly LogoReadabilityAnalyzer _readabilityAnalyzer; private readonly IStringLocalizer _localizer; public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger logger, LogoReadabilityAnalyzer readabilityAnalyzer, IStringLocalizer localizer) { _cache = cache; _config = config; _logger = logger; _readabilityAnalyzer = readabilityAnalyzer; _localizer = localizer; // Limit simultaneous generations to maintain performance var maxConcurrent = _config.GetValue("Performance:MaxConcurrentGenerations", 100); _semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent); } public async Task GenerateRapidAsync(QRGenerationRequest request) { var stopwatch = Stopwatch.StartNew(); try { await _semaphore.WaitAsync(); // Generate tracking ID for premium users with tracking enabled string? trackingId = null; string originalContent = request.Content; // DEBUG: Log all conditions _logger.LogInformation("[TRACKING DEBUG] IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}", request.IsPremium, request.EnableTracking, request.Type); if (request.IsPremium && request.EnableTracking && request.Type == "url") { // Only URL type QR codes can use tracking (redirect functionality) trackingId = Guid.NewGuid().ToString("N").Substring(0, 12); // 12-char tracking ID var baseUrl = _config.GetValue("App:BaseUrl", "https://qrrapido.site"); request.Content = $"{baseUrl}/t/{trackingId}"; _logger.LogInformation("✅ Tracking enabled for QR - TrackingId: {TrackingId}, OriginalURL: {OriginalURL}", trackingId, originalContent); } else { _logger.LogWarning("❌ Tracking NOT enabled - IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}", request.IsPremium, request.EnableTracking, request.Type); } // 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(cached); if (cachedResult != null) { cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds; cachedResult.FromCache = true; // Se não tem análise de legibilidade no cache, gerar agora if (cachedResult.ReadabilityInfo == null) { cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size); } return cachedResult; } } // Optimized generation var qrCode = await GenerateQRCodeOptimizedAsync(request); var base64 = Convert.ToBase64String(qrCode); // Análise de legibilidade do logo var readabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size); var result = new QRGenerationResult { QRCodeBase64 = base64, QRId = Guid.NewGuid().ToString(), GenerationTimeMs = stopwatch.ElapsedMilliseconds, FromCache = false, Size = qrCode.Length, RequestSettings = request, Success = true, ReadabilityInfo = readabilityInfo, TrackingId = trackingId // Include tracking ID if enabled }; // Restore original content if tracking was enabled if (trackingId != null) { request.Content = originalContent; } // Cache for configurable time var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue("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 GenerateQRCodeOptimizedAsync(QRGenerationRequest request) { return await Task.Run(() => { using var qrGenerator = new QRCodeGenerator(); // CORREÇÃO 1: Usar nível de correção HIGH para logos var errorCorrectionLevel = request.HasLogo && request.Logo != null ? QRCodeGenerator.ECCLevel.H : // 30% correção para logos QRCodeGenerator.ECCLevel.M; // 15% correção normal using var qrCodeData = qrGenerator.CreateQrCode(request.Content, errorCorrectionLevel); // CORREÇÃO 2: Usar PngByteQRCode para compatibilidade using var qrCode = new PngByteQRCode(qrCodeData); var primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor)); var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor)); var pixelsPerModule = request.IsPremium ? GetOptimalPixelsPerModule(request.Size) : Math.Max(8, request.Size / 40); byte[] qrBytes; // CORREÇÃO 3: Para logos, usar implementação otimizada com alta correção de erro if (request.HasLogo && request.Logo != null) { try { // Validar logo primeiro if (!ValidateLogoForQR(request.Logo, out string errorMessage)) { _logger.LogWarning("Logo validation failed: {ErrorMessage}, generating QR without logo", errorMessage); // Fallback para QR sem logo qrBytes = qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes); } else { // NOVO: Preprocessar logo (redimensionar se necessário) var processedLogo = PreprocessLogo(request.Logo); // Gerar QR base com alta correção de erro qrBytes = qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes); // CORREÇÃO 4: Aplicar logo processado com configurações aprimoradas qrBytes = ApplyLogoOverlayEnhanced(qrBytes, processedLogo, request); _logger.LogInformation("QR code with logo generated - Size: {LogoSize}%, Colorized: {IsColorized}", request.LogoSizePercent ?? 20, request.ApplyLogoColorization); } } catch (Exception ex) { _logger.LogError(ex, "Error applying logo, falling back to no logo"); // Fallback para QR sem logo se der erro qrBytes = qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes); } } else { // QR sem logo - usar correção padrão qrBytes = qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes); } // CORREÇÃO 5: Aplicar estilos de canto APÓS o logo (se necessário) if (request.IsPremium && !string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square") { qrBytes = ApplyCornerStyle(qrBytes, request.CornerStyle, request.Size); } return qrBytes; }); } private QRCodeGenerator.ECCLevel GetErrorCorrectionLevel(QRGenerationRequest request) { // Para logos, sempre usar correção HIGH if (request.HasLogo && request.Logo != null) { _logger.LogInformation("Using HIGH error correction level for logo QR code"); return QRCodeGenerator.ECCLevel.H; // 30% correção } // Para velocidade, usar correção baixa apenas se especificado if (request.OptimizeForSpeed) { return QRCodeGenerator.ECCLevel.L; // 7% correção } // Padrão: correção média return QRCodeGenerator.ECCLevel.M; // 15% correção } // CORREÇÃO 8: Método de validação e processamento de logo aprimorado private bool ValidateLogoForQR(byte[] logoBytes, out string errorMessage) { errorMessage = ""; if (logoBytes == null || logoBytes.Length == 0) { errorMessage = _localizer["LogoNotProvided"]; return false; } // Validar tamanho máximo if (logoBytes.Length > 2 * 1024 * 1024) // 2MB { errorMessage = _localizer["LogoTooLarge"]; return false; } try { using var stream = new MemoryStream(logoBytes); using var image = Image.Load(stream); // Validar dimensões mínimas if (image.Width < 32 || image.Height < 32) { errorMessage = _localizer["LogoTooSmall"]; return false; } // Log das dimensões originais _logger.LogInformation("Logo original dimensions: {Width}x{Height} pixels", image.Width, image.Height); return true; } catch (Exception ex) { errorMessage = _localizer["InvalidImageFormat"]; _logger.LogError(ex, "Error validating logo format"); return false; } } // NOVO MÉTODO: Preprocessar logo para otimizar tamanho private byte[] PreprocessLogo(byte[] originalLogoBytes) { try { using var stream = new MemoryStream(originalLogoBytes); using var image = Image.Load(stream); // Se a largura for maior que 400px, redimensionar proporcionalmente if (image.Width > 400) { var aspectRatio = (double)image.Height / image.Width; var newWidth = 400; var newHeight = (int)(newWidth * aspectRatio); _logger.LogInformation("Redimensionando logo de {OriginalWidth}x{OriginalHeight} para {NewWidth}x{NewHeight}", image.Width, image.Height, newWidth, newHeight); using var resizedImage = image.Clone(ctx => ctx.Resize(new ResizeOptions { Size = new Size(newWidth, newHeight), Mode = ResizeMode.Stretch, Sampler = KnownResamplers.Lanczos3 // Alta qualidade })); using var outputStream = new MemoryStream(); // Preservar formato e cores originais ao redimensionar resizedImage.Save(outputStream, new PngEncoder { ColorType = PngColorType.RgbWithAlpha, CompressionLevel = PngCompressionLevel.DefaultCompression }); var resizedBytes = outputStream.ToArray(); _logger.LogInformation("Logo redimensionado: {OriginalSize} -> {NewSize} bytes", originalLogoBytes.Length, resizedBytes.Length); return resizedBytes; } _logger.LogDebug("Logo não precisa ser redimensionado: {Width}x{Height}", image.Width, image.Height); return originalLogoBytes; } catch (Exception ex) { _logger.LogError(ex, "Erro ao preprocessar logo, usando original"); return originalLogoBytes; } } 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) { // CORREÇÃO DE CACHE: Incluir parâmetros de logo na chave de cache var logoHash = ""; if (request.HasLogo && request.Logo != null && request.Logo.Length > 0) { // Usar logo processado (redimensionado se necessário) para o hash do cache var processedLogo = PreprocessLogo(request.Logo); using var logoSha = SHA256.Create(); var logoHashBytes = logoSha.ComputeHash(processedLogo); logoHash = Convert.ToBase64String(logoHashBytes)[..16]; // Mais chars para evitar colisões _logger.LogDebug("Logo hash generated: {LogoHash} for processed logo size: {LogoSize} bytes", logoHash, processedLogo.Length); } else { logoHash = "no_logo"; _logger.LogDebug("Using 'no_logo' hash for request without logo"); } var keyData = $"{request.Content}|{request.Type}|{request.Size}|{request.PrimaryColor}|{request.BackgroundColor}|{request.QuickStyle}|{request.CornerStyle}|{request.Margin}|{request.HasLogo}|{logoHash}|{request.LogoSizePercent ?? 20}|{request.ApplyLogoColorization}"; using var sha256 = SHA256.Create(); var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData)); var cacheKey = $"qr_rapid_{Convert.ToBase64String(hash)[..16]}"; _logger.LogDebug("Generated cache key: {CacheKey} for content: {Content}", cacheKey, request.Content[..Math.Min(20, request.Content.Length)]); return cacheKey; } public async Task ConvertToSvgAsync(string qrCodeBase64) { return await Task.Run(() => { // Convert PNG to SVG (simplified implementation) var svgContent = $@" "; return Encoding.UTF8.GetBytes(svgContent); }); } public async Task 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<>endobj\n" + $"2 0 obj<>endobj\n" + $"3 0 obj<>endobj\n" + $"xref\n0 4\n0000000000 65535 f \n0000000010 00000 n \n0000000053 00000 n \n0000000125 00000 n \n"; var pdfContent = pdfBody + $"trailer<>\nstartxref\n{pdfHeader.Length + pdfBody.Length}\n%%EOF"; return Encoding.UTF8.GetBytes(pdfHeader + pdfContent); }); } public async Task 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 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 Color ParseHtmlColor(string htmlColor) { if (string.IsNullOrEmpty(htmlColor) || !htmlColor.StartsWith("#")) { return 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 Color.FromRgb(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 Color.FromRgb(r, g, b); } } catch { return Color.Black; } return Color.Black; } private byte[] ColorToBytes(Color color) { var rgba = color.ToPixel(); return new byte[] { rgba.R, rgba.G, rgba.B }; } // CORREÇÃO 7: Método ApplyLogoOverlay aprimorado com configurações avançadas private byte[] ApplyLogoOverlayEnhanced(byte[] qrBytes, byte[] logoBytes, QRGenerationRequest request) { _logger.LogDebug("Starting logo overlay - QR size: {QRSize} bytes, Logo size: {LogoSize} bytes", qrBytes.Length, logoBytes.Length); try { using var qrStream = new MemoryStream(qrBytes); using var logoStream = new MemoryStream(logoBytes); _logger.LogDebug("Creating QR image from stream"); using var qrImage = Image.Load(qrStream); _logger.LogDebug("Creating logo image from stream - QR dimensions: {QRWidth}x{QRHeight}", qrImage.Width, qrImage.Height); // CORREÇÃO: Validar stream do logo antes de criar image if (logoStream.Length == 0) { _logger.LogError("Logo stream is empty - Size: {StreamLength} bytes", logoStream.Length); return qrBytes; } logoStream.Position = 0; // Reset stream position using var logoImage = Image.Load(logoStream); _logger.LogDebug("Logo image created successfully - Logo dimensions: {LogoWidth}x{LogoHeight}", logoImage.Width, logoImage.Height); // CORREÇÃO: Validar dimensões do logo (muito grande pode causar problemas) if (logoImage.Width > 2000 || logoImage.Height > 2000) { _logger.LogWarning("Logo image is very large - {LogoWidth}x{LogoHeight} pixels. This may cause processing issues.", logoImage.Width, logoImage.Height); } // CORREÇÃO: Validar se QR é muito pequeno para receber logo if (qrImage.Width < 50 || qrImage.Height < 50) { _logger.LogWarning("QR image is very small - {QRWidth}x{QRHeight} pixels. Logo may not be visible.", qrImage.Width, qrImage.Height); } _logger.LogDebug("Creating final image with logo overlay"); // MELHORIA: Tamanho configurável do logo (10-25%) var logoSizePercent = request.LogoSizePercent ?? 20; if (logoSizePercent > 25) logoSizePercent = 25; // Limite de segurança if (logoSizePercent < 10) logoSizePercent = 10; // Mínimo visível var logoSize = Math.Min(qrImage.Width, qrImage.Height) * logoSizePercent / 100; // 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; // Borda maior para melhor contraste var backgroundX = (qrImage.Width - backgroundSize) / 2; var backgroundY = (qrImage.Height - backgroundSize) / 2; // MELHORIA: Aplicar colorização se solicitado _logger.LogInformation("🎨 [LOGO DEBUG] Processing logo - ApplyColorization: {ApplyColorization}, PrimaryColor: {PrimaryColor}", request.ApplyLogoColorization, request.PrimaryColor); using Image finalLogo = request.ApplyLogoColorization ? ApplyLogoColorization(logoImage, request.PrimaryColor) : logoImage.Clone(ctx => { }); // Create a copy to avoid disposing the original _logger.LogInformation("🎨 [LOGO DEBUG] Logo processing path: {Path}", request.ApplyLogoColorization ? "COLORIZED" : "ORIGINAL_COLORS"); if (finalLogo == null) { _logger.LogError("Final logo is null after processing, cannot apply logo overlay"); return qrBytes; // Return original QR if logo processing fails } _logger.LogDebug("Drawing logo at position ({LogoX}, {LogoY}) with size {LogoSize}", logoX, logoY, logoSize); // Clone the QR image and apply logo overlay using var finalImage = qrImage.Clone(ctx => { // Fill white background circle for logo var center = new PointF(backgroundX + backgroundSize / 2f, backgroundY + backgroundSize / 2f); var radius = backgroundSize / 2f; ctx.Fill(Color.White, new SixLabors.ImageSharp.Drawing.EllipsePolygon(center, radius)); // Resize logo to fit using var resizedLogo = finalLogo.Clone(logoCtx => logoCtx.Resize(new ResizeOptions { Size = new Size(logoSize, logoSize), Mode = ResizeMode.Stretch, Sampler = KnownResamplers.Lanczos3 })); // Draw the logo ctx.DrawImage(resizedLogo, new Point(logoX, logoY), 1.0f); }); // Convert back to byte array _logger.LogDebug("Saving final image with logo overlay"); using var outputStream = new MemoryStream(); finalImage.Save(outputStream, new PngEncoder { ColorType = PngColorType.RgbWithAlpha, CompressionLevel = PngCompressionLevel.DefaultCompression }); var result = outputStream.ToArray(); _logger.LogInformation("Logo overlay completed successfully - Final image size: {FinalSize} bytes", result.Length); return result; } catch (Exception ex) { _logger.LogError(ex, "Error applying enhanced logo overlay - QR: {QRSize}bytes, Logo: {LogoSize}bytes, Error: {ErrorMessage}", qrBytes.Length, logoBytes.Length, ex.Message); _logger.LogError("Stack trace: {StackTrace}", ex.StackTrace); // Return original QR code if logo overlay fails return qrBytes; } } // CORREÇÃO 9: Método para colorização do logo private Image ApplyLogoColorization(Image originalLogo, string targetColor) { _logger.LogDebug("Starting logo colorization - Original size: {Width}x{Height}, Target color: {Color}", originalLogo.Width, originalLogo.Height, targetColor); try { var color = ParseHtmlColor(targetColor); var rgba = color.ToPixel(); _logger.LogDebug("Parsed color: R={R}, G={G}, B={B}", rgba.R, rgba.G, rgba.B); // CORREÇÃO: Verificar se as dimensões são válidas para colorização if (originalLogo.Width <= 0 || originalLogo.Height <= 0) { _logger.LogError("Invalid logo dimensions for colorization - {Width}x{Height}", originalLogo.Width, originalLogo.Height); return originalLogo.Clone(ctx => { }); } _logger.LogDebug("Creating colorized image with dimensions {Width}x{Height}", originalLogo.Width, originalLogo.Height); // Apply intelligent colorization: preserve light backgrounds, colorize dark elements var colorizedLogo = originalLogo.Clone(ctx => { ctx.ProcessPixelRowsAsVector4((span, point) => { for (int x = 0; x < span.Length; x++) { var pixel = span[x]; // Calculate luminance (brightness) of the pixel var luminance = (pixel.X * 0.299f + pixel.Y * 0.587f + pixel.Z * 0.114f); // Define threshold for "background" vs "content" // Values above 0.8 (bright) are considered background and preserved var brightnessThreshold = 0.8f; if (luminance > brightnessThreshold) { // Preserve bright pixels (background) - keep original colors span[x] = pixel; } else { // Colorize dark pixels (content/drawings) with QR color // Use inverse luminance for intensity (darker pixels get more color) var intensity = Math.Max(0.1f, 1.0f - luminance); span[x] = new Vector4( rgba.R / 255f * intensity, rgba.G / 255f * intensity, rgba.B / 255f * intensity, pixel.W // Preserve alpha ); } } }); }); _logger.LogInformation("✨ Smart logo colorization completed - Preserving bright pixels (>0.8), colorizing dark content with {Color}", targetColor); return colorizedLogo; } catch (Exception ex) { _logger.LogError(ex, "Error applying logo colorization - Size: {Width}x{Height}, Color: {Color}, Error: {ErrorMessage}", originalLogo.Width, originalLogo.Height, targetColor, ex.Message); return originalLogo.Clone(ctx => { }); // Return a copy if error occurs } } private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize) { try { // Simplified implementation for cross-platform compatibility // The complex corner styling can be re-implemented later using ImageSharp drawing primitives _logger.LogInformation("Corner style '{CornerStyle}' temporarily disabled for cross-platform compatibility. Returning original QR code.", cornerStyle); return qrBytes; } 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; } } // QRModule class removed - was only used for corner styling which is temporarily simplified } }