Compare commits

..
3 Commits
2 changed files with 72 additions and 15 deletions

No files matched your search

@@ -15,7 +15,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CaptchaKit.Core\CaptchaKit.Core.csproj" /> <ProjectReference Include="..\CaptchaKit.Core\CaptchaKit.Core.csproj" />
<FrameworkReference Include="Microsoft.AspNetCore.App" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="SkiaSharp" Version="4.152.0" /> <PackageReference Include="SkiaSharp" Version="4.152.1" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" /> <None Include="..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\LICENSE" Pack="true" PackagePath="\" /> <None Include="..\..\LICENSE" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
+71 -14
View File
@@ -128,6 +128,48 @@ public sealed class RandomCaptchaGenerator(CaptchaKitOptions options) : ICaptcha
public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator
{ {
private const string Alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; private const string Alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
private static readonly IReadOnlyDictionary<char, string[]> Glyphs = new Dictionary<char, string[]>
{
['0'] = ["01110", "10001", "10011", "10101", "11001", "10001", "01110"],
['1'] = ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
['2'] = ["01110", "10001", "00001", "00010", "00100", "01000", "11111"],
['3'] = ["11110", "00001", "00001", "01110", "00001", "00001", "11110"],
['4'] = ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
['5'] = ["11111", "10000", "10000", "11110", "00001", "00001", "11110"],
['6'] = ["01110", "10000", "10000", "11110", "10001", "10001", "01110"],
['7'] = ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
['8'] = ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
['9'] = ["01110", "10001", "10001", "01111", "00001", "00001", "01110"],
['A'] = ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
['B'] = ["11110", "10001", "10001", "11110", "10001", "10001", "11110"],
['C'] = ["01110", "10001", "10000", "10000", "10000", "10001", "01110"],
['D'] = ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
['E'] = ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
['F'] = ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
['G'] = ["01110", "10001", "10000", "10111", "10001", "10001", "01110"],
['H'] = ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
['J'] = ["00111", "00010", "00010", "00010", "00010", "10010", "01100"],
['K'] = ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
['L'] = ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
['M'] = ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
['N'] = ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
['P'] = ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
['Q'] = ["01110", "10001", "10001", "10001", "10101", "10010", "01101"],
['R'] = ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
['S'] = ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
['T'] = ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
['U'] = ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
['V'] = ["10001", "10001", "10001", "10001", "10001", "01010", "00100"],
['W'] = ["10001", "10001", "10001", "10101", "10101", "10101", "01010"],
['X'] = ["10001", "10001", "01010", "00100", "01010", "10001", "10001"],
['Y'] = ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
['Z'] = ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
['+'] = ["00000", "00100", "00100", "11111", "00100", "00100", "00000"],
['-'] = ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
['='] = ["00000", "11111", "00000", "11111", "00000", "00000", "00000"],
['?'] = ["01110", "10001", "00001", "00010", "00100", "00000", "00100"],
[' '] = ["00000", "00000", "00000", "00000", "00000", "00000", "00000"]
};
public GeneratedCaptcha Generate(CaptchaGenerationRequest request) public GeneratedCaptcha Generate(CaptchaGenerationRequest request)
{ {
@@ -147,21 +189,30 @@ public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator
using var bitmap = new SKBitmap(width, 54); using var bitmap = new SKBitmap(width, 54);
using var canvas = new SKCanvas(bitmap); using var canvas = new SKCanvas(bitmap);
canvas.Clear(new SKColor(244, 248, 252)); canvas.Clear(new SKColor(244, 248, 252));
DrawNoise(canvas, width, 54, 16); DrawNoise(canvas, width, 54, 20);
using var paint = new SKPaint { IsAntialias = true }; using var textPaint = new SKPaint
using var font = new SKFont(SKTypeface.FromFamilyName("Arial", SKFontStyle.Bold), 30); {
var spacing = Math.Max(22, (width - 24) / displayText.Length); IsAntialias = true,
Color = new SKColor(20, 48, 80)
};
var scale = Math.Min(4f, (width - 28f) / (displayText.Length * 6.5f - 1.5f));
var glyphWidth = scale * 5;
var glyphHeight = scale * 7;
var gap = scale * 1.5f;
var totalWidth = displayText.Length * glyphWidth + (displayText.Length - 1) * gap;
var x = (width - totalWidth) / 2f;
var y = (54 - glyphHeight) / 2f;
for (var index = 0; index < displayText.Length; index++) for (var index = 0; index < displayText.Length; index++)
{ {
var x = 12 + index * spacing + RandomNumberGenerator.GetInt32(-2, 3); var glyph = Glyphs.GetValueOrDefault(displayText[index], Glyphs['?']);
var y = 38 + RandomNumberGenerator.GetInt32(-4, 5);
paint.Color = RandomColor(35, 145);
canvas.Save(); canvas.Save();
canvas.RotateDegrees(RandomNumberGenerator.GetInt32(-20, 21), x, y); canvas.Translate(x + glyphWidth / 2, y + glyphHeight / 2);
canvas.DrawText(displayText[index].ToString(), x, y, SKTextAlign.Left, font, paint); canvas.RotateDegrees(RandomNumberGenerator.GetInt32(-12, 13));
canvas.Translate(-glyphWidth / 2, -glyphHeight / 2);
DrawGlyph(canvas, glyph, scale, textPaint);
canvas.Restore(); canvas.Restore();
x += glyphWidth + gap;
} }
DrawNoise(canvas, width, 54, 10);
using var image = SKImage.FromBitmap(bitmap); using var image = SKImage.FromBitmap(bitmap);
using var data = image.Encode(SKEncodedImageFormat.Png, 100); using var data = image.Encode(SKEncodedImageFormat.Png, 100);
var challenge = new CaptchaChallenge(request.Id, type, request.ExpiresAt, var challenge = new CaptchaChallenge(request.Id, type, request.ExpiresAt,
@@ -169,6 +220,16 @@ public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator
return new GeneratedCaptcha(challenge, answer); return new GeneratedCaptcha(challenge, answer);
} }
private static void DrawGlyph(SKCanvas canvas, IReadOnlyList<string> glyph, float scale, SKPaint paint)
{
for (var row = 0; row < glyph.Count; row++)
for (var column = 0; column < glyph[row].Length; column++)
{
if (glyph[row][column] == '1')
canvas.DrawRect(column * scale, row * scale, scale, scale, paint);
}
}
private static void DrawNoise(SKCanvas canvas, int width, int height, int count) private static void DrawNoise(SKCanvas canvas, int width, int height, int count)
{ {
using var paint = new SKPaint { IsAntialias = true, StrokeWidth = 1 }; using var paint = new SKPaint { IsAntialias = true, StrokeWidth = 1 };
@@ -179,10 +240,6 @@ public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator
} }
} }
private static SKColor RandomColor(int min, int max) => new(
(byte)RandomNumberGenerator.GetInt32(min, max),
(byte)RandomNumberGenerator.GetInt32(min, max),
(byte)RandomNumberGenerator.GetInt32(min, max));
private static byte RandomByte() => RandomNumberGenerator.GetBytes(1)[0]; private static byte RandomByte() => RandomNumberGenerator.GetBytes(1)[0];
} }