From 8ab0b913e2638d51ce9ca607a1e879d06a674987 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Mon, 4 Aug 2025 01:50:54 -0300 Subject: [PATCH] fix: Melhorar a leitura mesmo com o logotipo maior. --- .claude/settings.local.json | 4 +- Models/LogoReadability.cs | 34 +++ Models/ViewModels/QRGenerationRequest.cs | 3 + Program.cs | 1 + Services/LogoReadabilityAnalyzer.cs | 348 +++++++++++++++++++++++ Services/QRRapidoService.cs | 17 +- Tests/Services/QRRapidoServiceTests.cs | 5 +- wwwroot/js/qr-speed-generator.js | 273 +++++++++++++++++- 8 files changed, 676 insertions(+), 9 deletions(-) create mode 100644 Models/LogoReadability.cs create mode 100644 Services/LogoReadabilityAnalyzer.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c7e95d3..ab4d209 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,7 +21,9 @@ "Bash(convert:*)", "Bash(dotnet add package:*)", "Bash(dotnet remove package:*)", - "Bash(dotnet restore:*)" + "Bash(dotnet restore:*)", + "Bash(rg:*)", + "Bash(dotnet test:*)" ], "deny": [] } diff --git a/Models/LogoReadability.cs b/Models/LogoReadability.cs new file mode 100644 index 0000000..71fe0ba --- /dev/null +++ b/Models/LogoReadability.cs @@ -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 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 + } +} \ No newline at end of file diff --git a/Models/ViewModels/QRGenerationRequest.cs b/Models/ViewModels/QRGenerationRequest.cs index b2c1d11..0b71c75 100644 --- a/Models/ViewModels/QRGenerationRequest.cs +++ b/Models/ViewModels/QRGenerationRequest.cs @@ -1,3 +1,5 @@ +using QRRapidoApp.Models; + namespace QRRapidoApp.Models.ViewModels { public class QRGenerationRequest @@ -39,5 +41,6 @@ namespace QRRapidoApp.Models.ViewModels public int? RemainingQRs { get; set; } // For free users public bool Success { get; set; } = true; public string? ErrorMessage { get; set; } + public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index add4295..a83387b 100644 --- a/Program.cs +++ b/Program.cs @@ -181,6 +181,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Background Services builder.Services.AddHostedService(); diff --git a/Services/LogoReadabilityAnalyzer.cs b/Services/LogoReadabilityAnalyzer.cs new file mode 100644 index 0000000..450d3db --- /dev/null +++ b/Services/LogoReadabilityAnalyzer.cs @@ -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 _logger; + private readonly IStringLocalizer _localizer; + + public LogoReadabilityAnalyzer(ILogger logger, IStringLocalizer localizer) + { + _logger = logger; + _localizer = localizer; + } + + public async Task 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 { "⚠️ Análise limitada devido a erro técnico. Teste sempre em vários dispositivos." }; + + return result; + } + } + + private async Task AnalyzeLogoComplexityAsync(byte[] logoBytes) + { + return await Task.Run(() => + { + try + { + using var stream = new MemoryStream(logoBytes); + using var image = Image.Load(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 GeneratePersonalizedTips(LogoComplexity complexity, int logoSizePercent, int score) + { + var tips = new List(); + + // 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 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 image) + { + try + { + var colorCounts = new Dictionary(); + 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"; + } + } +} \ No newline at end of file diff --git a/Services/QRRapidoService.cs b/Services/QRRapidoService.cs index e8f872f..0dc4ec6 100644 --- a/Services/QRRapidoService.cs +++ b/Services/QRRapidoService.cs @@ -20,12 +20,14 @@ namespace QRRapidoApp.Services private readonly IConfiguration _config; private readonly ILogger _logger; private readonly SemaphoreSlim _semaphore; + private readonly LogoReadabilityAnalyzer _readabilityAnalyzer; - public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger logger) + public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger logger, LogoReadabilityAnalyzer readabilityAnalyzer) { _cache = cache; _config = config; _logger = logger; + _readabilityAnalyzer = readabilityAnalyzer; // Limit simultaneous generations to maintain performance var maxConcurrent = _config.GetValue("Performance:MaxConcurrentGenerations", 100); @@ -54,6 +56,13 @@ namespace QRRapidoApp.Services { cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds; cachedResult.FromCache = true; + + // Se não tem análise de legibilidade no cache, gerar agora + if (cachedResult.ReadabilityInfo == null) + { + cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size); + } + return cachedResult; } } @@ -62,6 +71,9 @@ namespace QRRapidoApp.Services var qrCode = await GenerateQRCodeOptimizedAsync(request); var base64 = Convert.ToBase64String(qrCode); + // Análise de legibilidade do logo + var readabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size); + var result = new QRGenerationResult { QRCodeBase64 = base64, @@ -70,7 +82,8 @@ namespace QRRapidoApp.Services FromCache = false, Size = qrCode.Length, RequestSettings = request, - Success = true + Success = true, + ReadabilityInfo = readabilityInfo }; // Cache for configurable time diff --git a/Tests/Services/QRRapidoServiceTests.cs b/Tests/Services/QRRapidoServiceTests.cs index 9bef90e..4bd14d4 100644 --- a/Tests/Services/QRRapidoServiceTests.cs +++ b/Tests/Services/QRRapidoServiceTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; using Moq; using QRRapidoApp.Models.ViewModels; using QRRapidoApp.Services; @@ -14,6 +15,7 @@ namespace QRRapidoApp.Tests.Services private readonly Mock _cacheMock; private readonly Mock _configMock; private readonly Mock> _loggerMock; + private readonly Mock _readabilityAnalyzerMock; private readonly QRRapidoService _service; public QRRapidoServiceTests() @@ -21,9 +23,10 @@ namespace QRRapidoApp.Tests.Services _cacheMock = new Mock(); _configMock = new Mock(); _loggerMock = new Mock>(); + _readabilityAnalyzerMock = new Mock(Mock.Of>(), Mock.Of>()); SetupDefaultConfiguration(); - _service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object); + _service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object, _readabilityAnalyzerMock.Object); } private void SetupDefaultConfiguration() diff --git a/wwwroot/js/qr-speed-generator.js b/wwwroot/js/qr-speed-generator.js index d05df0e..d6c384d 100644 --- a/wwwroot/js/qr-speed-generator.js +++ b/wwwroot/js/qr-speed-generator.js @@ -78,6 +78,18 @@ class QRRapidoGenerator { 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 const cornerStyle = document.getElementById('corner-style'); if (cornerStyle) { @@ -506,7 +518,20 @@ class QRRapidoGenerator { let content, actualType; 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) { 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 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; } @@ -623,7 +660,19 @@ class QRRapidoGenerator { // Função auxiliar para obter conteúdo baseado no tipo getContentForType(type) { 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; } else if (type === 'wifi') { return window.wifiGenerator?.generateWiFiString() || ''; @@ -661,6 +710,11 @@ class QRRapidoGenerator { 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 const logoUpload = document.getElementById('logo-upload'); const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0; @@ -668,7 +722,8 @@ class QRRapidoGenerator { hasLogo: hasLogo, logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum', generationTime: generationTime + 's', - imageSize: result.qrCodeBase64.length + ' chars' + imageSize: result.qrCodeBase64.length + ' chars', + readabilityScore: result.readabilityInfo?.readabilityScore }); // Show generation statistics @@ -685,7 +740,8 @@ class QRRapidoGenerator { base64: result.qrCodeBase64, qrCodeBase64: result.qrCodeBase64, // Both properties for compatibility id: result.qrId, - generationTime: generationTime + generationTime: generationTime, + readabilityInfo: result.readabilityInfo }; // Increment rate limit counter after successful generation @@ -916,6 +972,11 @@ class QRRapidoGenerator { type: file.type, timestamp: new Date().toLocaleTimeString() }); + + // Atualizar preview de legibilidade + if (typeof this.updateLogoReadabilityPreview === 'function') { + this.updateLogoReadabilityPreview(); + } }; reader.readAsDataURL(file); } else { @@ -925,6 +986,11 @@ class QRRapidoGenerator { if (logoVisualPreview) logoVisualPreview.style.display = 'none'; if (logoPreviewImage) logoPreviewImage.src = ''; + // Limpar preview de legibilidade + if (typeof this.clearReadabilityPreview === 'function') { + this.clearReadabilityPreview(); + } + console.log('🗑️ Logo removido'); } } @@ -2424,6 +2490,203 @@ class DynamicQRManager { 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 + ? `
+
    + ${readabilityInfo.tips.map(tip => `
  • ${tip}
  • `).join('')} +
+
` + : ''; + + // Criar HTML da análise + const scoreClass = getScoreClass(readabilityInfo.readabilityScore); + const iconClass = getIconClass(readabilityInfo.readabilityScore); + + analysisContainer.innerHTML = ` +
+
+ +
+
Análise de Legibilidade
+

${readabilityInfo.userMessage}

+
+
+
+
${readabilityInfo.readabilityScore}/100
+ ${readabilityInfo.tips && readabilityInfo.tips.length > 0 + ? `` + : ''} +
+
+ ${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 = ` +
+
+ + ${previewInfo.userMessage} +
+
${previewInfo.readabilityScore}/100
+
+ `; + } + + clearReadabilityPreview() { + const previewContainer = document.getElementById('readability-preview'); + if (previewContainer) { + previewContainer.remove(); + } + } } class SMSQRGenerator {