167 lines
No EOL
6 KiB
C#
167 lines
No EOL
6 KiB
C#
using SkiaSharp;
|
|
using System;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace LarpixServer.Account;
|
|
|
|
public class Captcha
|
|
{
|
|
private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
private static readonly SKTypeface CaptchaTypeface = SKTypeface.FromFamilyName("Google Sans Code", SKFontStyle.Bold);
|
|
|
|
public static (byte[] ImageBytes, string CaptchaText) GenerateCaptcha()
|
|
{
|
|
int width = 1120;
|
|
int height = 320;
|
|
int charCount = 8;
|
|
|
|
Span<char> captchaSpan = stackalloc char[charCount];
|
|
Span<SKColor> charColors = stackalloc SKColor[charCount];
|
|
|
|
for (int i = 0; i < charCount; i++)
|
|
{
|
|
captchaSpan[i] = Chars[Random.Shared.Next(Chars.Length)];
|
|
charColors[i] = new SKColor(
|
|
(byte)Random.Shared.Next(100, 256),
|
|
(byte)Random.Shared.Next(100, 256),
|
|
(byte)Random.Shared.Next(100, 256)
|
|
);
|
|
}
|
|
string captchaText = new string(captchaSpan);
|
|
|
|
var imageInfo = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
|
|
using var sourceBitmap = new SKBitmap(imageInfo);
|
|
using var canvas = new SKCanvas(sourceBitmap);
|
|
canvas.Clear(SKColors.Transparent);
|
|
|
|
using var dotPaint = new SKPaint { IsAntialias = true };
|
|
float spacing = 12f;
|
|
|
|
Span<float> randOffsets = stackalloc float[256];
|
|
Span<float> randRadii = stackalloc float[256];
|
|
Span<SKColor> randColors = stackalloc SKColor[256];
|
|
|
|
for (int i = 0; i < 256; i++)
|
|
{
|
|
randOffsets[i] = (float)(Random.Shared.NextDouble() - 0.5) * 8f;
|
|
randRadii[i] = (float)Random.Shared.NextDouble() * 3.5f + 1f;
|
|
randColors[i] = new SKColor(
|
|
(byte)Random.Shared.Next(120, 200),
|
|
(byte)Random.Shared.Next(120, 200),
|
|
(byte)Random.Shared.Next(120, 200)
|
|
);
|
|
}
|
|
|
|
int noiseIdx = 0;
|
|
for (float y = 0; y < height; y += spacing)
|
|
{
|
|
for (float x = 0; x < width; x += spacing)
|
|
{
|
|
noiseIdx = (noiseIdx + 1) & 255;
|
|
dotPaint.Color = randColors[noiseIdx];
|
|
canvas.DrawCircle(x + randOffsets[noiseIdx], y + randOffsets[255 - noiseIdx], randRadii[noiseIdx], dotPaint);
|
|
}
|
|
}
|
|
|
|
using var textPaint = new SKPaint
|
|
{
|
|
IsAntialias = true,
|
|
TextSize = 158,
|
|
Typeface = CaptchaTypeface
|
|
};
|
|
|
|
float cellWidth = width / (float)charCount;
|
|
|
|
for (int i = 0; i < charCount; i++)
|
|
{
|
|
string charStr = captchaText[i].ToString();
|
|
SKRect bounds = new SKRect();
|
|
textPaint.MeasureText(charStr, ref bounds);
|
|
textPaint.Color = charColors[i];
|
|
|
|
canvas.Save();
|
|
|
|
float maxRadius = (float)Math.Sqrt((bounds.Width * bounds.Width) + (bounds.Height * bounds.Height)) / 2f;
|
|
float margin = 5f;
|
|
|
|
float minX = (cellWidth * i) + maxRadius + margin;
|
|
float maxX = (cellWidth * (i + 1)) - maxRadius - margin;
|
|
float xCenter = (minX >= maxX) ? (cellWidth * i) + (cellWidth / 2f) : minX + (float)Random.Shared.NextDouble() * (maxX - minX);
|
|
|
|
float minY = maxRadius + margin;
|
|
float maxY = height - maxRadius - margin;
|
|
float yCenter = (minY >= maxY) ? height / 2f : minY + (float)Random.Shared.NextDouble() * (maxY - minY);
|
|
|
|
canvas.Translate(xCenter, yCenter);
|
|
canvas.RotateDegrees(Random.Shared.Next(-45, 46));
|
|
canvas.DrawText(charStr, -bounds.MidX, -bounds.MidY, textPaint);
|
|
canvas.Restore();
|
|
}
|
|
|
|
using var linePaint = new SKPaint { StrokeWidth = 8, IsAntialias = true };
|
|
|
|
for (int i = 0; i < charCount; i++)
|
|
{
|
|
linePaint.Color = charColors[i];
|
|
float cellStartX = cellWidth * i;
|
|
float x1 = cellStartX + (float)Random.Shared.NextDouble() * cellWidth;
|
|
float x2 = cellStartX + (float)Random.Shared.NextDouble() * cellWidth;
|
|
canvas.DrawLine(x1, 0, x2, height, linePaint);
|
|
}
|
|
|
|
float bandHeight = height / 4f;
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
linePaint.Color = charColors[Random.Shared.Next(charColors.Length)];
|
|
float bandStartY = bandHeight * i;
|
|
float y1 = bandStartY + (float)Random.Shared.NextDouble() * bandHeight;
|
|
float y2 = bandStartY + (float)Random.Shared.NextDouble() * bandHeight;
|
|
canvas.DrawLine(0, y1, width, y2, linePaint);
|
|
}
|
|
|
|
canvas.Flush();
|
|
|
|
float amplitude = 10.5f;
|
|
float frequency = 0.03f;
|
|
|
|
Span<int> sinY = stackalloc int[height];
|
|
for (int y = 0; y < height; y++)
|
|
sinY[y] = (int)(Math.Sin(y * frequency) * amplitude);
|
|
|
|
Span<int> cosX = stackalloc int[width];
|
|
for (int x = 0; x < width; x++)
|
|
cosX[x] = (int)(Math.Cos(x * frequency) * amplitude);
|
|
|
|
using var warpedBitmap = new SKBitmap(imageInfo);
|
|
|
|
Span<uint> srcPixels = MemoryMarshal.Cast<byte, uint>(sourceBitmap.GetPixelSpan());
|
|
Span<uint> dstPixels = MemoryMarshal.Cast<byte, uint>(warpedBitmap.GetPixelSpan());
|
|
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
int offsetX = sinY[y];
|
|
int yOffsetBase = y * width;
|
|
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
int srcX = x + offsetX;
|
|
int srcY = y + cosX[x];
|
|
|
|
if ((uint)srcX < (uint)width && (uint)srcY < (uint)height)
|
|
{
|
|
dstPixels[yOffsetBase + x] = srcPixels[srcY * width + srcX];
|
|
}
|
|
else
|
|
{
|
|
dstPixels[yOffsetBase + x] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
using var warpedImage = SKImage.FromBitmap(warpedBitmap);
|
|
|
|
using var data = warpedImage.Encode(SKEncodedImageFormat.Webp, 90);
|
|
|
|
return (data.ToArray(), captchaText);
|
|
}
|
|
} |