QrRapido/Services/LogoReadabilityAnalyzer.cs
Ricardo Carneiro 8ab0b913e2
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
fix: Melhorar a leitura mesmo com o logotipo maior.
2025-08-04 01:50:54 -03:00

348 lines
14 KiB
C#

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";
}
}
}