New to Telerik UI for .NET MAUIStart a free 30-day trial

Extracting Point Data from Signature in SignaturePad for UI for .NET MAUI

Updated on Nov 20, 2025

Environment

ProductUI for .NET MAUI SignaturePad
VersionCurrent

Description

I want to extract point and line data from the signature created using the SignaturePad control of UI for .NET MAUI. The goal is to format the signature data as a string that represents the x, y coordinates of the strokes in the signature. Different formats for the output data are required, such as a custom format or an SVG-like format.

This knowledge base article also answers the following questions:

  • How to save signature data as points and strokes from SignaturePad UI for .NET MAUI?
  • How to format SignaturePad data into custom or SVG-like string formats?
  • How to process and scan SignaturePad image to extract point coordinates?

Solution

To achieve this, process the signature image generated by the SignaturePad component and extract vector data representing the point coordinates. Follow these steps:

1. Save the Signature as an Image

Save the signature as a PNG image using the SaveImageAsync method:

csharp
using var stream = new MemoryStream();

await SigPad1.SaveImageAsync(stream, new SaveImageSettings
{
    ImageFormat = Telerik.Maui.Controls.SignaturePad.ImageFormat.Png,
    BackgroundColor = Colors.AliceBlue,
    StrokeColor = Colors.DarkBlue,
    StrokeThickness = 2
});

var imageBytes = stream.ToArray();
GetVectorFromSignatureImage(imageBytes);

2. Process the Image to Extract Vector Data

Create a method to process the image and extract vector data. This involves three stages:

  1. Prepare the Bitmaps.

    1. Decode the image bytes into SKBitmap.
    2. Create a grayscale bitmap for better contrast.
    3. Convert the grayscale bitmap into a binary bitmap (black and white) using a threshold.
  2. Scan the Bitmap and create point groups.

    1. Scan the binary bitmap for black pixels.
    2. Group neighboring black pixels into strokes using an algorithm.
    3. Store the grouped strokes as List<List<SKPoint>>.
  3. Serialize the grouped strokes into your preferred format:

    • Custom: "1,2;3,4;5,6/7,8;9,10;11,12"
    • SVG-like: "M x y L x y L x y ..."

Below is the complete implementation:

csharp
public static string GetVectorFromSignatureImage(byte[] imageBytes, string formatType = "custom")
{
    using var bitmap = SKBitmap.Decode(imageBytes);
    var grayscaleBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
    var binaryBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

    // Convert to grayscale
    for (var y = 0; y < bitmap.Height; y++)
    {
        for (var x = 0; x < bitmap.Width; x++)
        {
            var color = bitmap.GetPixel(x, y);
            var gray = (byte)(0.3 * color.Red + 0.59 * color.Green + 0.11 * color.Blue);
            grayscaleBitmap.SetPixel(x, y, new SKColor(gray, gray, gray));
        }
    }

    // Convert to binary (black/white)
    for (var y = 0; y < grayscaleBitmap.Height; y++)
    {
        for (var x = 0; x < grayscaleBitmap.Width; x++)
        {
            var color = grayscaleBitmap.GetPixel(x, y);
            var value = color.Red < 128 ? SKColors.Black : SKColors.White;
            binaryBitmap.SetPixel(x, y, value);
        }
    }

    var pxScanned = new bool[binaryBitmap.Width, binaryBitmap.Height];
    var groupedStrokes = new List<List<SKPoint>>();

    int[] dx = { -1, 0, 1, 0, -1, -1, 1, 1 };
    int[] dy = { 0, -1, 0, 1, -1, 1, -1, 1 };

    for (var y = 0; y < binaryBitmap.Height; y++)
    {
        for (var x = 0; x < binaryBitmap.Width; x++)
        {
            if (pxScanned[x, y] || binaryBitmap.GetPixel(x, y) != SKColors.Black)
                continue;

            pxScanned[x, y] = true;

            var queue = new Queue<SKPoint>();
            queue.Enqueue(new SKPoint(x, y));

            var stroke = new List<SKPoint>();

            while (queue.Count > 0)
            {
                var point = queue.Dequeue();
                stroke.Add(point);

                for (var i = 0; i < dx.Length; i++)
                {
                    var nx = (int)point.X + dx[i];
                    var ny = (int)point.Y + dy[i];

                    if (nx >= 0 && ny >= 0 && nx < binaryBitmap.Width && ny < binaryBitmap.Height &&
                        !pxScanned[nx, ny] && binaryBitmap.GetPixel(nx, ny) == SKColors.Black)
                    {
                        pxScanned[nx, ny] = true;
                        queue.Enqueue(new SKPoint(nx, ny));
                    }
                }
            }

            if (stroke.Count > 0)
                groupedStrokes.Add(stroke);
        }
    }

    var sb = new System.Text.StringBuilder();
    var ic = System.Globalization.CultureInfo.InvariantCulture;
    var fm = "N1";

    switch (formatType.ToLower())
    {
        case "custom":
            for (var i = 0; i < groupedStrokes.Count; i++)
            {
                for (var j = 0; j < groupedStrokes[i].Count; j++)
                {
                    sb.Append(groupedStrokes[i][j].X.ToString(fm, ic));
                    sb.Append(",");
                    sb.Append(groupedStrokes[i][j].Y.ToString(fm, ic));
                    if (j < groupedStrokes[i].Count - 1) sb.Append(";");
                }
                if (i < groupedStrokes.Count - 1) sb.Append("/");
            }
            break;

        case "svg":
            for (var gs = 0; gs < groupedStrokes.Count; gs++)
            {
                sb.Append(gs == 0 ? "M " : " L ");
                for (var p = 0; p < groupedStrokes[gs].Count; p++)
                {
                    sb.Append(groupedStrokes[gs][p].X.ToString(fm, ic));
                    sb.Append(" ");
                    sb.Append(groupedStrokes[gs][p].Y.ToString(fm, ic));
                }
            }
            break;
    }

    return sb.ToString();
}

See Also