fix: Melhorar a leitura mesmo com o logotipo maior.
Some checks failed
Deploy QR Rapido / test (push) Successful in 26s
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

This commit is contained in:
Ricardo Carneiro 2025-08-04 01:50:54 -03:00
parent 5f0d3dbf66
commit 8ab0b913e2
8 changed files with 676 additions and 9 deletions

View File

@ -21,7 +21,9 @@
"Bash(convert:*)", "Bash(convert:*)",
"Bash(dotnet add package:*)", "Bash(dotnet add package:*)",
"Bash(dotnet remove package:*)", "Bash(dotnet remove package:*)",
"Bash(dotnet restore:*)" "Bash(dotnet restore:*)",
"Bash(rg:*)",
"Bash(dotnet test:*)"
], ],
"deny": [] "deny": []
} }

34
Models/LogoReadability.cs Normal file
View File

@ -0,0 +1,34 @@
namespace QRRapidoApp.Models
{
public class LogoReadabilityInfo
{
public bool HasLogo { get; set; }
public int LogoSizePercent { get; set; }
public int ReadabilityScore { get; set; } // 0-100
public string DifficultyLevel { get; set; } = string.Empty; // VeryEasy, Easy, Medium, Hard, VeryHard
public string UserMessage { get; set; } = string.Empty;
public List<string> Tips { get; set; } = new();
public LogoComplexity LogoComplexity { get; set; } = new();
}
public class LogoComplexity
{
public int Width { get; set; }
public int Height { get; set; }
public bool IsSquare { get; set; }
public bool HasTransparency { get; set; }
public int DominantColorCount { get; set; }
public double ContrastRatio { get; set; }
public string FileFormat { get; set; } = string.Empty;
public int FileSizeBytes { get; set; }
}
public enum ReadabilityLevel
{
VeryEasy = 85, // 85-100
Easy = 70, // 70-84
Medium = 55, // 55-69
Hard = 40, // 40-54
VeryHard = 0 // 0-39
}
}

View File

@ -1,3 +1,5 @@
using QRRapidoApp.Models;
namespace QRRapidoApp.Models.ViewModels namespace QRRapidoApp.Models.ViewModels
{ {
public class QRGenerationRequest public class QRGenerationRequest
@ -39,5 +41,6 @@ namespace QRRapidoApp.Models.ViewModels
public int? RemainingQRs { get; set; } // For free users public int? RemainingQRs { get; set; } // For free users
public bool Success { get; set; } = true; public bool Success { get; set; } = true;
public string? ErrorMessage { get; set; } public string? ErrorMessage { get; set; }
public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade
} }
} }

View File

@ -181,6 +181,7 @@ builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>(); builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>();
builder.Services.AddScoped<AdDisplayService>(); builder.Services.AddScoped<AdDisplayService>();
builder.Services.AddScoped<StripeService>(); builder.Services.AddScoped<StripeService>();
builder.Services.AddScoped<LogoReadabilityAnalyzer>();
// Background Services // Background Services
builder.Services.AddHostedService<HistoryCleanupService>(); builder.Services.AddHostedService<HistoryCleanupService>();

View File

@ -0,0 +1,348 @@
using Microsoft.Extensions.Localization;
using QRRapidoApp.Models;
using QRRapidoApp.Models.ViewModels;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System.Collections.Generic;
namespace QRRapidoApp.Services
{
public class LogoReadabilityAnalyzer
{
private readonly ILogger<LogoReadabilityAnalyzer> _logger;
private readonly IStringLocalizer<LogoReadabilityAnalyzer> _localizer;
public LogoReadabilityAnalyzer(ILogger<LogoReadabilityAnalyzer> logger, IStringLocalizer<LogoReadabilityAnalyzer> localizer)
{
_logger = logger;
_localizer = localizer;
}
public async Task<LogoReadabilityInfo> AnalyzeAsync(QRGenerationRequest request, int qrSize = 300)
{
var result = new LogoReadabilityInfo();
if (!request.HasLogo || request.Logo == null || request.Logo.Length == 0)
{
result.HasLogo = false;
result.ReadabilityScore = 100;
result.DifficultyLevel = ReadabilityLevel.VeryEasy.ToString();
result.UserMessage = "✅ Perfeita legibilidade! QR code sem logo é sempre facilmente lido.";
return result;
}
try
{
result.HasLogo = true;
result.LogoComplexity = await AnalyzeLogoComplexityAsync(request.Logo);
result.LogoSizePercent = request.LogoSizePercent ?? 20;
// Calcular score baseado em múltiplos fatores
result.ReadabilityScore = CalculateReadabilityScore(result.LogoComplexity, result.LogoSizePercent, qrSize);
result.DifficultyLevel = GetDifficultyLevel(result.ReadabilityScore).ToString();
result.UserMessage = GenerateUserMessage(result.ReadabilityScore, result.LogoSizePercent);
result.Tips = GeneratePersonalizedTips(result.LogoComplexity, result.LogoSizePercent, result.ReadabilityScore);
_logger.LogInformation("Logo readability analysis completed - Score: {Score}, Size: {Size}%, Level: {Level}",
result.ReadabilityScore, result.LogoSizePercent, result.DifficultyLevel);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing logo readability");
// Fallback para análise básica baseada apenas no tamanho
result.HasLogo = true;
result.LogoSizePercent = request.LogoSizePercent ?? 20;
result.ReadabilityScore = Math.Max(40, 100 - (result.LogoSizePercent - 15) * 3);
result.DifficultyLevel = GetDifficultyLevel(result.ReadabilityScore).ToString();
result.UserMessage = GenerateUserMessage(result.ReadabilityScore, result.LogoSizePercent);
result.Tips = new List<string> { "⚠️ Análise limitada devido a erro técnico. Teste sempre em vários dispositivos." };
return result;
}
}
private async Task<LogoComplexity> AnalyzeLogoComplexityAsync(byte[] logoBytes)
{
return await Task.Run(() =>
{
try
{
using var stream = new MemoryStream(logoBytes);
using var image = Image.Load<Rgba32>(stream);
var complexity = new LogoComplexity
{
Width = image.Width,
Height = image.Height,
IsSquare = Math.Abs(image.Width - image.Height) <= Math.Min(image.Width, image.Height) * 0.1,
FileSizeBytes = logoBytes.Length,
FileFormat = DetectImageFormat(logoBytes)
};
// Analisar transparência
complexity.HasTransparency = HasTransparency(image);
// Analisar complexidade de cores
var colorAnalysis = AnalyzeColors(image);
complexity.DominantColorCount = colorAnalysis.dominantColors;
complexity.ContrastRatio = colorAnalysis.contrastRatio;
_logger.LogDebug("Logo complexity analysis - {Width}x{Height}, Square: {IsSquare}, Transparency: {HasTransparency}, Colors: {Colors}",
complexity.Width, complexity.Height, complexity.IsSquare, complexity.HasTransparency, complexity.DominantColorCount);
return complexity;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing logo complexity");
return new LogoComplexity
{
Width = 100,
Height = 100,
IsSquare = true,
HasTransparency = false,
DominantColorCount = 5,
ContrastRatio = 0.5,
FileFormat = "unknown",
FileSizeBytes = logoBytes.Length
};
}
});
}
private int CalculateReadabilityScore(LogoComplexity complexity, int logoSizePercent, int qrSize)
{
int score = 100; // Base score
// Penalização por tamanho (fator mais importante)
if (logoSizePercent > 20)
{
score -= (logoSizePercent - 20) * 2; // -2 pontos por % acima de 20%
}
// Penalização por complexidade de cores
if (complexity.DominantColorCount > 5)
{
score -= (complexity.DominantColorCount - 5) * 3; // -3 pontos por cor extra
}
// Penalização por baixo contraste
if (complexity.ContrastRatio < 0.3)
{
score -= 10; // Contraste muito baixo
}
// Bonificações
if (complexity.HasTransparency)
{
score += 5; // PNG com transparência é melhor
}
if (complexity.IsSquare)
{
score += 5; // Logo quadrado funciona melhor
}
if (qrSize >= 500)
{
score += 5; // QR codes maiores facilitam leitura
}
// Penalização por logo muito grande em pixels (pode ser mais difícil de processar)
if (complexity.Width > 800 || complexity.Height > 800)
{
score -= 5;
}
// Garantir que o score fique entre 0-100
return Math.Max(0, Math.Min(100, score));
}
private ReadabilityLevel GetDifficultyLevel(int score)
{
return score switch
{
>= 85 => ReadabilityLevel.VeryEasy,
>= 70 => ReadabilityLevel.Easy,
>= 55 => ReadabilityLevel.Medium,
>= 40 => ReadabilityLevel.Hard,
_ => ReadabilityLevel.VeryHard
};
}
private string GenerateUserMessage(int score, int logoSizePercent)
{
return score switch
{
>= 85 => $"✅ Excelente legibilidade! Logo de {logoSizePercent}% será facilmente lido por qualquer celular.",
>= 70 => $"🟢 Boa legibilidade! Logo de {logoSizePercent}% deve funcionar bem na maioria dos dispositivos.",
>= 55 => $"⚠️ Legibilidade moderada. Logo de {logoSizePercent}% pode exigir melhor iluminação ou posicionamento.",
>= 40 => $"🟡 Legibilidade baixa. Logo de {logoSizePercent}% pode ser difícil de ler. Considere reduzir para melhor resultado.",
_ => $"🔴 Legibilidade muito baixa. Logo de {logoSizePercent}% provavelmente será difícil de escanear. Recomendamos reduzir significativamente."
};
}
private List<string> GeneratePersonalizedTips(LogoComplexity complexity, int logoSizePercent, int score)
{
var tips = new List<string>();
// Dicas básicas sempre presentes
tips.Add("📱 Teste sempre em vários apps de QR Code diferentes");
// Dicas baseadas no score
if (score < 70)
{
tips.Add("💡 Use boa iluminação ao escanear para melhor resultado");
}
// Dicas baseadas no tamanho
if (logoSizePercent > 22)
{
tips.Add($"📏 Considere reduzir logo para 18-20% (atual: {logoSizePercent}%)");
}
// Dicas baseadas na complexidade
if (!complexity.HasTransparency)
{
tips.Add("🎨 PNG com fundo transparente melhora a legibilidade");
}
if (!complexity.IsSquare)
{
tips.Add("🔲 Logos quadrados funcionam melhor que retangulares");
}
if (complexity.DominantColorCount > 6)
{
tips.Add("🌈 Logos com menos cores (2-4) são mais fáceis de ler");
}
if (complexity.ContrastRatio < 0.4)
{
tips.Add("⚡ Aumente o contraste do logo para melhor visibilidade");
}
// Dica sobre tamanho do QR
tips.Add("📐 QR Codes maiores (500px+) facilitam leitura com logo");
// Dica final baseada no nível de dificuldade
if (score < 55)
{
tips.Add("🔧 Para máxima compatibilidade, considere versão sem logo para impressões pequenas");
}
return tips;
}
private bool HasTransparency(Image<Rgba32> image)
{
try
{
// Verificar se há pixels com alpha < 255 (transparência)
var hasTransparency = false;
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height && !hasTransparency; y++)
{
var pixelRow = accessor.GetRowSpan(y);
for (int x = 0; x < pixelRow.Length && !hasTransparency; x++)
{
if (pixelRow[x].A < 255)
{
hasTransparency = true;
}
}
}
});
return hasTransparency;
}
catch
{
return false; // Assume não transparente se der erro
}
}
private (int dominantColors, double contrastRatio) AnalyzeColors(Image<Rgba32> image)
{
try
{
var colorCounts = new Dictionary<uint, int>();
var totalPixels = 0;
var brightPixels = 0;
// Amostragem para performance (analisa a cada 4 pixels)
image.ProcessPixelRows(accessor =>
{
for (int y = 0; y < accessor.Height; y += 4)
{
var pixelRow = accessor.GetRowSpan(y);
for (int x = 0; x < pixelRow.Length; x += 4)
{
var pixel = pixelRow[x];
// Ignorar pixels totalmente transparentes
if (pixel.A < 128) continue;
// Quantizar cores para reduzir variações
var quantizedColor = QuantizeColor(pixel);
colorCounts[quantizedColor] = colorCounts.GetValueOrDefault(quantizedColor, 0) + 1;
// Calcular brilho para contraste
var luminance = (pixel.R * 0.299 + pixel.G * 0.587 + pixel.B * 0.114) / 255.0;
if (luminance > 0.5) brightPixels++;
totalPixels++;
}
}
});
var dominantColors = Math.Min(colorCounts.Count, 10); // Limitar a 10 cores dominantes
var contrastRatio = totalPixels > 0 ? (double)brightPixels / totalPixels : 0.5;
return (dominantColors, contrastRatio);
}
catch
{
return (5, 0.5); // Valores padrão em caso de erro
}
}
private uint QuantizeColor(Rgba32 color)
{
// Quantizar cores para 32 níveis (reduz variações mínimas)
var r = (byte)((color.R / 8) * 8);
var g = (byte)((color.G / 8) * 8);
var b = (byte)((color.B / 8) * 8);
return (uint)(r << 16 | g << 8 | b);
}
private string DetectImageFormat(byte[] imageBytes)
{
if (imageBytes.Length < 4) return "unknown";
// PNG signature
if (imageBytes[0] == 0x89 && imageBytes[1] == 0x50 && imageBytes[2] == 0x4E && imageBytes[3] == 0x47)
return "PNG";
// JPEG signature
if (imageBytes[0] == 0xFF && imageBytes[1] == 0xD8)
return "JPEG";
// GIF signature
if (imageBytes[0] == 0x47 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46)
return "GIF";
// WebP signature
if (imageBytes.Length > 12 &&
imageBytes[0] == 0x52 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46 && imageBytes[3] == 0x46 &&
imageBytes[8] == 0x57 && imageBytes[9] == 0x45 && imageBytes[10] == 0x42 && imageBytes[11] == 0x50)
return "WebP";
return "unknown";
}
}
}

View File

@ -20,12 +20,14 @@ namespace QRRapidoApp.Services
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly ILogger<QRRapidoService> _logger; private readonly ILogger<QRRapidoService> _logger;
private readonly SemaphoreSlim _semaphore; private readonly SemaphoreSlim _semaphore;
private readonly LogoReadabilityAnalyzer _readabilityAnalyzer;
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger) public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger, LogoReadabilityAnalyzer readabilityAnalyzer)
{ {
_cache = cache; _cache = cache;
_config = config; _config = config;
_logger = logger; _logger = logger;
_readabilityAnalyzer = readabilityAnalyzer;
// Limit simultaneous generations to maintain performance // Limit simultaneous generations to maintain performance
var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100); var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100);
@ -54,6 +56,13 @@ namespace QRRapidoApp.Services
{ {
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds; cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
cachedResult.FromCache = true; 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; return cachedResult;
} }
} }
@ -62,6 +71,9 @@ namespace QRRapidoApp.Services
var qrCode = await GenerateQRCodeOptimizedAsync(request); var qrCode = await GenerateQRCodeOptimizedAsync(request);
var base64 = Convert.ToBase64String(qrCode); var base64 = Convert.ToBase64String(qrCode);
// Análise de legibilidade do logo
var readabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size);
var result = new QRGenerationResult var result = new QRGenerationResult
{ {
QRCodeBase64 = base64, QRCodeBase64 = base64,
@ -70,7 +82,8 @@ namespace QRRapidoApp.Services
FromCache = false, FromCache = false,
Size = qrCode.Length, Size = qrCode.Length,
RequestSettings = request, RequestSettings = request,
Success = true Success = true,
ReadabilityInfo = readabilityInfo
}; };
// Cache for configurable time // Cache for configurable time

View File

@ -1,6 +1,7 @@
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Localization;
using Moq; using Moq;
using QRRapidoApp.Models.ViewModels; using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services; using QRRapidoApp.Services;
@ -14,6 +15,7 @@ namespace QRRapidoApp.Tests.Services
private readonly Mock<IDistributedCache> _cacheMock; private readonly Mock<IDistributedCache> _cacheMock;
private readonly Mock<IConfiguration> _configMock; private readonly Mock<IConfiguration> _configMock;
private readonly Mock<ILogger<QRRapidoService>> _loggerMock; private readonly Mock<ILogger<QRRapidoService>> _loggerMock;
private readonly Mock<LogoReadabilityAnalyzer> _readabilityAnalyzerMock;
private readonly QRRapidoService _service; private readonly QRRapidoService _service;
public QRRapidoServiceTests() public QRRapidoServiceTests()
@ -21,9 +23,10 @@ namespace QRRapidoApp.Tests.Services
_cacheMock = new Mock<IDistributedCache>(); _cacheMock = new Mock<IDistributedCache>();
_configMock = new Mock<IConfiguration>(); _configMock = new Mock<IConfiguration>();
_loggerMock = new Mock<ILogger<QRRapidoService>>(); _loggerMock = new Mock<ILogger<QRRapidoService>>();
_readabilityAnalyzerMock = new Mock<LogoReadabilityAnalyzer>(Mock.Of<ILogger<LogoReadabilityAnalyzer>>(), Mock.Of<Microsoft.Extensions.Localization.IStringLocalizer<LogoReadabilityAnalyzer>>());
SetupDefaultConfiguration(); SetupDefaultConfiguration();
_service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object); _service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object, _readabilityAnalyzerMock.Object);
} }
private void SetupDefaultConfiguration() private void SetupDefaultConfiguration()

View File

@ -78,6 +78,18 @@ class QRRapidoGenerator {
logoUpload.addEventListener('change', this.handleLogoSelection.bind(this)); logoUpload.addEventListener('change', this.handleLogoSelection.bind(this));
} }
// Logo size slider preview em tempo real
const logoSizeSlider = document.getElementById('logo-size-slider');
if (logoSizeSlider && typeof this.updateLogoReadabilityPreview === 'function') {
logoSizeSlider.addEventListener('input', this.updateLogoReadabilityPreview.bind(this));
}
// Logo colorize toggle preview em tempo real
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
if (logoColorizeToggle && typeof this.updateLogoReadabilityPreview === 'function') {
logoColorizeToggle.addEventListener('change', this.updateLogoReadabilityPreview.bind(this));
}
// Corner style validation for non-premium users // Corner style validation for non-premium users
const cornerStyle = document.getElementById('corner-style'); const cornerStyle = document.getElementById('corner-style');
if (cornerStyle) { if (cornerStyle) {
@ -506,7 +518,20 @@ class QRRapidoGenerator {
let content, actualType; let content, actualType;
if (type === 'url') { if (type === 'url') {
const dynamicData = window.dynamicQRManager.getDynamicQRData(); let dynamicData;
if (typeof this.getDynamicQRData === 'function') {
dynamicData = this.getDynamicQRData();
} else {
// Fallback se a função não existir
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
dynamicData = {
isDynamic: isDynamic,
originalUrl: document.getElementById('qr-content').value,
requiresPremium: !this.isPremium && isDynamic
};
}
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.');
} }
@ -558,7 +583,19 @@ class QRRapidoGenerator {
// Add dynamic QR data if it's a URL type // Add dynamic QR data if it's a URL type
if (type === 'url') { if (type === 'url') {
const dynamicData = window.dynamicQRManager.getDynamicQRData(); let dynamicData;
if (typeof this.getDynamicQRData === 'function') {
dynamicData = this.getDynamicQRData();
} else {
// Fallback se a função não existir
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
dynamicData = {
isDynamic: isDynamic,
originalUrl: document.getElementById('qr-content').value,
requiresPremium: !this.isPremium && isDynamic
};
}
commonData.isDynamic = dynamicData.isDynamic; commonData.isDynamic = dynamicData.isDynamic;
} }
@ -623,7 +660,19 @@ class QRRapidoGenerator {
// Função auxiliar para obter conteúdo baseado no tipo // Função auxiliar para obter conteúdo baseado no tipo
getContentForType(type) { getContentForType(type) {
if (type === 'url') { if (type === 'url') {
const dynamicData = window.dynamicQRManager?.getDynamicQRData(); let dynamicData;
if (typeof this.getDynamicQRData === 'function') {
dynamicData = this.getDynamicQRData();
} else {
// Fallback se a função não existir
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
dynamicData = {
isDynamic: isDynamic,
originalUrl: document.getElementById('qr-content').value,
requiresPremium: !this.isPremium && isDynamic
};
}
return dynamicData?.originalUrl || document.getElementById('qr-content').value; return dynamicData?.originalUrl || document.getElementById('qr-content').value;
} else if (type === 'wifi') { } else if (type === 'wifi') {
return window.wifiGenerator?.generateWiFiString() || ''; return window.wifiGenerator?.generateWiFiString() || '';
@ -661,6 +710,11 @@ class QRRapidoGenerator {
style="image-rendering: crisp-edges;"> style="image-rendering: crisp-edges;">
`; `;
// Exibir análise de legibilidade do logo se disponível
if (result.readabilityInfo && typeof this.displayReadabilityAnalysis === 'function') {
this.displayReadabilityAnalysis(result.readabilityInfo);
}
// CORREÇÃO: Log para debug - verificar se QR code tem logo // CORREÇÃO: Log para debug - verificar se QR code tem logo
const logoUpload = document.getElementById('logo-upload'); const logoUpload = document.getElementById('logo-upload');
const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0; const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0;
@ -668,7 +722,8 @@ class QRRapidoGenerator {
hasLogo: hasLogo, hasLogo: hasLogo,
logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum', logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum',
generationTime: generationTime + 's', generationTime: generationTime + 's',
imageSize: result.qrCodeBase64.length + ' chars' imageSize: result.qrCodeBase64.length + ' chars',
readabilityScore: result.readabilityInfo?.readabilityScore
}); });
// Show generation statistics // Show generation statistics
@ -685,7 +740,8 @@ class QRRapidoGenerator {
base64: result.qrCodeBase64, base64: result.qrCodeBase64,
qrCodeBase64: result.qrCodeBase64, // Both properties for compatibility qrCodeBase64: result.qrCodeBase64, // Both properties for compatibility
id: result.qrId, id: result.qrId,
generationTime: generationTime generationTime: generationTime,
readabilityInfo: result.readabilityInfo
}; };
// Increment rate limit counter after successful generation // Increment rate limit counter after successful generation
@ -916,6 +972,11 @@ class QRRapidoGenerator {
type: file.type, type: file.type,
timestamp: new Date().toLocaleTimeString() timestamp: new Date().toLocaleTimeString()
}); });
// Atualizar preview de legibilidade
if (typeof this.updateLogoReadabilityPreview === 'function') {
this.updateLogoReadabilityPreview();
}
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} else { } else {
@ -925,6 +986,11 @@ class QRRapidoGenerator {
if (logoVisualPreview) logoVisualPreview.style.display = 'none'; if (logoVisualPreview) logoVisualPreview.style.display = 'none';
if (logoPreviewImage) logoPreviewImage.src = ''; if (logoPreviewImage) logoPreviewImage.src = '';
// Limpar preview de legibilidade
if (typeof this.clearReadabilityPreview === 'function') {
this.clearReadabilityPreview();
}
console.log('🗑️ Logo removido'); console.log('🗑️ Logo removido');
} }
} }
@ -2424,6 +2490,203 @@ class DynamicQRManager {
requiresPremium: !this.isPremium && isDynamic requiresPremium: !this.isPremium && isDynamic
}; };
} }
displayReadabilityAnalysis(readabilityInfo) {
if (!readabilityInfo || !readabilityInfo.hasLogo) return;
// Criar ou encontrar container para análise de legibilidade
let analysisContainer = document.getElementById('readability-analysis');
if (!analysisContainer) {
// Criar container se não existir
analysisContainer = document.createElement('div');
analysisContainer.id = 'readability-analysis';
analysisContainer.className = 'mt-3 p-3 border rounded bg-light';
// Inserir após o preview do QR code
const previewDiv = document.getElementById('qr-preview');
if (previewDiv && previewDiv.parentNode) {
previewDiv.parentNode.insertBefore(analysisContainer, previewDiv.nextSibling);
}
}
// Determinar classe de cor baseada no score
const getScoreClass = (score) => {
if (score >= 85) return 'text-success';
if (score >= 70) return 'text-info';
if (score >= 55) return 'text-warning';
return 'text-danger';
};
// Determinar ícone baseado no nível de dificuldade
const getIconClass = (score) => {
if (score >= 85) return 'fas fa-check-circle';
if (score >= 70) return 'fas fa-thumbs-up';
if (score >= 55) return 'fas fa-exclamation-triangle';
return 'fas fa-exclamation-circle';
};
// Gerar lista de dicas
const tipsHtml = readabilityInfo.tips && readabilityInfo.tips.length > 0
? `<div class="collapse mt-2" id="readabilityTips">
<ul class="list-unstyled mb-0">
${readabilityInfo.tips.map(tip => `<li class="mb-1"><small>${tip}</small></li>`).join('')}
</ul>
</div>`
: '';
// Criar HTML da análise
const scoreClass = getScoreClass(readabilityInfo.readabilityScore);
const iconClass = getIconClass(readabilityInfo.readabilityScore);
analysisContainer.innerHTML = `
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<i class="${iconClass} ${scoreClass} me-2"></i>
<div>
<h6 class="mb-1 ${scoreClass}">Análise de Legibilidade</h6>
<p class="mb-0 small">${readabilityInfo.userMessage}</p>
</div>
</div>
<div class="text-end">
<div class="badge bg-secondary">${readabilityInfo.readabilityScore}/100</div>
${readabilityInfo.tips && readabilityInfo.tips.length > 0
? `<button class="btn btn-sm btn-outline-secondary ms-2" type="button"
data-bs-toggle="collapse" data-bs-target="#readabilityTips">
<i class="fas fa-lightbulb"></i> Dicas
</button>`
: ''}
</div>
</div>
${tipsHtml}
`;
// Animação de entrada suave
analysisContainer.style.opacity = '0';
analysisContainer.style.transform = 'translateY(-10px)';
setTimeout(() => {
analysisContainer.style.transition = 'all 0.3s ease';
analysisContainer.style.opacity = '1';
analysisContainer.style.transform = 'translateY(0)';
}, 100);
console.log('📊 Análise de legibilidade exibida:', {
score: readabilityInfo.readabilityScore,
level: readabilityInfo.difficultyLevel,
logoSize: readabilityInfo.logoSizePercent + '%',
tipsCount: readabilityInfo.tips?.length || 0
});
}
updateLogoReadabilityPreview() {
const logoUpload = document.getElementById('logo-upload');
const logoSizeSlider = document.getElementById('logo-size-slider');
// Só mostrar preview se há logo selecionado
if (!logoUpload || !logoUpload.files || logoUpload.files.length === 0) {
this.clearReadabilityPreview();
return;
}
const logoSize = parseInt(logoSizeSlider?.value || '20');
// Calcular score estimado baseado apenas no tamanho (simplificado para preview)
let estimatedScore = 100;
if (logoSize > 20) {
estimatedScore -= (logoSize - 20) * 2;
}
estimatedScore = Math.max(40, estimatedScore); // Mínimo de 40
// Simular análise básica para preview
const previewInfo = {
hasLogo: true,
logoSizePercent: logoSize,
readabilityScore: estimatedScore,
difficultyLevel: this.getDifficultyLevelFromScore(estimatedScore),
userMessage: this.generatePreviewMessage(estimatedScore, logoSize),
tips: this.generatePreviewTips(logoSize, estimatedScore)
};
this.displayReadabilityPreview(previewInfo);
}
getDifficultyLevelFromScore(score) {
if (score >= 85) return 'VeryEasy';
if (score >= 70) return 'Easy';
if (score >= 55) return 'Medium';
if (score >= 40) return 'Hard';
return 'VeryHard';
}
generatePreviewMessage(score, logoSize) {
if (score >= 85) {
return `✅ Estimativa: Excelente legibilidade com logo de ${logoSize}%`;
} else if (score >= 70) {
return `🟢 Estimativa: Boa legibilidade com logo de ${logoSize}%`;
} else if (score >= 55) {
return `⚠️ Estimativa: Legibilidade moderada com logo de ${logoSize}%`;
} else {
return `🟡 Estimativa: Legibilidade baixa com logo de ${logoSize}%`;
}
}
generatePreviewTips(logoSize, score) {
const tips = [];
if (logoSize > 22) {
tips.push(`📏 Considere reduzir para 18-20% (atual: ${logoSize}%)`);
}
if (score < 70) {
tips.push('💡 Use boa iluminação ao escanear');
}
tips.push('🎨 PNG transparente melhora a legibilidade');
tips.push('📱 Teste em vários apps de QR Code');
return tips;
}
displayReadabilityPreview(previewInfo) {
// Usar a mesma função de display, mas com ID diferente para preview
let previewContainer = document.getElementById('readability-preview');
if (!previewContainer) {
previewContainer = document.createElement('div');
previewContainer.id = 'readability-preview';
previewContainer.className = 'mt-2 p-2 border rounded bg-info bg-opacity-10 border-info border-opacity-25';
// Inserir após o slider de tamanho do logo
const logoSizeSlider = document.getElementById('logo-size-slider');
if (logoSizeSlider && logoSizeSlider.parentNode) {
logoSizeSlider.parentNode.insertBefore(previewContainer, logoSizeSlider.nextSibling);
}
}
const scoreClass = previewInfo.readabilityScore >= 85 ? 'text-success' :
previewInfo.readabilityScore >= 70 ? 'text-info' :
previewInfo.readabilityScore >= 55 ? 'text-warning' : 'text-danger';
const iconClass = previewInfo.readabilityScore >= 85 ? 'fas fa-check-circle' :
previewInfo.readabilityScore >= 70 ? 'fas fa-thumbs-up' :
previewInfo.readabilityScore >= 55 ? 'fas fa-exclamation-triangle' : 'fas fa-exclamation-circle';
previewContainer.innerHTML = `
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<i class="${iconClass} ${scoreClass} me-2"></i>
<small class="${scoreClass}">${previewInfo.userMessage}</small>
</div>
<div class="badge bg-secondary">${previewInfo.readabilityScore}/100</div>
</div>
`;
}
clearReadabilityPreview() {
const previewContainer = document.getElementById('readability-preview');
if (previewContainer) {
previewContainer.remove();
}
}
} }
class SMSQRGenerator { class SMSQRGenerator {