QrRapido/Services/QRRapidoService.cs
Ricardo Carneiro 5f0d3dbf66
Some checks failed
Deploy QR Rapido / test (push) Successful in 3m39s
Deploy QR Rapido / build-and-push (push) Failing after 5s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped
fix: logo cores e tamanho
2025-08-04 01:22:29 -03:00

652 lines
30 KiB
C#

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;
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();
// 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 = "Logo não fornecido";
return false;
}
// Validar tamanho máximo
if (logoBytes.Length > 2 * 1024 * 1024) // 2MB
{
errorMessage = "Logo muito grande. Máximo 2MB.";
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 = "Logo muito pequeno. Mínimo 32x32 pixels.";
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 = "Formato de imagem inválido";
_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<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 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<Rgba32>();
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<Rgba32>();
_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
}
}