348 lines
14 KiB
C#
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";
|
|
}
|
|
}
|
|
} |