Building Cross-Platform CAPTCHA Solutions Using SkiaSharp on Linux

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.

  1. Locate a supported font file (e.g., TTF) compatible with you're application directory.
  2. Copy the font file into the project's root or /app/fonts directory.
  3. Update the path reference in LINUX_FONT_PATH variable with in the renderer constructor or initialization logic.

Tags: SkiaSharp .NET Core CAPTCHA Linux Image Processing

Posted on Wed, 19 Aug 2026 16:35:05 +0000 by sb