Overview
Rendering graphical user interface elements such as verification codes typically relies on GDI+ in Windows environments. However, implementing these features in a Linux-based .NET Core ecosystem requires avoiding platform-specific dependencies. SkiaSharp provides a robust, cross-platform solution for high-fidelity 2D graphics rendering with out requiring additional system-level graphics packages.
Prerequisites
To ensure compatibility with Linux distributions without pre-installed graphics libraries, install the following NuGet package:
dotnet add package SkiaSharp.NativeAssets.Linux.NoDependencies
Core Implementation
The following service class demonstrates how to generate randomized image-based authentication tokens. Key improvements over basic implementations include dynamic color generation for contrast, configurable distortion parameters, and efficient memory management via disposable patterns.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using SkiaSharp;
namespace App.Helpers.Captcha
{
public class LinuxCaptchaRenderer
{
private readonly Random _rng = new();
// Configuration Properties
public int CodeLength { get; set; } = 4;
public int CanvasWidth { get; set; } = 200;
public int CanvasHeight { get; set; } = 60;
public int FontSize { get; set; } = 36;
public bool EnableRotation { get; set; } = true;
public int MaxRotateAngle { get; set; } = 30;
public int NoiseDotCount { get; set; } = 50;
// State
public string GeneratedValue { get; private set; }
public string MathResult { get; private set; }
public LinuxCaptchaRenderer()
{
CalculateCanvasDimensions();
}
/// <summary>
/// Initializes random colors for the character strokes.
/// </summary>
private void InitSettings()
{
// Reset state based on mode
GeneratedValue = "";
MathResult = "";
GenerateTokenString();
}
private void GenerateTokenString()
{
StringBuilder sb = new StringBuilder();
// Avoid ambiguous characters 'O' and '0', 'l' and '1'
const string Chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
char[] charPool = Chars.ToCharArray();
// Shuffle pool to allow repeated picking from full set safely
var shuffled = charPool.OrderBy(c => Guid.NewGuid()).ToArray();
for (int i = 0; i < CodeLength; i++)
{
int index = _rng.Next(shuffled.Length);
GeneratedValue += shuffled[index];
}
}
private void CalculateCanvasDimensions()
{
CanvasWidth = (int)(FontSize * (CodeLength + 1));
CanvasHeight = Convert.ToInt32(FontSize * 1.2f);
}
/// <summary>
/// Generates a contrasting color suitable for a white background.
/// </summary>
private SKColor GenerateContrastColor()
{
int r = _rng.Next(50, 200);
int g = _rng.Next(50, 200);
int b = _rng.Next(50, 200);
// Ensure brightness isn't too low for visibility
if ((r + g + b) / 3 < 100)
{
r = _rng.Next(100, 255);
g = _rng.Next(100, 255);
b = _rng.Next(100, 255);
}
return SKColor.FromArgb(r, g, b);
}
/// <summary>
/// Renders the final image into a byte array.
/// </summary>
public byte[] RenderImage()
{
// Allocate Bitmap surface
using (var bmp = new SKBitmap(CanvasWidth, CanvasHeight))
using (var canvas = new SKCanvas(bmp))
{
// Clear Background
canvas.Clear(SKColors.White);
// Load Custom Typeface if available, else fallback
var typefacePath = "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf";
var font = File.Exists(typefacePath)
? SKTypeface.FromFile(typefacePath)
: SKTypeface.Default;
var paintOptions = new SKPaint
{
Typeface = font,
TextSize = FontSize,
IsAntialias = true
};
var offset = 20;
// Render Characters
for (int i = 0; i < GeneratedValue.Length; i++)
{
paintOptions.Color = GenerateContrastColor();
float charX = offset + (i * (CanvasWidth / CodeLength) - 20);
float charY = CanvasHeight / 2;
if (EnableRotation)
{
float angle = _rng.Next(-MaxRotateAngle, MaxRotateAngle);
canvas.Translate(charX, charY);
canvas.RotateDegrees(angle);
canvas.DrawText($"{GeneratedValue[i]}", 0, 0, paintOptions);
canvas.RotateDegrees(-angle);
canvas.Translate(-charX, -charY);
}
else
{
canvas.DrawText($"{GeneratedValue[i]}", charX, charY, paintOptions);
}
}
// Add Visual Noise (Lines)
using (var linePen = new SKPaint())
{
linePen.StrokeWidth = 2;
for(int i=0; i<5; i++)
{
linePen.Color = GenerateContrastColor();
float x1 = _rng.Next(CanvasWidth);
float y1 = _rng.Next(CanvasHeight);
float x2 = _rng.Next(CanvasWidth);
float y2 = _rng.Next(CanvasHeight);
canvas.DrawLine(x1, y1, x2, y2, linePen);
}
}
// Add Visual Noise (Scattered Points)
for (int i = 0; i < NoiseDotCount; i++)
{
var dotColor = GenerateContrastColor();
int px = _rng.Next(CanvasWidth);
int py = _rng.Next(CanvasHeight);
bmp.SetPixel(px, py, dotColor.ToSKColor());
}
// Encode to PNG Stream
using (var data = bmp.Encode(SKEncodedImageFormat.Png, 90))
{
return data.ToArray();
}
}
}
/// <summary>
/// Returns Base64 representation for HTTP transfer.
/// </summary>
public string GetBase64Output()
{
var bytes = RenderImage();
return Convert.ToBase64String(bytes);
}
}
}
Integration Example
Expose this logic through an API controller. This example assumes an ASP.NET Core Web API setup.
[HttpGet("api/v1/captcha")]
public IActionResult GetVerification()
{
var renderer = new LinuxCaptchaRenderer();
// Configure dimensions specifically for mobile
renderer.FontSize = 28;
renderer.CanvasHeight = 50;
// Trigger generation logic
renderer.InitSettings();
var base64Img = renderer.GetBase64Output();
return Ok(new
{
token = base64Img,
sessionData = renderer.GeneratedValue // Pass securely to client session
});
}
Font Considerations
CAPTCHA security relies heavily on character rendering. In containerized Linux environments, default fonts may differ from the host OS.
- Locate a supported font file (e.g., TTF) compatible with you're application directory.
- Copy the font file into the project's root or
/app/fontsdirectory. - Update the path reference in
LINUX_FONT_PATHvariable with in the renderer constructor or initialization logic.