@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
@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;
}