Skip to content

Commit

Permalink
List of changes for V0.1.2:
Browse files Browse the repository at this point in the history
- The captcha length is changeable (min. six characters tho)
- The image dimension and random characters depend on the length of the captcha
- You can change the quality of the image (harder or easier to read)
- Minor additional changes like comments and properties etc.
  • Loading branch information
liebki committed Aug 20, 2023
1 parent 47e20c1 commit 5311f7d
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 92 deletions.
6 changes: 3 additions & 3 deletions BlazorVerificationCaptcha.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
<PackageId>BlazorVerificationCaptcha</PackageId>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>liebki</Authors>
<Description>A simple solution to verify users by solving a simple captcha, all done locally thanks to the image library SkiaSharp</Description>
<Description>A simple solution to verify users by solving a simple captcha, all done locally without disrespecting privacy</Description>
<Copyright>liebki</Copyright>
<PackageProjectUrl>https://github.com/liebki/BlazorVerificationCaptcha</PackageProjectUrl>
<PackageIcon>uxwing-icon.png</PackageIcon>
<RepositoryUrl>https://github.com/liebki/BlazorVerificationCaptcha</RepositoryUrl>
<PackageTags>captcha, alternative, simple, blazor, recaptcha alternative</PackageTags>
<PackageReleaseNotes>Include of 'SkiaSharp.NativeAssets.Linux.NoDependencies' for linux, moved from property CaptchaBackup, to method GenerateCaptchaContent(), new option to enable or disable the use of numbers inside the captcha, changed the image from png to jpg, to save a tiny bit of image size</PackageReleaseNotes>
<PackageReleaseNotes>This update introduces customizable CAPTCHA length, dynamic image dimensions based on length, adjustable image quality, and various refinements for improved functionality.</PackageReleaseNotes>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<Version>0.1.1</Version>
<Version>0.1.2</Version>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

Expand Down
111 changes: 68 additions & 43 deletions BlazorVerificationCaptcha/CaptchaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,34 @@ namespace BlazorVerificationCaptcha
{
public static class CaptchaGenerator
{
private static readonly int minCaptchaLength = 6;
private static readonly int baseWidth = 220;
private static readonly int baseHeight = 50;

/// <summary>
/// The generator is subject to change, I want to make it more difficult to read the captcha-content
/// Generates a CAPTCHA image with customizable options.
/// </summary>
/// <returns>Image as base64</returns>
public static (string imagevalue, string textvalue) GenerateCaptcha(bool IncludeNumbers, string TypefaceFamilyName)
/// <param name="IncludeNumbers">Include numbers in the CAPTCHA text.</param>
/// <param name="TypefaceFamilyName">The font family name for the CAPTCHA text.</param>
/// <param name="CaptchaLength">The length of the CAPTCHA text.</param>
/// <param name="JpegQualityLevel">The JPEG quality level for the image.</param>
/// <param name="ReduceRandomCharacters">Reduce the random characters a bit.</param>
/// <returns>A tuple containing the image encoded as base64 and the CAPTCHA text.</returns>
public static (string imagevalue, string textvalue) GenerateCaptcha(bool IncludeNumbers, string TypefaceFamilyName, int CaptchaLength, int JpegQualityLevel, bool ReduceRandomCharacters)
{
const int width = 200;
const int height = 50;
int effectiveCaptchaLength = Math.Max(CaptchaLength, minCaptchaLength);
int dynamicWidth = baseWidth + (effectiveCaptchaLength - minCaptchaLength) * 10;

string captchaText = Tools.GenerateRandomText(IncludeNumbers);
int dynamicHeight = baseHeight;
string captchaText = Tools.GenerateRandomText(IncludeNumbers, CaptchaLength);

using SKBitmap bitmap = new(width, height);
using SKBitmap bitmap = new(dynamicWidth, dynamicHeight);
using SKCanvas canvas = new(bitmap);

using SKPaint paint = new();
canvas.Clear(RandomColor(false));

paint.TextSize = RandomFloatValue(19, 25);
paint.TextSize = Tools.RandomFloatValue(19, 25);
paint.Color = SKColors.Black;

using (SKTypeface typeface = SKTypeface.FromFamilyName(TypefaceFamilyName))
Expand All @@ -31,67 +41,82 @@ public static (string imagevalue, string textvalue) GenerateCaptcha(bool Include
paint.FilterQuality = SKFilterQuality.Low;

paint.TextSkewX = 1;
canvas.DrawText(captchaText, RandomFloatValue(44, 62), RandomFloatValue(26, 35), paint);
float textWidth = paint.MeasureText(captchaText);

float textXCord = (dynamicWidth - textWidth) / 2;
float textYCord = Tools.RandomFloatValue(26, 35);

RandomNumbersAndText(canvas, paint);
canvas.DrawText(captchaText, textXCord, textYCord, paint);
RandomNumbersAndText(canvas, paint, CaptchaLength, ReduceRandomCharacters);
}

using SKImage image = SKImage.FromBitmap(bitmap);
using SKData data = image.Encode(SKEncodedImageFormat.Jpeg, 100);
using SKData data = image.Encode(SKEncodedImageFormat.Jpeg, JpegQualityLevel);

return (ConvStreamToBase64(data), captchaText);
}

private static void RandomNumbersAndText(SKCanvas canvas, SKPaint paint)
/// <summary>
/// Adds random characters to the canvas to create additional complexity in the CAPTCHA image.
/// </summary>
/// <param name="Canvas">The SKCanvas object to draw on.</param>
/// <param name="Paint">The SKPaint object to define text rendering properties.</param>
/// <param name="CaptchaLength">The total length of the CAPTCHA text.</param>
/// <param name="ReduceRandomCharacters">Flag indicating whether to reduce random characters.</param>
private static void RandomNumbersAndText(SKCanvas Canvas, SKPaint Paint, int CaptchaLength, bool ReduceRandomCharacters)
{
paint.TextSize = 12;
paint.Color = RandomColor();

canvas.DrawText(Convert.ToString(RandomFloatValue(0, 99)), 20, 20, paint);
paint.Color = RandomColor();

paint.TextSize = 10;
canvas.DrawText(Convert.ToString(RandomFloatValue(99, 199)), 20, 40, paint);

paint.TextSize = 11;
paint.Color = RandomColor();

canvas.DrawText(Convert.ToString(RandomFloatValue(199, 299)), 175, 20, paint);
paint.Color = RandomColor();

canvas.DrawText(Convert.ToString(RandomFloatValue(299, 399)), 175, 33, paint);
paint.TextSize = 13;

paint.Color = RandomColor();
canvas.DrawText(Tools.GenerateRandomText(false), 80, RandomFloatValue(48, 54), paint);
for (int i = 0; i < CaptchaLength; i++)
{
char randomChar;
if (ReduceRandomCharacters && i % 2 == 0)
{
randomChar = ' ';
}
else
{
randomChar = Tools.AlphabetAndNumbers[Convert.ToInt32(Tools.RandomFloatValue(0, Tools.AlphabetAndNumbers.Length))];
}

string randomString = randomChar.ToString();
Paint.TextSize = Tools.RandomFloatValue(12, 18);

Paint.Color = RandomColor(true);
float xCord = Tools.RandomFloatValue(0, Convert.ToInt32(Canvas.LocalClipBounds.Width));

float yCord = Tools.RandomFloatValue(0, Convert.ToInt32(Canvas.LocalClipBounds.Height));
Canvas.DrawText(randomString, xCord, yCord, Paint);
}
}

private static string ConvStreamToBase64(SKData data)
/// <summary>
/// Converts the provided SKData to a base64-encoded string with a data URI for image embedding.
/// </summary>
/// <param name="Data">The SKData to be converted.</param>
/// <returns>A base64-encoded string representing the provided image data.</returns>
private static string ConvStreamToBase64(SKData Data)
{
MemoryStream ms = new();
data.SaveTo(ms);
Data.SaveTo(ms);

byte[] byteArray = ms.ToArray();
string b64String = Convert.ToBase64String(byteArray);

return $"data:image/jpg;base64,{b64String}";
}

private static float RandomFloatValue(int min, int max)
{
return Random.Shared.Next(min, max);
}

/// <summary>
/// Generates a random color for the background or foreground of the CAPTCHA image.
/// </summary>
/// <param name="IsForegroundColorNeeded">Flag indicating whether a foreground color is needed.</param>
/// <returns>A random SKColor object representing the selected color.</returns>
private static SKColor RandomColor(bool IsForegroundColorNeeded = true)
{
if (IsForegroundColorNeeded)
{
SKColor[] ForegroundColors = new SKColor[] { SKColors.Black, SKColors.DarkBlue, SKColors.DarkGreen, SKColors.DarkViolet };
return ForegroundColors[Random.Shared.Next(ForegroundColors.Length)];
return Tools.ForegroundColors[Random.Shared.Next(Tools.ForegroundColors.Length)];
}

SKColor[] BackgroundColors = new SKColor[] { SKColors.White, SKColors.Yellow, SKColors.LightBlue, SKColors.OrangeRed };
return BackgroundColors[Random.Shared.Next(BackgroundColors.Length)];
return Tools.BackgroundColors[Random.Shared.Next(Tools.BackgroundColors.Length)];
}
}
}
58 changes: 34 additions & 24 deletions BlazorVerificationCaptcha/Tools.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
using System.Text;
using SkiaSharp;
using System.Text;

namespace BlazorVerificationCaptcha
{
internal static class Tools
{
internal static readonly string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
internal static readonly string AlphabetAndNumbers = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
internal static readonly SKColor[] ForegroundColors = new SKColor[] { SKColors.Black, SKColors.DarkBlue, SKColors.DarkGreen, SKColors.DarkViolet };
internal static readonly SKColor[] BackgroundColors = new SKColor[] { SKColors.White, SKColors.Yellow, SKColors.LightBlue, SKColors.OrangeRed };
private static readonly Random randi = new();

private static StringBuilder RandomCaptchaText { get; set; } = new();

/// <summary>
/// Also subject to change, I want to allow more or less than six characters of captcha but for this I need to redo the image generation because of the dimensions
/// Generates a random text containing letters and optionally numbers for CAPTCHA purposes.
/// </summary>
/// <param name="AddSpaces"></param>
/// <returns>Random characters as string</returns>
internal static string GenerateRandomText(bool UseNumbers, bool AddSpaces = true)
/// <param name="UseNumbers">Indicates whether to include numbers in the generated text.</param>
/// <param name="CaptchaLength">The desired length of the generated text.</param>
/// <param name="AddSpaces">Indicates whether to add spaces between characters for better readability.</param>
/// <returns>A randomly generated string containing letters and optionally numbers.</returns>
internal static string GenerateRandomText(bool UseNumbers, int CaptchaLength, bool AddSpaces = true)
{
StringBuilder sb = new();
int outputLength = 6;

string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

if (UseNumbers)
RandomCaptchaText.Clear();
for (int i = 0; i < CaptchaLength; i++)
{
chars += "0123456789";
}

if (!AddSpaces)
{
outputLength = 8;
}
if (UseNumbers)
{
RandomCaptchaText.Append(AlphabetAndNumbers[Random.Shared.Next(AlphabetAndNumbers.Length)]);
}
else
{
RandomCaptchaText.Append(Alphabet[Random.Shared.Next(Alphabet.Length)]);
}

for (int i = 0; i < outputLength; i++)
{
sb.Append(chars[Random.Shared.Next(chars.Length)]);
if (i < outputLength && AddSpaces)
if (AddSpaces && i < CaptchaLength - 1)
{
sb.Append(' ');
RandomCaptchaText.Append(' ');
}
}

return sb.ToString();
return RandomCaptchaText.ToString();
}

internal static float RandomFloatValue(int Min, int Max)
{
return randi.Next(Min, Max);
}
}
}
2 changes: 1 addition & 1 deletion BlazorVerificationCaptcha/VerificationCaptcha.razor
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
@namespace BlazorVerificationCaptcha

<img src=@imageURL />
<img src=@ImageURL />
17 changes: 13 additions & 4 deletions BlazorVerificationCaptcha/VerificationCaptcha.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@
{
partial class VerificationCaptcha
{
private static string imageURL = string.Empty;
public static string ImageURL { get; private set; } = string.Empty;

public static string GenerateCaptchaContent(bool IncludeNumbers = true, string TypefaceFamilyName = "Arial")
/// <summary>
/// Generates CAPTCHA content, including an image and corresponding text, with customizable options.
/// </summary>
/// <param name="IncludeNumbers">Indicates whether to include numbers in the CAPTCHA text.</param>
/// <param name="TypefaceFamilyName">The font family name for the CAPTCHA text.</param>
/// <param name="CaptchaLength">The desired length of the CAPTCHA text.</param>
/// <param name="JpegQualityLevel">The JPEG quality level for the image encoding.</param>
/// <param name="ReduceRandomCharacters">Indicates whether to reduce randomness for certain characters in the image.</param>
/// <returns>The CAPTCHA text</returns>
public static string GenerateCaptchaContent(bool IncludeNumbers = true, string TypefaceFamilyName = "Arial", int CaptchaLength = 6, int JpegQualityLevel = 100, bool ReduceRandomCharacters = false)
{
(string image, string text) = CaptchaGenerator.GenerateCaptcha(IncludeNumbers, TypefaceFamilyName);
imageURL = image;
(string image, string text) = CaptchaGenerator.GenerateCaptcha(IncludeNumbers, TypefaceFamilyName, CaptchaLength, JpegQualityLevel, ReduceRandomCharacters);
ImageURL = image;

return text.Replace(" ", string.Empty);
}
Expand Down
Loading

0 comments on commit 5311f7d

Please sign in to comment.