@page "/signature" Signature - bit Bink

Signature

A production signature field: capture, clear, undo, validate and export. The signature is stored as vector ink, so it can be re-rendered at any size, embedded in a PDF as SVG, or rasterized at print resolution.

@* A fixed aspect signing area with a baseline, the way a paper form has one. *@
Ink length @_inkLength.ToString("0") units
@_message
@if (_saved is not null) {

Stored signature

This is the document as it would be persisted. It is compact vector data, not an image: @_savedBytes bytes for @_saved.ElementCount strokes.

Re-rendered from the stored document
@((MarkupString)_savedSvg)
Serialized document
@_savedJson
} @code { private BinkCanvas? _canvas; private BinkBrush _brush = new BinkPenBrush { Width = 3.5f, Color = BinkColor.Parse("#0f172a"), TaperEnd = 8f }; private string _color = "#0f172a"; private float _width = 3.5f; private float _inkLength; private bool _locked; private static readonly string[] InkColors = ["#0f172a", "#1d4ed8"]; private string _message = "Sign to continue."; private BinkDocument? _saved; private string _savedSvg = string.Empty; private string _savedJson = string.Empty; private int _savedBytes; private bool IsEmpty => _canvas?.CurrentDocument.IsEmpty ?? true; private bool CanUndo => _canvas?.History.CanUndo ?? false; private bool CanRedo => _canvas?.History.CanRedo ?? false; // A scribble of one dot is not a signature. Requiring a little ink is a real validation rule that the // vector model makes easy to express. private bool IsValid => _inkLength > MinimumInkLength; private const float MinimumInkLength = 120f; private void SetColor(string color) { _color = color; _brush = _brush with { Color = BinkColor.Parse(color) }; } private void OnWidthChanged(ChangeEventArgs e) { if (float.TryParse(e.Value?.ToString(), System.Globalization.CultureInfo.InvariantCulture, out float value)) { _width = value; _brush = _brush with { Width = value }; } } private void OnStrokeCompleted(BinkStrokeEventArgs args) { // Stroke and document notifications can arrive in either order, so measure here as well. MeasureInk(); string captured = args.Stroke is { } stroke && stroke.Input.HasRealPressure ? "Captured with pen pressure." : "Captured."; _message = IsValid ? captured : captured + " Keep signing: the signature is still too short to accept."; } private void OnDocumentChanged(BinkDocumentChangedEventArgs args) { MeasureInk(); StateHasChanged(); } private void MeasureInk() { _inkLength = 0f; if (_canvas is null) return; foreach (BinkElement element in _canvas.CurrentDocument.AllElements) { if (element is BinkStroke stroke) _inkLength += stroke.Points.Length; } } private void Clear() { // Signing again unlocks the field. The stored copy below is kept until a new one is accepted. _locked = false; // The canvas still has ReadOnly set until this component re-renders, and a read only canvas ignores // Clear. Clearing the document directly is the same undoable edit, without waiting for that render. _canvas?.CurrentDocument.Clear(); _message = "Sign to continue."; } private void Accept() { if (_canvas is null || !IsValid) return; // Persisting a signature: crop to the ink, keep it as vector data, and label it for accessibility. BinkDocument document = _canvas.CurrentDocument.Clone(); document.SetMetadata("signed-at", DateTimeOffset.UtcNow.ToString("O")); document.SetMetadata("purpose", "signature"); _saved = document; _savedJson = BinkSerializer.Serialize(document, BinkSerializerOptions.Readable); _savedBytes = BinkSerializer.SerializeToUtf8Bytes(document).Length; _savedSvg = BinkSvgExporter.Export(document, new BinkSvgExportOptions { Region = BinkExportRegion.Content, Padding = 12f, Background = BinkColor.Transparent, Title = "Signature", IncludeElementDescriptions = true, }); // An accepted signature is final, the way ink on paper is: the field becomes read only until the // signer chooses to sign again. _locked = true; _message = "Signature accepted."; } private Task DownloadSvgAsync() => _canvas?.SaveSvgAsync("signature.svg", new BinkSvgExportOptions { Region = BinkExportRegion.Content, Padding = 12f, Background = BinkColor.Transparent, }) ?? Task.CompletedTask; private Task DownloadPngAsync() => _canvas?.SavePngAsync("signature.png", new BinkPngExportOptions { Region = BinkExportRegion.Content, Padding = 12f, Scale = 3f, Background = BinkColor.Transparent, }) ?? Task.CompletedTask; }