mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 16:46:08 +02:00
feat: add image validation and ASCII conversion services
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
namespace EchoHub.Server.Services;
|
||||
|
||||
public static class FileValidationHelper
|
||||
{
|
||||
private static readonly byte[] JpegMagic = [0xFF, 0xD8, 0xFF];
|
||||
private static readonly byte[] PngMagic = [0x89, 0x50, 0x4E, 0x47];
|
||||
private static readonly byte[] GifMagic = [0x47, 0x49, 0x46];
|
||||
private static readonly byte[] WebpRiff = [0x52, 0x49, 0x46, 0x46]; // "RIFF"
|
||||
private static readonly byte[] WebpTag = [0x57, 0x45, 0x42, 0x50]; // "WEBP"
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a stream contains a recognized image format by checking magic bytes.
|
||||
/// The stream position is reset to the beginning after validation.
|
||||
/// </summary>
|
||||
public static bool IsValidImage(Stream stream)
|
||||
{
|
||||
if (!stream.CanSeek)
|
||||
return false;
|
||||
|
||||
var originalPosition = stream.Position;
|
||||
try
|
||||
{
|
||||
var header = new byte[12];
|
||||
var bytesRead = stream.Read(header, 0, header.Length);
|
||||
|
||||
if (bytesRead < 3)
|
||||
return false;
|
||||
|
||||
// JPEG: FF D8 FF
|
||||
if (StartsWith(header, bytesRead, JpegMagic))
|
||||
return true;
|
||||
|
||||
// PNG: 89 50 4E 47
|
||||
if (bytesRead >= 4 && StartsWith(header, bytesRead, PngMagic))
|
||||
return true;
|
||||
|
||||
// GIF: 47 49 46 (GIF87a or GIF89a)
|
||||
if (StartsWith(header, bytesRead, GifMagic))
|
||||
return true;
|
||||
|
||||
// WebP: RIFF....WEBP
|
||||
if (bytesRead >= 12 && StartsWith(header, bytesRead, WebpRiff)
|
||||
&& header[8] == WebpTag[0] && header[9] == WebpTag[1]
|
||||
&& header[10] == WebpTag[2] && header[11] == WebpTag[3])
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stream.Position = originalPosition;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> AudioExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".mp3", ".wav", ".ogg", ".flac", ".aac", ".m4a", ".wma"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the file name has a recognized audio extension.
|
||||
/// </summary>
|
||||
public static bool IsAudioFile(string fileName)
|
||||
{
|
||||
var ext = Path.GetExtension(fileName);
|
||||
return !string.IsNullOrEmpty(ext) && AudioExtensions.Contains(ext);
|
||||
}
|
||||
|
||||
private static bool StartsWith(byte[] buffer, int length, byte[] magic)
|
||||
{
|
||||
if (length < magic.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < magic.Length; i++)
|
||||
{
|
||||
if (buffer[i] != magic[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
using System.Text;
|
||||
using EchoHub.Core.Constants;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace EchoHub.Server.Services;
|
||||
|
||||
public class ImageToAsciiService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns (width, height) dimensions for the given size code.
|
||||
/// s = small (40x40), m = medium/default (80x80), l = large (120x120).
|
||||
/// </summary>
|
||||
public static (int Width, int Height) GetDimensions(string? size) => size?.ToLowerInvariant() switch
|
||||
{
|
||||
"s" => (40, 40),
|
||||
"l" => (120, 120),
|
||||
_ => (HubConstants.AsciiArtWidth, HubConstants.AsciiArtHeightHalfBlock),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Converts an image to ASCII art using half-block characters (▀▄█) with
|
||||
/// printable color tags for 2x vertical resolution.
|
||||
/// Each character cell represents two vertical pixels.
|
||||
/// Format: {F:RRGGBB} foreground, {B:RRGGBB} background, {X} reset.
|
||||
/// Uses only printable ASCII — no terminal escape bytes.
|
||||
/// </summary>
|
||||
public string ConvertToAscii(Stream imageStream, int width = HubConstants.AsciiArtWidth, int height = HubConstants.AsciiArtHeightHalfBlock)
|
||||
{
|
||||
using var image = Image.Load<Rgba32>(imageStream);
|
||||
|
||||
// Ensure height is even for pair processing
|
||||
if (height % 2 != 0) height++;
|
||||
|
||||
image.Mutate(x => x.Resize(width, height));
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (int y = 0; y < image.Height; y += 2)
|
||||
{
|
||||
byte lastFgR = 0, lastFgG = 0, lastFgB = 0;
|
||||
byte lastBgR = 0, lastBgG = 0, lastBgB = 0;
|
||||
bool hasLastColor = false;
|
||||
|
||||
for (int x = 0; x < image.Width; x++)
|
||||
{
|
||||
var topPixel = image[x, y];
|
||||
var bottomPixel = (y + 1 < image.Height) ? image[x, y + 1] : topPixel;
|
||||
|
||||
byte fgR, fgG, fgB, bgR, bgG, bgB;
|
||||
char blockChar;
|
||||
|
||||
if (topPixel.R == bottomPixel.R && topPixel.G == bottomPixel.G && topPixel.B == bottomPixel.B)
|
||||
{
|
||||
fgR = topPixel.R; fgG = topPixel.G; fgB = topPixel.B;
|
||||
bgR = topPixel.R; bgG = topPixel.G; bgB = topPixel.B;
|
||||
blockChar = '\u2588'; // █
|
||||
}
|
||||
else
|
||||
{
|
||||
fgR = topPixel.R; fgG = topPixel.G; fgB = topPixel.B;
|
||||
bgR = bottomPixel.R; bgG = bottomPixel.G; bgB = bottomPixel.B;
|
||||
blockChar = '\u2580'; // ▀
|
||||
}
|
||||
|
||||
bool fgChanged = !hasLastColor || fgR != lastFgR || fgG != lastFgG || fgB != lastFgB;
|
||||
bool bgChanged = !hasLastColor || bgR != lastBgR || bgG != lastBgG || bgB != lastBgB;
|
||||
|
||||
if (fgChanged)
|
||||
sb.Append($"{{F:{fgR:X2}{fgG:X2}{fgB:X2}}}");
|
||||
if (bgChanged)
|
||||
sb.Append($"{{B:{bgR:X2}{bgG:X2}{bgB:X2}}}");
|
||||
|
||||
sb.Append(blockChar);
|
||||
|
||||
lastFgR = fgR; lastFgG = fgG; lastFgB = fgB;
|
||||
lastBgR = bgR; lastBgG = bgG; lastBgB = bgB;
|
||||
hasLastColor = true;
|
||||
}
|
||||
|
||||
sb.Append("{X}");
|
||||
hasLastColor = false;
|
||||
|
||||
if (y + 2 < image.Height)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user