# fusion-captcha **Repository Path**: ymjake/fusion-captcha ## Basic Information - **Project Name**: fusion-captcha - **Description**: Vello和ImageSharp开发验证码生成器 - **Primary Language**: C# - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-12-02 - **Last Updated**: 2025-12-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: Vello, Captcha ## README # FusionCaptcha FusionCaptcha is a .NET captcha toolkit built on top of the [Vello](https://github.com/linebender/vello) rendering pipeline. It focuses on three core responsibilities—code generation, image rendering, and persistence—while leaving fonts, backgrounds, and other resources entirely in the hands of your application. This keeps the library lightweight and easy to integrate into Web API, Minimal API, or background services. ## Packages | Package | Description | | --- | --- | | `YMJake.FusionCaptcha.Abstractions` | Core contracts (`ICaptcha`, `ICaptchaRenderer`, `IStorage`, `CaptchaOptions`, etc.). | | `YMJake.FusionCaptcha.Core` | Default pipeline (background → text → noise → distortion), Vello renderer, animated GIF renderer, font/background providers, DI extensions. | | `YMJake.FusionCaptcha.Storage.Memory` | `IMemoryCache`-based storage provider. | | `YMJake.FusionCaptcha.Storage.Redis` | `StackExchange.Redis`-based storage provider with async APIs. | > 💡 **Sample Code**: Check out the [Minimal API sample](./samples/YMJake.FusionCaptcha.MinimalApi) in the repository for a complete working example with Swagger integration. ## Installation ```powershell dotnet add package YMJake.FusionCaptcha.Core dotnet add package YMJake.FusionCaptcha.Storage.Memory # or dotnet add package YMJake.FusionCaptcha.Storage.Redis ``` > Vello’s native dependencies (`vello_ffi.dll`, etc.) are shipped transitively via NuGet. Ensure your runtime RID is supported by Vello. ## Quick Start (ASP.NET Core) ```csharp using YMJake.FusionCaptcha.Abstractions; using YMJake.FusionCaptcha.Core; using YMJake.FusionCaptcha.Storage.Memory; var builder = WebApplication.CreateBuilder(args); builder.Services .AddCaptcha(options => { options.Image.Width = 160; options.Image.Height = 60; options.Image.FontSize = 36; options.Image.FontFamily = "ActionJ"; options.Image.BackgroundImage = "Grid"; }) .UseFileResources(resources => { resources.Fonts.BasePath = Path.Combine(builder.Environment.ContentRootPath, "Resources", "Fonts"); resources.Fonts.AddFont("actionj.ttf", "ActionJ"); resources.Backgrounds.BasePath = Path.Combine(builder.Environment.ContentRootPath, "Resources", "Backgrounds"); resources.Backgrounds.AddBackground("grid.png", "Grid"); }) .UseVelloRenderer() .UseCaptchaMemoryStorage(); var app = builder.Build(); app.MapGet("/captcha", async (string id, ICaptcha captcha) => { var result = await captcha.GenerateAsync(id); return Results.File(result.Bytes, result.ContentType); }); app.MapGet("/captcha/validate", (string id, string code, ICaptcha captcha) => { var success = captcha.Validate(id, code); return Results.Ok(new { id, code, success }); }); app.Run(); ``` ### Resource Management * **Fonts** – Inject via `IFontProvider`. `UseFileResources` / `UseFileFontOptions` loads fonts from disk and exposes them through `options.Image.FontFamily`. * **Backgrounds** – Provided by `IBackgroundProvider`. The default is a solid color, but you can register multiple background images and pick one via `options.Image.BackgroundImage`. * **Pipeline Steps** – `BackgroundStep`, `TextStep`, `NoiseStep`, `DistortionStep` all implement `ICaptchaPipelineStep`. You can replace or extend them with custom steps. ### Storage Options * `UseCaptchaMemoryStorage()` – Uses `IMemoryCache`, ideal for single-node deployments or tests. * `UseCaptchaRedisStorage(options => { ... })` – Backs captcha storage with Redis; configure connection string, database index, key prefix, etc. ### Animated GIF Renderer Need moving text/noise to highlight bots? Swap out the default PNG renderer with the GPU-accelerated GIF renderer: ```csharp builder.Services .AddCaptcha(options => { options.Image.Width = 160; options.Image.Height = 60; }) .UseFileResources(resources => { /* fonts / backgrounds */ }) .UseGifRenderer(gif => { gif.FrameCount = 6; // default gif.FrameDelay = 5; // 1/100th of a second gif.TextJitter = 1.5f; // per-frame wobble in pixels gif.NoiseJitter = 1.2f; gif.SequentialFade = true; // reveal characters progressively gif.TextAlphaMin = 0.35f; // fade range gif.TextAlphaMax = 1f; gif.NoiseAlpha = 0.7f; // keep lines subtle gif.RepeatCount = 0; // 0=infinite, >0 fixed loops }) .UseCaptchaMemoryStorage(); ``` `UseGifRenderer` internally reuses the Vello renderer in raw RGBA mode so every frame is generated on the GPU first and then encoded into a GIF via ImageSharp. The resulting `CaptchaRenderResult` has `ContentType = "image/gif"` and `isAnimated = true`. ### Bubbles & Noise Customization FusionCaptcha mirrors LazyCaptcha's "bubble + curved noise" style via `CaptchaImageOptions`: ```csharp builder.Services.AddCaptcha(options => { options.Image.Width = 160; options.Image.Height = 60; options.Image.BubbleCount = 4; options.Image.BubbleMinRadius = 3; options.Image.BubbleMaxRadius = 10; options.Image.BubbleOpacityMin = 0.2f; options.Image.BubbleOpacityMax = 0.45f; options.Image.InterferenceLineCount = 2; options.Image.InterferenceLineSegments = 3; // split straight line into segments options.Image.InterferenceLineCurvature = 8f; options.Image.NoiseThickness = 1.2f; }); ``` Bubble shapes render after the background step and before text, using random alpha per bubble. `InterferenceLineSegments`/`InterferenceLineCurvature` split noise lines into multiple segments so they resemble Bezier curves. ### Captcha Types Pick any of the 12 built-in captcha types (aligned with LazyCaptcha) by setting `options.CaptchaType`: | `CaptchaType` | Rendered Text | Validation Value | Notes | |-------------------|--------------------------------|------------------|----------------------------------| | `Default` | Mixed letters + digits | Same as rendered | Common combo | | `Chinese` | Random Chinese characters | Same as rendered | Uses built-in Hanzi list | | `Number` | Digits | Same as rendered | Numeric only | | `NumberZhCn` | Chinese numerals (lowercase) | Matching digit | Render Chinese, validate number | | `NumberZhHk` | Chinese numerals (uppercase) | Matching digit | Render Chinese, validate number | | `Word` | Letters (upper + lower) | Same as rendered | No digits | | `WordLower` | Lowercase letters | Same as rendered | No digits | | `WordUpper` | Uppercase letters | Same as rendered | No digits | | `WordNumberLower` | Lowercase letters + digits | Same as rendered | Matches Lazy's mode | | `WordNumberUpper` | Uppercase letters + digits | Same as rendered | Matches Lazy's mode | | `Arithmetic` | Expressions like `3+5`, `9-4` | Computed result | Auto-adjust addition/subtraction | | `ArithmeticZh` | Chinese expressions | Computed result | Render Chinese, return numbers | Arithmetic modes clamp results to keep expressions easy to solve. ### Slide Captcha (Beta) Install `YMJake.FusionCaptcha.SlideCaptcha` to get a puzzle-style slider captcha: ```csharp using YMJake.FusionCaptcha.SlideCaptcha; builder.Services .AddSlideCaptcha(options => { options.Width = 310; options.Height = 155; options.Tolerance = 0.03; options.InterferenceCount = 1; options.BackgroundImage = "Grid"; // optional, resolved via IBackgroundProvider }) .UseFileBackgrounds(backgrounds => { backgrounds.BasePath = Path.Combine(builder.Environment.ContentRootPath, "Resources", "Backgrounds"); backgrounds.AddBackground("flowers.png", "Grid"); }) .UseCaptchaMemoryStorage(); app.MapGet("/slide", (ISlideCaptcha captcha) => captcha.Generate()); app.MapPost("/slide/validate", (string id, SlideTrack track, ISlideCaptcha captcha) => captcha.Validate(id, track)); ``` - Backgrounds reuse your existing `UseFileResources`/`UseBackgroundProvider` configuration; otherwise a solid color fallback is used. - Backgrounds can reuse existing `UseFileResources` or be configured per-slide via `.UseFileBackgrounds(...)`. - Slider templates default to embedded resources but can be replaced by custom providers. - Validation expects a `SlideTrack` payload (drag trajectory + timings) and returns `ValidateResult` with success/fail/timeout. ### Click Selection Captcha (Beta) Install `YMJake.FusionCaptcha.ClickSelectionCaptcha` to add a tap-to-select experience similar to Lazy's point captcha. It reuses your background/font providers and stores session state via `IStorage`. ```csharp using YMJake.FusionCaptcha.ClickSelectionCaptcha; builder.Services .AddClickSelectionCaptcha(options => { options.Width = 320; options.Height = 200; options.TargetCount = 4; options.NoiseCount = 4; options.FontFamily = "SourceHanSans"; options.BackgroundImage = "Back"; }) .UseFileResources(resources => { resources.Fonts.BasePath = Path.Combine(builder.Environment.ContentRootPath, "Resources", "Fonts"); resources.Fonts.AddFont("SourceHanSansCN-Regular.otf", "SourceHanSans"); resources.Backgrounds.BasePath = Path.Combine(builder.Environment.ContentRootPath, "Resources", "Backgrounds"); resources.Backgrounds.AddBackground("back.png", "Back"); }) .UseCaptchaMemoryStorage(); app.MapGet("/point", (IClickSelectionCaptcha captcha) => captcha.Generate()); app.MapPost("/point/validate", (string id, ClickSelectionCaptchaAnswer answer, IClickSelectionCaptcha captcha) => captcha.Validate(id, answer)); ``` - The generator draws random characters on top of your chosen background and returns both the base64 image and the ordered instructions (e.g., `["木","冬","景","川"]`). - Validation expects clicks normalized to the original width/height; `ClickSelectionCaptchaAnswer` already mirrors what the Vue sample posts. - `UseFileResources` works the same as the core pipeline: fonts supply Chinese glyphs, backgrounds are reused from your main captcha configuration. ### Grid Icon Captcha (Alpha) Alpha-quality 3×3 / 4×4 icon picker that mirrors Google’s “select all buses” experience: ```csharp using YMJake.FusionCaptcha.GridCaptcha; builder.Services .AddGridCaptcha(options => { options.Width = 360; options.Height = 360; options.Rows = 3; options.Columns = 3; options.TargetCount = 3; }) .UseFileResources(resources => { resources.Icons.BasePath = Path.Combine(builder.Environment.ContentRootPath, "Resources", "Icons"); resources.Icons.AddIcon("bike-1.png", "自行车"); resources.Icons.AddIcon("bike-2.png", "自行车"); resources.Icons.AddIcon("bus-1.png", "公交车"); resources.Icons.AddIcon("umbrella-1.png", "雨伞"); }) .UseCaptchaMemoryStorage(); app.MapGet("/grid", (IGridCaptcha captcha) => captcha.Generate()); app.MapPost("/grid/validate", (string id, GridSelectionAnswer answer, IGridCaptcha captcha) => captcha.Validate(id, answer)); ``` - If you skip `UseFileResources` the module falls back to built-in emoji icons so the sample runs out-of-the-box. - Validation treats cell indices as a set: users can click in any order; optional tolerance allows a small number of mistakes (see `GridCaptchaOptions.Tolerance`). - The `samples/YMJake.FusionCaptcha.MinimalApi` project auto-registers everything under an `Icon//` folder (it probes the project directory, its `Resources/Icon` subfolder, as well as parent directories up to the repo root). Drop your assets anywhere in those locations (e.g., `Resources/Icon/自行车/1.jpg`) and the grid endpoints immediately pick them up. ### Rotate Captcha (Alpha) For scenes that prefer “rotate the image to the correct angle” UX, install `YMJake.FusionCaptcha.RotateCaptcha`: ```csharp using YMJake.FusionCaptcha.RotateCaptcha; builder.Services .AddRotateCaptcha(options => { options.Width = 220; options.Height = 220; options.CircleDiameter = 180; options.Tolerance = 8; // degrees options.Mode = RotateCaptchaMode.Overlay; // or RotateCaptchaMode.Standalone options.BackgroundImage = "Back"; }) .UseFileResources(resources => { resources.Backgrounds.BasePath = Path.Combine(builder.Environment.ContentRootPath, "Resources", "Backgrounds"); resources.Backgrounds.AddBackground("back.png", "Back"); }) .UseCaptchaMemoryStorage(); app.MapGet("/rotate", (IRotateCaptcha captcha, RotateCaptchaMode? mode) => captcha.Generate(modeOverride: mode)); app.MapGet("/rotate/overlay", (IRotateCaptcha captcha) => captcha.Generate(modeOverride: RotateCaptchaMode.Overlay)); app.MapGet("/rotate/standalone", (IRotateCaptcha captcha) => captcha.Generate(modeOverride: RotateCaptchaMode.Standalone)); app.MapPost("/rotate/validate", (string id, RotateCaptchaAnswer answer, IRotateCaptcha captcha) => captcha.Validate(id, answer)); ``` `RotateCaptchaOptions.Mode` lets you pick between two UX patterns. You can pick them globally, override per request via `?mode=Overlay|Standalone`, or simply call the dedicated `/rotate/overlay` and `/rotate/standalone` endpoints shown above: 1. `RotateCaptchaMode.Overlay` (default) — extracts a circular segment from the background, highlights its original position, and asks the user to rotate the disc until it matches. 2. `RotateCaptchaMode.Standalone` — renders a standalone disc without the overlay background (similar to popular rotate-to-upright challenges). `RotateCaptchaData.Mode` tells the frontend which layout to render. The Vue sample in `samples/captcha.web` switches the UI automatically based on this flag. Regardless of the mode, clients submit the delta angle plus optional motion samples; the validator normalizes the final angle and enforces the configured tolerance. ### Curve Slider (Incubating) `YMJake.FusionCaptcha.CurveSliderCaptcha` is the starting point for一个“手绘曲线匹配”验证码。当前版本包含所有核心契约(`ICurveSliderCaptcha`、`CurveSliderTrack`、`CurveSliderCaptchaOptions` 等)以及 DI 扩展,方便你率先接入或自研曲线模板/轨迹匹配逻辑: ```csharp using YMJake.FusionCaptcha.CurveSliderCaptcha; builder.Services.AddCurveSliderCaptcha(options => { options.Width = 320; options.Height = 180; options.PathTolerance = 0.08f; options.Prompt = "请沿着图中的参考曲线完成轨迹"; }); ``` 默认实现目前会返回占位数据并提示“功能建设中”。这样可以先把 API、序列化格式、前后端通信打通,后续只需替换 `ICurveSliderCaptcha` 实现即可完成真实的曲线渲染与轨迹验证。 ### Proof-of-Work CAP (Beta) Install `YMJake.FusionCaptcha.PowCap` to self-host a Proof-of-Work gate similar to reCAPTCHA/hCaptcha/Turnstile. Register any `IDistributedCache` implementation (`AddDistributedMemoryCache`, `AddStackExchangeRedisCache`, …) before calling `AddPowCapServer`. The ASP.NET Core sample wires it up next to the image-based captchas (`samples/YMJake.FusionCaptcha.MinimalApi/Program.cs:1`): ```csharp builder.Services.AddDistributedMemoryCache(); builder.Services.AddPowCapServer(options => { options.Default.ChallengeDifficulty = 4; options.UseCaseConfigs = new Dictionary { ["login"] = new PowCapConfig { ChallengeDifficulty = 5 } }; }); app.MapPowCapServer("/api/cap"); app.MapPost("/cap/validate", async (CapTokenRequest request, ICaptchaService captchaService) => { var success = await captchaService.ValidateCaptchaTokenAsync(request.Token); return success ? Results.Ok() : Results.BadRequest(); }); ``` Point the [@cap.js/widget](https://capjs.js.org/guide/widget.html) to `/api/cap/` (or `/api/cap/{useCase}/`) on your host and post the returned token to `/cap/validate` (or directly inject `ICaptchaService` into your own controller). Because the implementation relies on `IDistributedCache`, you can keep the default in-memory cache for demos or swap in Redis/SQL Server for multi-node deployments. ### Samples | Sample | Description | | --- | --- | | [`samples/YMJake.FusionCaptcha.MinimalApi`](./samples/YMJake.FusionCaptcha.MinimalApi) | ASP.NET Core Minimal API exposing `/captcha`, `/slide`, `/point`, and `/rotate` endpoints. Run with `dotnet run --project samples/YMJake.FusionCaptcha.MinimalApi`. | | [`samples/captcha.web`](./samples/captcha.web) | Vue 3 + Vite dashboard with tabs for slider, click selection, and both rotate modes. Use `npm install && npm run dev` and browse to `http://localhost:5173`. | | [`samples/captcha.test`](./samples/captcha.test) | Vue 3 + Vite playground that exercises the slide captcha and the click-selection captcha. Run `npm install && npm run dev` inside the folder, keep the Minimal API running on `http://localhost:5222`, and browse to `http://localhost:5173`. | The `captcha.web` sample mirrors Lazy's polished demo UX: the slider component records drag trajectories, `ClickBasedCaptcha.vue` collects sequential taps, and `RotateCaptcha.vue` renders either overlay/standalone layouts depending on `RotateCaptchaData.Mode`. The leaner `captcha.test` project keeps the wiring minimal if you only need a sandbox for API testing. ## Roadmap - TODO: Curve-matching slider puzzle with multi-point trajectory scoring. - TODO: Gesture tracing captcha (Z-shape, circle, check mark, etc.) with behavior analysis. - Pluggable pipeline DSL similar to `QrBuilder`. - GIF / WebP / ImageSharp encoders. - Storage-level TTL strategies and hit stats for custom rate limiting. Contributions and feature requests are always welcome!