JigsawPuzzleCaptcha 1.1.0

dotnet add package JigsawPuzzleCaptcha --version 1.1.0
                    
NuGet\Install-Package JigsawPuzzleCaptcha -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JigsawPuzzleCaptcha" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JigsawPuzzleCaptcha" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="JigsawPuzzleCaptcha" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JigsawPuzzleCaptcha --version 1.1.0
                    
#r "nuget: JigsawPuzzleCaptcha, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JigsawPuzzleCaptcha@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JigsawPuzzleCaptcha&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=JigsawPuzzleCaptcha&version=1.1.0
                    
Install as a Cake Tool

JigsawPuzzleCaptcha

NuGet License: MIT

Slide-to-fit jigsaw puzzle CAPTCHA for .NET. Give it any image and it returns two PNGs — a background with a puzzle-shaped hole, and the matching piece — plus the answer to store server-side.

Built on ImageSharp, so it runs on Linux, macOS and Windows with no System.Drawing dependency.

Install

dotnet add package JigsawPuzzleCaptcha

Targets net8.0 and net10.0.

Quick start

using JigsawPuzzleCaptcha;

var generator = new JigsawCaptchaGenerator();

byte[] source = File.ReadAllBytes("background.jpg");
CaptchaResult captcha = generator.Generate(source);

// Send these to the browser:
//   captcha.BackgroundDataUri
//   captcha.PieceDataUri
//   captcha.Y            (where to draw the piece)
//
// Keep this on the server, keyed by a challenge id:
//   captcha.X            (the answer)

Later, when the user drops the piece:

bool ok = generator.Validate(submittedX, storedX);

ASP.NET Core

builder.Services.AddJigsawCaptcha(options =>
{
    options.PieceWidth = 60;
    options.PieceHeight = 60;
    options.Tolerance = 5;
});
app.MapGet("/captcha", (IJigsawCaptchaGenerator captcha, IDistributedCache cache) =>
{
    CaptchaResult result = captcha.Generate(File.ReadAllBytes("wwwroot/bg.jpg"));

    var id = Guid.NewGuid().ToString("N");
    cache.SetString(id, result.X.ToString(), new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
    });

    // Note: X is deliberately not returned.
    return Results.Ok(new { id, background = result.BackgroundDataUri, piece = result.PieceDataUri, y = result.Y });
});

app.MapPost("/captcha/verify", (VerifyRequest req, IJigsawCaptchaGenerator captcha, IDistributedCache cache) =>
{
    var stored = cache.GetString(req.Id);
    if (stored is null) return Results.BadRequest("Challenge expired.");

    cache.Remove(req.Id); // one attempt per challenge
    return captcha.Validate(req.X, int.Parse(stored)) ? Results.Ok() : Results.BadRequest("Wrong position.");
});

record VerifyRequest(string Id, int X);

Options

Option Default Purpose
Shape Classic Outline of the piece — see Piece shapes.
PieceWidth 60 Width of the piece body, in pixels.
PieceHeight 60 Height of the piece body, in pixels.
Padding 20 Minimum gap kept between the slot and the image edges.
Tolerance 5 Horizontal error, in pixels, still accepted by Validate.
TabRatio 0.15 Tab and notch size, as a fraction of the smaller piece dimension. Only affects Classic.

Options can be set once at registration, or passed per call: generator.Generate(bytes, options).

Piece shapes

JigsawCaptchaOptions.Shape (JigsawPuzzleCaptcha.Shapes.PuzzleShapeKind) picks the piece outline:

Shape Description
Classic (default) Rectangle with an interlocking tab on top and a notch on the left — the original puzzle look.
Square A plain rectangular cutout, no tab or notch.
Triangle An isosceles triangle inscribed in the piece's bounding box.
Hexagon A regular, flat-top hexagon inscribed in the piece's bounding box.
Circle An ellipse inscribed in the piece's bounding box — a true circle when PieceWidth == PieceHeight.

Shapes are resolved through an internal factory keyed by PuzzleShapeKind, so picking one is just setting Shape on the options passed to Generate — no extra registration needed, and it can change per call/request, not only at DI setup time:

CaptchaResult result = generator.Generate(source, new JigsawCaptchaOptions
{
    Shape = PuzzleShapeKind.Hexagon,
});

Only Classic needs extra canvas height for the tab overhang; the other shapes stay inside PieceWidth x PieceHeight.

Result

CaptchaResult carries the two PNGs as byte[] (with BackgroundDataUri / PieceDataUri convenience properties), the answer X, the render position Y, the dimensions of both images, and the Shape that was used.

PieceHeight on the result can be larger than the configured PieceHeight for shapes that stick out of their bounding box (currently just Classic, whose canvas includes the tab).

Security notes

This is a friction device, not an authentication mechanism. To get real value from it:

  • Never send X to the client. Store it server-side against a challenge id. If it reaches the browser, the CAPTCHA is decorative.
  • One attempt per challenge. Delete the stored answer on the first verification, pass or fail.
  • Expire challenges after a minute or two.
  • Rate-limit both the issue and the verify endpoint per IP or session.
  • Rotate background images. A fixed background lets an attacker precompute the slot positions.
  • Validate uploads if the source image comes from users — check dimensions before decoding to avoid decompression bombs.

The answer is generated with RandomNumberGenerator, not Random, so the sequence is not predictable from earlier challenges.

Licensing

This package is MIT.

It depends on ImageSharp, which uses the Six Labors Split License. Because you consume ImageSharp transitively through this package, it is licensed to you under Apache 2.0 regardless of your company's revenue — the split license's commercial threshold applies to direct package dependencies. If you also reference ImageSharp directly, check sixlabors.com/pricing for your own situation.

Contributing

Issues and pull requests welcome. Run dotnet test before opening a PR.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 78 8/12/2026
1.0.4 84 8/9/2026