feat: add image validation and ASCII conversion services

This commit is contained in:
HueByte
2026-07-16 03:50:08 +02:00
parent 3ca9dbfd91
commit ea8e583ee5
2 changed files with 0 additions and 0 deletions
@@ -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();
}
}