diff --git a/src/CaptchaKit.AspNetCore/CaptchaKitService.cs b/src/CaptchaKit.AspNetCore/CaptchaKitService.cs index c153ae2..b608144 100644 --- a/src/CaptchaKit.AspNetCore/CaptchaKitService.cs +++ b/src/CaptchaKit.AspNetCore/CaptchaKitService.cs @@ -128,6 +128,48 @@ public sealed class RandomCaptchaGenerator(CaptchaKitOptions options) : ICaptcha public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator { private const string Alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + private static readonly IReadOnlyDictionary Glyphs = new Dictionary + { + ['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) { @@ -148,33 +190,28 @@ public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator using var canvas = new SKCanvas(bitmap); canvas.Clear(new SKColor(244, 248, 252)); DrawNoise(canvas, width, 54, 20); - // Use Skia's portable default typeface instead of relying on a server-installed font such as Arial. - using var font = new SKFont(SKTypeface.Default, 32) { Embolden = true }; - using var outlinePaint = new SKPaint - { - IsAntialias = true, - Color = new SKColor(255, 255, 255), - Style = SKPaintStyle.Stroke, - StrokeWidth = 3 - }; using var textPaint = new SKPaint { IsAntialias = true, Color = new SKColor(20, 48, 80) }; - var spacing = Math.Min(34, Math.Max(24, (width - 28) / displayText.Length)); - var startX = width / 2f - (displayText.Length - 1) * spacing / 2f; - var metrics = font.Metrics; - var baseline = (54 - (metrics.Descent - metrics.Ascent)) / 2f - metrics.Ascent; + 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++) { - var x = startX + index * spacing + RandomNumberGenerator.GetInt32(-2, 3); - var y = baseline + RandomNumberGenerator.GetInt32(-3, 4); + var glyph = Glyphs.GetValueOrDefault(displayText[index], Glyphs['?']); canvas.Save(); - canvas.RotateDegrees(RandomNumberGenerator.GetInt32(-20, 21), x, y); - canvas.DrawText(displayText[index].ToString(), x, y, SKTextAlign.Center, font, outlinePaint); - canvas.DrawText(displayText[index].ToString(), x, y, SKTextAlign.Center, font, textPaint); + canvas.Translate(x + glyphWidth / 2, y + glyphHeight / 2); + canvas.RotateDegrees(RandomNumberGenerator.GetInt32(-12, 13)); + canvas.Translate(-glyphWidth / 2, -glyphHeight / 2); + DrawGlyph(canvas, glyph, scale, textPaint); canvas.Restore(); + x += glyphWidth + gap; } using var image = SKImage.FromBitmap(bitmap); using var data = image.Encode(SKEncodedImageFormat.Png, 100); @@ -183,6 +220,16 @@ public sealed class SkiaTextCaptchaGenerator : ICaptchaGenerator return new GeneratedCaptcha(challenge, answer); } + private static void DrawGlyph(SKCanvas canvas, IReadOnlyList 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) { using var paint = new SKPaint { IsAntialias = true, StrokeWidth = 1 };