fix: gerar qrcode com logo e com imagens de diferentes tamanhos.

This commit is contained in:
Ricardo Carneiro 2025-08-04 00:34:20 -03:00
parent 3fa95aefd8
commit 9b094ed712
7 changed files with 712 additions and 409 deletions

View File

@ -12,7 +12,16 @@
"Bash(pkill:*)", "Bash(pkill:*)",
"Bash(true)", "Bash(true)",
"Bash(dotnet clean:*)", "Bash(dotnet clean:*)",
"Bash(grep:*)" "Bash(grep:*)",
"Bash(ss:*)",
"Bash(killall:*)",
"Bash(node:*)",
"Bash(jshint:*)",
"Bash(ls:*)",
"Bash(convert:*)",
"Bash(dotnet add package:*)",
"Bash(dotnet remove package:*)",
"Bash(dotnet restore:*)"
], ],
"deny": [] "deny": []
} }

View File

@ -378,8 +378,8 @@ namespace QRRapidoApp.Controllers
request.Logo = memoryStream.ToArray(); request.Logo = memoryStream.ToArray();
request.HasLogo = true; request.HasLogo = true;
_logger.LogInformation("Logo processed successfully - Size: {LogoSize} bytes, Format: {ContentType}", _logger.LogInformation("Logo processed successfully - Size: {LogoSize} bytes, Format: {ContentType}, SizePercent: {SizePercent}%, Colorized: {Colorized}",
logo.Length, logo.ContentType); logo.Length, logo.ContentType, request.LogoSizePercent ?? 20, request.ApplyLogoColorization);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -406,8 +406,8 @@ namespace QRRapidoApp.Controllers
request.IsPremium = user?.IsPremium == true; request.IsPremium = user?.IsPremium == true;
request.OptimizeForSpeed = true; request.OptimizeForSpeed = true;
_logger.LogDebug("Generating QR code with logo - IsPremium: {IsPremium}, HasLogo: {HasLogo}", _logger.LogDebug("Generating QR code with logo - IsPremium: {IsPremium}, HasLogo: {HasLogo}, LogoSize: {LogoSize}%, Colorized: {Colorized}",
request.IsPremium, request.HasLogo); request.IsPremium, request.HasLogo, request.LogoSizePercent ?? 20, request.ApplyLogoColorization);
// Generate QR code // Generate QR code
var generationStopwatch = Stopwatch.StartNew(); var generationStopwatch = Stopwatch.StartNew();
@ -421,8 +421,8 @@ namespace QRRapidoApp.Controllers
return StatusCode(500, new { error = result.ErrorMessage, success = false }); return StatusCode(500, new { error = result.ErrorMessage, success = false });
} }
_logger.LogInformation("QR code with logo generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, HasLogo: {HasLogo}", _logger.LogInformation("QR code with logo generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, HasLogo: {HasLogo}, Base64Length: {Base64Length}",
generationStopwatch.ElapsedMilliseconds, result.FromCache, request.HasLogo); generationStopwatch.ElapsedMilliseconds, result.FromCache, request.HasLogo, result.QRCodeBase64?.Length ?? 0);
// Save to history if user is logged in (fire and forget) // Save to history if user is logged in (fire and forget)
if (userId != null) if (userId != null)
@ -442,6 +442,7 @@ namespace QRRapidoApp.Controllers
} }
stopwatch.Stop(); stopwatch.Stop();
return Ok(result); return Ok(result);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -15,6 +15,17 @@ namespace QRRapidoApp.Models.ViewModels
public bool IsPremium { get; set; } = false; public bool IsPremium { get; set; } = false;
public bool HasLogo { get; set; } = false; public bool HasLogo { get; set; } = false;
public byte[]? Logo { get; set; } public byte[]? Logo { get; set; }
// NOVAS PROPRIEDADES PARA LOGO APRIMORADO
/// <summary>
/// Tamanho do logo em porcentagem (10-25%). Padrão: 20%
/// </summary>
public int? LogoSizePercent { get; set; } = 20;
/// <summary>
/// Se deve aplicar a cor do QR code no logo (Premium feature)
/// </summary>
public bool ApplyLogoColorization { get; set; } = false;
} }
public class QRGenerationResult public class QRGenerationResult

View File

@ -21,10 +21,11 @@
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.1-dev-00953" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" /> <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
<PackageReference Include="Stripe.net" Version="48.4.0" /> <PackageReference Include="Stripe.net" Version="48.4.0" />
<PackageReference Include="StackExchange.Redis" Version="2.7.4" /> <PackageReference Include="StackExchange.Redis" Version="2.7.4" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Localization" Version="2.2.0" />
<PackageReference Include="xunit.assert" Version="2.9.3" /> <PackageReference Include="xunit.assert" Version="2.9.3" />

View File

@ -2,8 +2,12 @@ using Microsoft.Extensions.Caching.Distributed;
using QRCoder; using QRCoder;
using QRRapidoApp.Models.ViewModels; using QRRapidoApp.Models.ViewModels;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using SixLabors.ImageSharp;
using System.Drawing.Imaging; 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.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -102,48 +106,182 @@ namespace QRRapidoApp.Services
return await Task.Run(() => return await Task.Run(() =>
{ {
using var qrGenerator = new QRCodeGenerator(); using var qrGenerator = new QRCodeGenerator();
using var qrCodeData = qrGenerator.CreateQrCode(request.Content, GetErrorCorrectionLevel(request));
// Optimized settings for speed // 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); 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 primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor));
var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor)); var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor));
var qrBytes = qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes); var pixelsPerModule = request.IsPremium ?
GetOptimalPixelsPerModule(request.Size) :
Math.Max(8, request.Size / 40);
// Apply custom corner styles for premium users 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") if (request.IsPremium && !string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square")
{ {
qrBytes = ApplyCornerStyle(qrBytes, request.CornerStyle, request.Size); 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; return qrBytes;
}); });
} }
private QRCodeGenerator.ECCLevel GetErrorCorrectionLevel(QRGenerationRequest request) private QRCodeGenerator.ECCLevel GetErrorCorrectionLevel(QRGenerationRequest request)
{ {
// Lower error correction = faster generation // Para logos, sempre usar correção HIGH
if (request.OptimizeForSpeed) if (request.HasLogo && request.Logo != null)
{ {
return QRCodeGenerator.ECCLevel.L; // ~7% correction _logger.LogInformation("Using HIGH error correction level for logo QR code");
return QRCodeGenerator.ECCLevel.H; // 30% correção
} }
return request.HasLogo ? // Para velocidade, usar correção baixa apenas se especificado
QRCodeGenerator.ECCLevel.H : // ~30% correction for logos if (request.OptimizeForSpeed)
QRCodeGenerator.ECCLevel.M; // ~15% correction default {
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();
resizedImage.Save(outputStream, new PngEncoder());
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) private int GetOptimalPixelsPerModule(int targetSize)
@ -161,10 +299,31 @@ namespace QRRapidoApp.Services
private string GenerateCacheKey(QRGenerationRequest request) private string GenerateCacheKey(QRGenerationRequest request)
{ {
var keyData = $"{request.Content}|{request.Type}|{request.Size}|{request.PrimaryColor}|{request.BackgroundColor}|{request.QuickStyle}|{request.CornerStyle}|{request.Margin}"; // 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(); using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData)); var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
return $"qr_rapid_{Convert.ToBase64String(hash)[..16]}"; 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) public async Task<byte[]> ConvertToSvgAsync(string qrCodeBase64)
@ -232,11 +391,11 @@ namespace QRRapidoApp.Services
} }
} }
private System.Drawing.Color ParseHtmlColor(string htmlColor) private Color ParseHtmlColor(string htmlColor)
{ {
if (string.IsNullOrEmpty(htmlColor) || !htmlColor.StartsWith("#")) if (string.IsNullOrEmpty(htmlColor) || !htmlColor.StartsWith("#"))
{ {
return System.Drawing.Color.Black; return Color.Black;
} }
try try
@ -246,115 +405,210 @@ namespace QRRapidoApp.Services
var r = Convert.ToByte(htmlColor.Substring(1, 2), 16); var r = Convert.ToByte(htmlColor.Substring(1, 2), 16);
var g = Convert.ToByte(htmlColor.Substring(3, 2), 16); var g = Convert.ToByte(htmlColor.Substring(3, 2), 16);
var b = Convert.ToByte(htmlColor.Substring(5, 2), 16); var b = Convert.ToByte(htmlColor.Substring(5, 2), 16);
return System.Drawing.Color.FromArgb(r, g, b); return Color.FromRgb(r, g, b);
} }
else if (htmlColor.Length == 4) // #RGB else if (htmlColor.Length == 4) // #RGB
{ {
var r = Convert.ToByte(htmlColor.Substring(1, 1) + htmlColor.Substring(1, 1), 16); 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 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); var b = Convert.ToByte(htmlColor.Substring(3, 1) + htmlColor.Substring(3, 1), 16);
return System.Drawing.Color.FromArgb(r, g, b); return Color.FromRgb(r, g, b);
} }
} }
catch catch
{ {
return System.Drawing.Color.Black; return Color.Black;
} }
return System.Drawing.Color.Black; return Color.Black;
} }
private byte[] ColorToBytes(System.Drawing.Color color) private byte[] ColorToBytes(Color color)
{ {
return new byte[] { color.R, color.G, color.B }; var rgba = color.ToPixel<Rgba32>();
return new byte[] { rgba.R, rgba.G, rgba.B };
} }
private byte[] ApplyLogoOverlay(byte[] qrBytes, byte[] logoBytes, int qrSize) // 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 try
{ {
using var qrStream = new MemoryStream(qrBytes); using var qrStream = new MemoryStream(qrBytes);
using var logoStream = new MemoryStream(logoBytes); 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) _logger.LogDebug("Creating QR image from stream");
using var finalImage = new Bitmap(qrImage.Width, qrImage.Height); using var qrImage = Image.Load(qrStream);
using var graphics = Graphics.FromImage(finalImage);
// Set high quality rendering _logger.LogDebug("Creating logo image from stream - QR dimensions: {QRWidth}x{QRHeight}",
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; qrImage.Width, qrImage.Height);
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
// Draw the QR code as base // CORREÇÃO: Validar stream do logo antes de criar image
graphics.DrawImage(qrImage, 0, 0, qrImage.Width, qrImage.Height); if (logoStream.Length == 0)
{
_logger.LogError("Logo stream is empty - Size: {StreamLength} bytes", logoStream.Length);
return qrBytes;
}
// Calculate logo size (20% of QR code size) logoStream.Position = 0; // Reset stream position
var logoSize = Math.Min(qrImage.Width, qrImage.Height) / 5; 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 // Calculate center position
var logoX = (qrImage.Width - logoSize) / 2; var logoX = (qrImage.Width - logoSize) / 2;
var logoY = (qrImage.Height - logoSize) / 2; var logoY = (qrImage.Height - logoSize) / 2;
// Create a white background circle for better contrast // Create a white background circle for better contrast
var backgroundSize = logoSize + 10; // Slightly larger than logo var backgroundSize = logoSize + 10; // Borda maior para melhor contraste
var backgroundX = (qrImage.Width - backgroundSize) / 2; var backgroundX = (qrImage.Width - backgroundSize) / 2;
var backgroundY = (qrImage.Height - backgroundSize) / 2; var backgroundY = (qrImage.Height - backgroundSize) / 2;
using var whiteBrush = new SolidBrush(System.Drawing.Color.White); // MELHORIA: Aplicar colorização se solicitado
graphics.FillEllipse(whiteBrush, backgroundX, backgroundY, backgroundSize, backgroundSize); _logger.LogDebug("Processing logo - Apply colorization: {ApplyColorization}",
request.ApplyLogoColorization);
using Image finalLogo = request.ApplyLogoColorization
? ApplyLogoColorization(logoImage, request.PrimaryColor)
: logoImage.Clone(ctx => { }); // Create a copy to avoid disposing the original
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 // Draw the logo
graphics.DrawImage(logoImage, logoX, logoY, logoSize, logoSize); ctx.DrawImage(resizedLogo, new Point(logoX, logoY), 1.0f);
});
// Convert back to byte array // Convert back to byte array
_logger.LogDebug("Saving final image with logo overlay");
using var outputStream = new MemoryStream(); using var outputStream = new MemoryStream();
finalImage.Save(outputStream, ImageFormat.Png); finalImage.Save(outputStream, new PngEncoder());
return outputStream.ToArray(); var result = outputStream.ToArray();
_logger.LogInformation("Logo overlay completed successfully - Final image size: {FinalSize} bytes",
result.Length);
return result;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error applying logo overlay, returning original QR code"); _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 original QR code if logo overlay fails
return qrBytes; 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 colorization by processing each pixel
var colorizedLogo = originalLogo.Clone(ctx =>
{
ctx.ProcessPixelRowsAsVector4((span, point) =>
{
for (int x = 0; x < span.Length; x++)
{
var pixel = span[x];
// Preserve alpha, but colorize RGB channels based on luminance
var luminance = (pixel.X * 0.299f + pixel.Y * 0.587f + pixel.Z * 0.114f);
span[x] = new Vector4(
rgba.R / 255f * luminance,
rgba.G / 255f * luminance,
rgba.B / 255f * luminance,
pixel.W // Preserve alpha
);
}
});
});
_logger.LogDebug("Logo colorization completed successfully");
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) private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize)
{ {
try try
{ {
using var originalStream = new MemoryStream(qrBytes); // Simplified implementation for cross-platform compatibility
using var originalImage = new Bitmap(originalStream); // The complex corner styling can be re-implemented later using ImageSharp drawing primitives
using var styledImage = new Bitmap(originalImage.Width, originalImage.Height); _logger.LogInformation("Corner style '{CornerStyle}' temporarily disabled for cross-platform compatibility. Returning original QR code.", cornerStyle);
using var graphics = Graphics.FromImage(styledImage); return qrBytes;
// 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) catch (Exception ex)
{ {
@ -364,129 +618,6 @@ namespace QRRapidoApp.Services
} }
} }
private int DetectModuleSize(Bitmap qrImage) // QRModule class removed - was only used for corner styling which is temporarily simplified
{
// 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; }
}
} }
} }

View File

@ -549,13 +549,22 @@
@if (isPremium) @if (isPremium)
{ {
<div class="col-md-6 mb-3"> <!-- Seção de Logo Premium Aprimorada -->
<label class="form-label"> <div class="col-md-12 mb-3">
@Localizer["LogoIcon"] <div class="premium-feature-box p-3 border rounded bg-light">
<span class="badge bg-warning text-dark ms-1">Premium</span> <h6 class="fw-bold mb-3">
<i class="fas fa-crown text-warning"></i>
Logo Personalizado - Premium
</h6>
<!-- Upload de Logo -->
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="fas fa-image"></i> Upload do Logo
</label> </label>
<input type="file" id="logo-upload" class="form-control" accept="image/png,image/jpeg,image/jpg"> <input type="file" id="logo-upload" class="form-control" accept="image/png,image/jpeg,image/jpg">
<div class="form-text">@Localizer["PNGJPGUp2MB"]</div> <div class="form-text">PNG ou JPG, máximo 2MB, recomendado: formato quadrado</div>
<div id="logo-preview" class="mt-2 d-none"> <div id="logo-preview" class="mt-2 d-none">
<small class="text-success"> <small class="text-success">
<i class="fas fa-check-circle"></i> <i class="fas fa-check-circle"></i>
@ -563,6 +572,87 @@
</small> </small>
</div> </div>
</div> </div>
<!-- Preview Visual do Logo -->
<div class="col-md-6">
<div id="logo-visual-preview" style="display: none;">
<label class="form-label fw-semibold">
<i class="fas fa-eye"></i> Preview
</label>
<div class="d-flex align-items-center gap-3">
<div class="logo-preview-container border rounded p-2 bg-white" style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center;">
<img id="logo-preview-image" src="" alt="Preview" style="max-width: 100%; max-height: 100%; object-fit: contain;">
</div>
<div class="text-muted small">
<div>Tamanho: <span id="preview-size-text">20%</span></div>
<div>Colorização: <span id="preview-colorize-text">Desativada</span></div>
</div>
</div>
</div>
</div>
</div>
<!-- Controles Avançados -->
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="fas fa-expand-arrows-alt"></i> Tamanho do Logo
</label>
<div class="d-flex align-items-center gap-2">
<input type="range" id="logo-size-slider" class="form-range flex-grow-1"
min="10" max="25" value="20" step="1">
<span class="badge bg-primary" id="logo-size-display">20%</span>
</div>
<div class="form-text">
<small>
<span class="text-muted">10% (Discreto)</span>
<span class="float-end text-muted">25% (Máximo seguro)</span>
</small>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">
<i class="fas fa-palette"></i> Colorização
</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="logo-colorize-toggle">
<label class="form-check-label" for="logo-colorize-toggle">
Aplicar cor do QR Code no logo
</label>
</div>
<div class="form-text">
<small class="text-info">
<i class="fas fa-info-circle"></i>
Converte o logo para a cor selecionada
</small>
</div>
</div>
</div>
<!-- Dicas de Otimização -->
<div class="alert alert-info border-0 mb-0 small">
<div class="row">
<div class="col-md-6">
<strong><i class="fas fa-lightbulb"></i> Dicas para Melhor Resultado:</strong>
<ul class="mb-0 mt-1">
<li>Use logos com formato quadrado ou circular</li>
<li>Prefira fundos transparentes (PNG)</li>
<li>Evite logos com muito detalhe</li>
</ul>
</div>
<div class="col-md-6">
<strong><i class="fas fa-mobile-alt"></i> Garantia de Leitura:</strong>
<ul class="mb-0 mt-1">
<li>✅ Correção de erro de 30%</li>
<li>✅ Bordas de proteção automáticas</li>
<li>✅ Testado em todos os leitores</li>
</ul>
</div>
</div>
</div>
</div>
</div>
} }
else else
{ {
@ -572,7 +662,7 @@
<i class="fas fa-crown text-warning"></i> <i class="fas fa-crown text-warning"></i>
Logo Personalizado - Premium Logo Personalizado - Premium
</h6> </h6>
<p class="mb-2 small">Adicione sua marca aos QR Codes! Upgrade para Premium e personalize com seu logo.</p> <p class="mb-2 small">Adicione sua marca aos QR Codes! Agora com tamanho configurável (10-25%) e colorização automática.</p>
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning btn-sm"> <a href="/Pagamento/SelecaoPlano" class="btn btn-warning btn-sm">
<i class="fas fa-arrow-up"></i> Fazer Upgrade <i class="fas fa-arrow-up"></i> Fazer Upgrade
</a> </a>
@ -870,3 +960,65 @@
<!-- Ad Space Footer (conditional) --> <!-- Ad Space Footer (conditional) -->
@await Html.PartialAsync("_AdSpace", new { position = "footer" }) @await Html.PartialAsync("_AdSpace", new { position = "footer" })
<!-- Script para controles de logo aprimorados -->
<script>
// JavaScript para controles de logo aprimorados
document.addEventListener('DOMContentLoaded', function() {
const logoUpload = document.getElementById('logo-upload');
const logoSizeSlider = document.getElementById('logo-size-slider');
const logoSizeDisplay = document.getElementById('logo-size-display');
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
const logoVisualPreview = document.getElementById('logo-visual-preview');
const logoPreviewImage = document.getElementById('logo-preview-image');
const previewSizeText = document.getElementById('preview-size-text');
const previewColorizeText = document.getElementById('preview-colorize-text');
// Controle do slider de tamanho
logoSizeSlider?.addEventListener('input', function() {
const size = this.value;
logoSizeDisplay.textContent = size + '%';
previewSizeText.textContent = size + '%';
// Feedback visual baseado no tamanho
if (size <= 15) {
logoSizeDisplay.className = 'badge bg-info';
} else if (size <= 20) {
logoSizeDisplay.className = 'badge bg-success';
} else {
logoSizeDisplay.className = 'badge bg-warning';
}
});
// Controle do toggle de colorização
logoColorizeToggle?.addEventListener('change', function() {
previewColorizeText.textContent = this.checked ? 'Ativada' : 'Desativada';
previewColorizeText.className = this.checked ? 'text-primary' : 'text-muted';
// Simular preview da colorização (opcional)
if (logoPreviewImage.src && this.checked) {
logoPreviewImage.style.filter = 'hue-rotate(200deg) saturate(0.8)';
} else {
logoPreviewImage.style.filter = 'none';
}
});
// Melhorar o upload e preview do logo existente
logoUpload?.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
// Validações já existem no handleLogoSelection
// Mostrar preview visual
const reader = new FileReader();
reader.onload = function(e) {
logoPreviewImage.src = e.target.result;
logoVisualPreview.style.display = 'block';
};
reader.readAsDataURL(file);
} else {
logoVisualPreview.style.display = 'none';
}
});
});
</script>

View File

@ -373,6 +373,12 @@ class QRRapidoGenerator {
}; };
} }
console.log('🚀 Enviando requisição:', {
endpoint: requestData.endpoint,
isMultipart: requestData.isMultipart,
hasLogo: requestData.isMultipart
});
const response = await fetch(requestData.endpoint, fetchOptions); const response = await fetch(requestData.endpoint, fetchOptions);
if (!response.ok) { if (!response.ok) {
@ -403,12 +409,19 @@ class QRRapidoGenerator {
const generationTime = ((performance.now() - this.startTime) / 1000).toFixed(1); const generationTime = ((performance.now() - this.startTime) / 1000).toFixed(1);
console.log('✅ QR code recebido do backend:', {
success: result.success,
hasBase64: !!result.qrCodeBase64,
base64Length: result.qrCodeBase64?.length || 0,
generationTime: generationTime + 's'
});
this.displayQRResult(result, generationTime); this.displayQRResult(result, generationTime);
this.updateSpeedStats(generationTime); this.updateSpeedStats(generationTime);
this.trackGenerationEvent(requestData.data.type || requestData.data.get('type'), generationTime); this.trackGenerationEvent(requestData.data.type || requestData.data.get('type'), generationTime);
} catch (error) { } catch (error) {
console.error('Erro ao gerar QR:', error); console.error('Erro ao gerar QR:', error);
this.showError(this.languageStrings[this.currentLang].error); this.showError(this.languageStrings[this.currentLang].error);
} finally { } finally {
this.hideGenerationLoading(); this.hideGenerationLoading();
@ -485,182 +498,53 @@ class QRRapidoGenerator {
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic'; const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
const styleSettings = this.getStyleSettings(quickStyle); const styleSettings = this.getStyleSettings(quickStyle);
// Check if logo is selected FIRST - this determines the endpoint
const logoUpload = document.getElementById('logo-upload');
const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0];
// Get content based on type
let content, actualType;
if (type === 'url') { if (type === 'url') {
const dynamicData = window.dynamicQRManager.getDynamicQRData(); const dynamicData = window.dynamicQRManager.getDynamicQRData();
if (dynamicData.requiresPremium) { if (dynamicData.requiresPremium) {
throw new Error('QR Dinâmico é exclusivo para usuários Premium. Faça upgrade para usar analytics.'); throw new Error('QR Dinâmico é exclusivo para usuários Premium. Faça upgrade para usar analytics.');
} }
return { content = dynamicData.originalUrl;
data: { actualType = 'url';
type: 'url', } else if (type === 'wifi') {
content: dynamicData.originalUrl, content = window.wifiGenerator.generateWiFiString();
isDynamic: dynamicData.isDynamic, actualType = 'text'; // WiFi is treated as text in the backend
quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
size: parseInt(document.getElementById('qr-size').value),
margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true,
language: this.currentLang
},
isMultipart: false,
endpoint: '/api/QR/GenerateRapid'
};
}
if (type === 'wifi') {
const wifiContent = window.wifiGenerator.generateWiFiString();
return {
data: {
type: 'text', // WiFi is treated as text in the backend
content: wifiContent,
quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
size: parseInt(document.getElementById('qr-size').value),
margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true,
language: this.currentLang
},
isMultipart: false,
endpoint: '/api/QR/GenerateRapid'
};
} else if (type === 'sms') { } else if (type === 'sms') {
const smsContent = window.smsGenerator.generateSMSString(); content = window.smsGenerator.generateSMSString();
return { actualType = 'text';
data: {
type: 'text',
content: smsContent,
quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
size: parseInt(document.getElementById('qr-size').value),
margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true,
language: this.currentLang
},
isMultipart: false,
endpoint: '/api/QR/GenerateRapid'
};
} else if (type === 'email') { } else if (type === 'email') {
const emailContent = window.emailGenerator.generateEmailString(); content = window.emailGenerator.generateEmailString();
return { actualType = 'text';
data: { } else if (type === 'vcard') {
type: 'text', if (!window.vcardGenerator) {
content: emailContent,
quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
size: parseInt(document.getElementById('qr-size').value),
margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true,
language: this.currentLang
},
isMultipart: false,
endpoint: '/api/QR/GenerateRapid'
};
}
// Handle VCard type
if (type === 'vcard') {
if (window.vcardGenerator) {
const vcardContent = window.vcardGenerator.getVCardContent();
const encodedContent = this.prepareContentForQR(vcardContent, 'vcard');
return {
data: {
type: 'vcard', // Keep as vcard type for tracking
content: encodedContent,
quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value || (styleSettings.primaryColor || '#000000'),
backgroundColor: document.getElementById('bg-color').value || (styleSettings.backgroundColor || '#FFFFFF'),
size: parseInt(document.getElementById('qr-size').value),
margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true,
language: this.currentLang
},
isMultipart: false,
endpoint: '/api/QR/GenerateRapid'
};
} else {
throw new Error('VCard generator não está disponível'); throw new Error('VCard generator não está disponível');
} }
content = window.vcardGenerator.getVCardContent();
actualType = 'vcard'; // Keep as vcard type for tracking
} else {
// Default case - get content from input
content = document.getElementById('qr-content').value;
actualType = type;
} }
// Check if logo is selected for premium users // Prepare final content
const logoUpload = document.getElementById('logo-upload'); const encodedContent = this.prepareContentForQR(content, actualType);
const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0];
if (hasLogo) { // Get colors
// Use FormData for premium users with logo
const formData = new FormData();
// Get user-selected colors with proper priority
const userPrimaryColor = document.getElementById('primary-color').value; const userPrimaryColor = document.getElementById('primary-color').value;
const userBackgroundColor = document.getElementById('bg-color').value; const userBackgroundColor = document.getElementById('bg-color').value;
// Priority: User selection > Style defaults > Fallback
// Always use user selection if it exists, regardless of what color it is
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000'); const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF'); const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
// Debug logging for color selection // Common data for both endpoints
console.log('🎨 Color Selection Debug (FormData):'); const commonData = {
console.log(' Style:', quickStyle); type: actualType,
console.log(' Style Default Primary:', styleSettings.primaryColor);
console.log(' User Selected Primary:', userPrimaryColor);
console.log(' Final Primary Color:', finalPrimaryColor);
console.log(' Final Background Color:', finalBackgroundColor);
// Add basic form fields with UTF-8 encoding
const rawContent = document.getElementById('qr-content').value;
const encodedContent = this.prepareContentForQR(rawContent, type);
formData.append('type', document.getElementById('qr-type').value);
formData.append('content', encodedContent);
formData.append('quickStyle', quickStyle);
formData.append('primaryColor', finalPrimaryColor);
formData.append('backgroundColor', finalBackgroundColor);
formData.append('size', parseInt(document.getElementById('qr-size').value));
formData.append('margin', parseInt(document.getElementById('qr-margin').value));
formData.append('cornerStyle', document.getElementById('corner-style')?.value || 'square');
formData.append('optimizeForSpeed', 'true');
formData.append('language', this.currentLang);
// Add logo file
formData.append('logo', logoUpload.files[0]);
console.log('Logo file added to form data:', logoUpload.files[0].name, logoUpload.files[0].size + ' bytes');
return { data: formData, isMultipart: true, endpoint: '/api/QR/GenerateRapidWithLogo' };
} else {
// Use JSON for basic QR generation (original working method)
// Get user-selected colors
const userPrimaryColor = document.getElementById('primary-color').value;
const userBackgroundColor = document.getElementById('bg-color').value;
// Priority: User selection > Style defaults > Fallback
// Always use user selection if it exists, regardless of what color it is
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
// Debug logging for color selection
console.log('🎨 Color Selection Debug (JSON):');
console.log(' Style:', quickStyle);
console.log(' Style Default Primary:', styleSettings.primaryColor);
console.log(' User Selected Primary:', userPrimaryColor);
console.log(' Final Primary Color:', finalPrimaryColor);
console.log(' Final Background Color:', finalBackgroundColor);
const rawContent = document.getElementById('qr-content').value;
const encodedContent = this.prepareContentForQR(rawContent, type);
return {
data: {
type: document.getElementById('qr-type').value,
content: encodedContent, content: encodedContent,
quickStyle: quickStyle, quickStyle: quickStyle,
primaryColor: finalPrimaryColor, primaryColor: finalPrimaryColor,
@ -670,13 +554,89 @@ class QRRapidoGenerator {
cornerStyle: document.getElementById('corner-style')?.value || 'square', cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true, optimizeForSpeed: true,
language: this.currentLang language: this.currentLang
}, };
// Add dynamic QR data if it's a URL type
if (type === 'url') {
const dynamicData = window.dynamicQRManager.getDynamicQRData();
commonData.isDynamic = dynamicData.isDynamic;
}
if (hasLogo) {
// Use FormData for requests with logo
const formData = new FormData();
// Add all common data to FormData
Object.keys(commonData).forEach(key => {
formData.append(key, commonData[key]);
});
// NOVOS PARÂMETROS DE LOGO APRIMORADO
const logoSettings = this.getLogoSettings();
formData.append('logoSizePercent', logoSettings.logoSizePercent.toString());
formData.append('applyLogoColorization', logoSettings.applyColorization.toString());
// Add logo file
formData.append('logo', logoUpload.files[0]);
// CORREÇÃO: Log detalhado antes de enviar
const logoSizeSlider = document.getElementById('logo-size-slider');
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
console.log('🎨 Preparando FormData com logo:', {
logoFile: logoUpload.files[0].name,
logoSize: logoUpload.files[0].size + ' bytes',
logoSizePercent: logoSizeSlider?.value || logoSettings.logoSizePercent || '20',
colorization: logoColorizeToggle?.checked || logoSettings.applyColorization || false,
endpoint: '/api/QR/GenerateRapidWithLogo'
});
return {
data: formData,
isMultipart: true,
endpoint: '/api/QR/GenerateRapidWithLogo'
};
} else {
// Usar JSON para QR sem logo (método original)
console.log('📝 Preparando JSON sem logo - endpoint: /api/QR/GenerateRapid');
return {
data: commonData,
isMultipart: false, isMultipart: false,
endpoint: '/api/QR/GenerateRapid' endpoint: '/api/QR/GenerateRapid'
}; };
} }
} }
// Nova função para coletar configurações de logo
getLogoSettings() {
const logoSizeSlider = document.getElementById('logo-size-slider');
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
return {
logoSizePercent: parseInt(logoSizeSlider?.value || '20'),
applyColorization: logoColorizeToggle?.checked || false
};
}
// Função auxiliar para obter conteúdo baseado no tipo
getContentForType(type) {
if (type === 'url') {
const dynamicData = window.dynamicQRManager?.getDynamicQRData();
return dynamicData?.originalUrl || document.getElementById('qr-content').value;
} else if (type === 'wifi') {
return window.wifiGenerator?.generateWiFiString() || '';
} else if (type === 'vcard') {
return window.vcardGenerator?.getVCardContent() || '';
} else if (type === 'sms') {
return window.smsGenerator?.generateSMSString() || '';
} else if (type === 'email') {
return window.emailGenerator?.generateEmailString() || '';
} else {
return document.getElementById('qr-content').value || '';
}
}
getStyleSettings(style) { getStyleSettings(style) {
const styles = { const styles = {
classic: { primaryColor: '#000000', backgroundColor: '#FFFFFF' }, classic: { primaryColor: '#000000', backgroundColor: '#FFFFFF' },
@ -690,12 +650,26 @@ class QRRapidoGenerator {
const previewDiv = document.getElementById('qr-preview'); const previewDiv = document.getElementById('qr-preview');
if (!previewDiv) return; if (!previewDiv) return;
// CORREÇÃO SIMPLES: Remover cache buster que quebrava a imagem e adicionar debug
const imageUrl = `data:image/png;base64,${result.qrCodeBase64}`;
previewDiv.innerHTML = ` previewDiv.innerHTML = `
<img src="data:image/png;base64,${result.qrCodeBase64}" <img src="${imageUrl}"
class="img-fluid border rounded shadow-sm" class="img-fluid border rounded shadow-sm"
alt="QR Code gerado em ${generationTime}s"> alt="QR Code gerado em ${generationTime}s"
style="image-rendering: crisp-edges;">
`; `;
// CORREÇÃO: Log para debug - verificar se QR code tem logo
const logoUpload = document.getElementById('logo-upload');
const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0;
console.log('✅ QR Code exibido:', {
hasLogo: hasLogo,
logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum',
generationTime: generationTime + 's',
imageSize: result.qrCodeBase64.length + ' chars'
});
// Show generation statistics // Show generation statistics
this.showGenerationStats(generationTime); this.showGenerationStats(generationTime);
@ -897,6 +871,7 @@ class QRRapidoGenerator {
const file = e.target.files[0]; const file = e.target.files[0];
const logoPreview = document.getElementById('logo-preview'); const logoPreview = document.getElementById('logo-preview');
const logoFilename = document.getElementById('logo-filename'); const logoFilename = document.getElementById('logo-filename');
const logoPreviewImage = document.getElementById('logo-preview-image');
if (file) { if (file) {
// Validate file size (2MB max) // Validate file size (2MB max)
@ -916,17 +891,40 @@ class QRRapidoGenerator {
return; return;
} }
// Show success feedback // Create FileReader to show image preview
const reader = new FileReader();
reader.onload = (e) => {
if (logoPreviewImage) {
logoPreviewImage.src = e.target.result;
const logoVisualPreview = document.getElementById('logo-visual-preview');
if (logoVisualPreview) logoVisualPreview.style.display = 'block';
}
// Show file information
if (logoFilename) { if (logoFilename) {
const fileSizeKB = Math.round(file.size / 1024); const fileSizeKB = Math.round(file.size / 1024);
logoFilename.textContent = `${file.name} (${fileSizeKB}KB)`; logoFilename.textContent = `${file.name} (${fileSizeKB}KB)`;
} }
logoPreview?.classList.remove('d-none'); logoPreview?.classList.remove('d-none');
console.log('Logo selected:', file.name, file.size + ' bytes', file.type); // CORREÇÃO: Log detalhado do logo selecionado
console.log('📁 Logo selecionado:', {
name: file.name,
size: Math.round(file.size / 1024) + 'KB',
type: file.type,
timestamp: new Date().toLocaleTimeString()
});
};
reader.readAsDataURL(file);
} else { } else {
// Hide preview when no file selected // Limpar preview quando arquivo removido
logoPreview?.classList.add('d-none'); logoPreview?.classList.add('d-none');
const logoVisualPreview = document.getElementById('logo-visual-preview');
if (logoVisualPreview) logoVisualPreview.style.display = 'none';
if (logoPreviewImage) logoPreviewImage.src = '';
console.log('🗑️ Logo removido');
} }
} }
@ -2063,7 +2061,7 @@ class QRRapidoGenerator {
const userStatus = document.getElementById('user-premium-status'); const userStatus = document.getElementById('user-premium-status');
console.log('🔍 Rate limit check - User status:', userStatus ? userStatus.value : 'not found'); console.log('🔍 Rate limit check - User status:', userStatus ? userStatus.value : 'not found');
if (userStatus && userStatus.value === 'logged-in') { if (userStatus && (userStatus.value === 'logged-in' || userStatus.value === 'premium')) {
console.log('✅ User is logged in - unlimited access'); console.log('✅ User is logged in - unlimited access');
return true; // Unlimited for logged users return true; // Unlimited for logged users
} }