The personal website of Scott W Harden
February 1st, 2022

Rotated Rectangle Hit Detection with C#

I recently had the need to determine if a point is inside a rotated rectangle. This need arose when I wanted to make a rotated rectangular textbox draggable, but I wanted to determine if the mouse was over the rectangle. I know the rectangle's location, size, and rotation, and the position of the mouse cursor, and my goal is to tell if the mouse is inside the rotated rectangle. In this example I'll use Maui.Graphics to render a test image in a Windows Forms application (with SkiaSharp and OpenGL), but the same could be achieved with System.Drawing or other similar 2D graphics libraries.

I started just knowing the width and height of my rectangle. I created an array of points representing its corners.

float rectWidth = 300;
float rectHeight = 150;

PointF[] rectCorners =
{
    new(0, 0),
    new(rectWidth, 0),
    new(rectWidth, rectHeight),
    new(0, rectHeight),
};

I then rotated the rectangle around an origin point by applying a rotation transformation to each corner.

PointF origin = new(200, 300); // center of rotation
double angleRadians = 1.234;
PointF[] rotatedCorners = rectCorners.Select(x => Rotate(origin, x, angleRadians)).ToArray();
private PointF Rotate(PointF origin, PointF point, double radians)
{
    double dx = point.X * Math.Cos(radians) - point.Y * Math.Sin(radians);
    double dy = point.X * Math.Sin(radians) + point.Y * Math.Cos(radians);
    return new PointF(origin.X + (float)dx, origin.Y + (float)dy);
}

To determine if a given point is inside the rotated rectangle I called this method which accepts the point of interest and an array containing the four corners of the rotated rectangle.

public bool IsPointInsideRectangle(PointF pt, PointF[] rectCorners)
{
    double x1 = rectCorners[0].X;
    double x2 = rectCorners[1].X;
    double x3 = rectCorners[2].X;
    double x4 = rectCorners[3].X;

    double y1 = rectCorners[0].Y;
    double y2 = rectCorners[1].Y;
    double y3 = rectCorners[2].Y;
    double y4 = rectCorners[3].Y;

    double a1 = Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    double a2 = Math.Sqrt((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3));
    double a3 = Math.Sqrt((x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4));
    double a4 = Math.Sqrt((x4 - x1) * (x4 - x1) + (y4 - y1) * (y4 - y1));

    double b1 = Math.Sqrt((x1 - pt.X) * (x1 - pt.X) + (y1 - pt.Y) * (y1 - pt.Y));
    double b2 = Math.Sqrt((x2 - pt.X) * (x2 - pt.X) + (y2 - pt.Y) * (y2 - pt.Y));
    double b3 = Math.Sqrt((x3 - pt.X) * (x3 - pt.X) + (y3 - pt.Y) * (y3 - pt.Y));
    double b4 = Math.Sqrt((x4 - pt.X) * (x4 - pt.X) + (y4 - pt.Y) * (y4 - pt.Y));

    double u1 = (a1 + b1 + b2) / 2;
    double u2 = (a2 + b2 + b3) / 2;
    double u3 = (a3 + b3 + b4) / 2;
    double u4 = (a4 + b4 + b1) / 2;

    double A1 = Math.Sqrt(u1 * (u1 - a1) * (u1 - b1) * (u1 - b2));
    double A2 = Math.Sqrt(u2 * (u2 - a2) * (u2 - b2) * (u2 - b3));
    double A3 = Math.Sqrt(u3 * (u3 - a3) * (u3 - b3) * (u3 - b4));
    double A4 = Math.Sqrt(u4 * (u4 - a4) * (u4 - b4) * (u4 - b1));

    double difference = A1 + A2 + A3 + A4 - a1 * a2;
    return difference < 1;
}

How does it work?

Consider 4 triangles formed by lines between the point and the 4 corners...

If the point is inside the rectangle, the area of the four triangles will equal the area of the rectangle.

If the point is outside the rectangle, the area of the four triangles will be greater than the area of the rectangle.

The code above calculates the area of the 4 rectangles and returns true if it is approximately equal to the area of the rectangle.

Notes

  • In practice you'll probably want to use a more intelligent data structure than a 4-element Pointf[] when calling these functions.

  • The points in the array are clockwise, but I assume this method will work regardless of the order of the points in the array.

  • At the very end of IsPointInsideRectangle() the final decision is made based on a distance being less than a given value. It's true that the cursor will be inside the rectangle if the distance is exactly zero, but with the possible accumulation of floating-point math errors this seemed like a safer option.

Resources

Markdown source code last modified on February 2nd, 2022
---
Title: Point Inside Rectangle
Description: How to determine if a point is inside a rotated rectangle with C#
Date: 2022-02-01 12:10AM EST
Tags: csharp, graphics
---

# Rotated Rectangle Hit Detection with C# 

**I recently had the need to determine if a point is inside a rotated rectangle.** This need arose when I wanted to make a rotated rectangular textbox draggable, but I wanted to determine if the mouse was over the rectangle. I know the rectangle's location, size, and rotation, and the position of the mouse cursor, and my goal is to tell if the mouse is inside the rotated rectangle. In this example I'll use [`Maui.Graphics`](https://maui.graphics) to render a test image in a Windows Forms application (with SkiaSharp and OpenGL), but the same could be achieved with `System.Drawing` or other similar 2D graphics libraries.

<div class="text-center">

![](point-inside-rotated-rectangle.gif)

</div>

I started just knowing the width and height of my rectangle. I created an array of points representing its corners.

```cs
float rectWidth = 300;
float rectHeight = 150;

PointF[] rectCorners =
{
    new(0, 0),
    new(rectWidth, 0),
    new(rectWidth, rectHeight),
    new(0, rectHeight),
};
```

I then rotated the rectangle around an origin point by applying a rotation transformation to each corner.

```cs
PointF origin = new(200, 300); // center of rotation
double angleRadians = 1.234;
PointF[] rotatedCorners = rectCorners.Select(x => Rotate(origin, x, angleRadians)).ToArray();
```

```cs
private PointF Rotate(PointF origin, PointF point, double radians)
{
	double dx = point.X * Math.Cos(radians) - point.Y * Math.Sin(radians);
	double dy = point.X * Math.Sin(radians) + point.Y * Math.Cos(radians);
	return new PointF(origin.X + (float)dx, origin.Y + (float)dy);
}
```

To determine if a given point is inside the rotated rectangle I called this method which accepts the point of interest and an array containing the four corners of the rotated rectangle.

```cs
public bool IsPointInsideRectangle(PointF pt, PointF[] rectCorners)
{
    double x1 = rectCorners[0].X;
    double x2 = rectCorners[1].X;
    double x3 = rectCorners[2].X;
    double x4 = rectCorners[3].X;

    double y1 = rectCorners[0].Y;
    double y2 = rectCorners[1].Y;
    double y3 = rectCorners[2].Y;
    double y4 = rectCorners[3].Y;

    double a1 = Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    double a2 = Math.Sqrt((x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3));
    double a3 = Math.Sqrt((x3 - x4) * (x3 - x4) + (y3 - y4) * (y3 - y4));
    double a4 = Math.Sqrt((x4 - x1) * (x4 - x1) + (y4 - y1) * (y4 - y1));

    double b1 = Math.Sqrt((x1 - pt.X) * (x1 - pt.X) + (y1 - pt.Y) * (y1 - pt.Y));
    double b2 = Math.Sqrt((x2 - pt.X) * (x2 - pt.X) + (y2 - pt.Y) * (y2 - pt.Y));
    double b3 = Math.Sqrt((x3 - pt.X) * (x3 - pt.X) + (y3 - pt.Y) * (y3 - pt.Y));
    double b4 = Math.Sqrt((x4 - pt.X) * (x4 - pt.X) + (y4 - pt.Y) * (y4 - pt.Y));

    double u1 = (a1 + b1 + b2) / 2;
    double u2 = (a2 + b2 + b3) / 2;
    double u3 = (a3 + b3 + b4) / 2;
    double u4 = (a4 + b4 + b1) / 2;

    double A1 = Math.Sqrt(u1 * (u1 - a1) * (u1 - b1) * (u1 - b2));
    double A2 = Math.Sqrt(u2 * (u2 - a2) * (u2 - b2) * (u2 - b3));
    double A3 = Math.Sqrt(u3 * (u3 - a3) * (u3 - b3) * (u3 - b4));
    double A4 = Math.Sqrt(u4 * (u4 - a4) * (u4 - b4) * (u4 - b1));

    double difference = A1 + A2 + A3 + A4 - a1 * a2;
    return difference < 1;
}
```

## How does it work?

Consider 4 triangles formed by lines between the point and the 4 corners...

**If the point is _inside_ the rectangle,** the area of the four triangles will _equal_ the area of the rectangle.

<div class="text-center">

![](rectangle-point-inside.png)

</div>

**If the point is _outside_ the rectangle,** the area of the four triangles will be _greater_ than the area of the rectangle.

<div class="text-center">

![](rectangle-point-outside.png)

</div>

**The code above calculates the area of the 4 rectangles** and returns `true` if it is approximately equal to the area of the rectangle.

## Notes

* In practice you'll probably want to use a more intelligent data structure than a 4-element `Pointf[]` when calling these functions.

* The points in the array are clockwise, but I assume this method will work regardless of the order of the points in the array.

* At the very end of `IsPointInsideRectangle()` the final decision is made based on a distance being less than a given value. It's true that the cursor will be inside the rectangle if the distance is exactly zero, but with the possible accumulation of floating-point math errors this seemed like a safer option.

## Resources
* Source code for this application: [Form1.cs](https://github.com/swharden/Csharp-Data-Visualization/blob/203e024253a2545fc325d1f68d2861a1b9fac74d/projects/rotated-rectangle-intersection/Form1.cs)

* Thanks [@BambOoxX](https://github.com/BambOoxX) for suggesting this in [ScottPlot/PR#1616](https://github.com/ScottPlot/ScottPlot/pull/1616)

* [How to check if a point is inside a rectangle?](https://math.stackexchange.com/q/190403) (StackExchange)

* [swharden / C# Data Visualization](https://github.com/swharden/Csharp-Data-Visualization)
January 22nd, 2022

Spline Interpolation with C#

I recently had the need to create a smoothed curve from a series of X/Y data points in a C# application. I achieved this using cubic spline interpolation. I prefer this strategy because I can control the exact number of points in the output curve, and the generated curve (given sufficient points) will pass through the original data making it excellent for data smoothing applications.

The code below is an adaptation of original work by Ryan Seghers (links below) that I modified to narrow its scope, support double types, use modern language features, and operate statelessly in a functional style with all static methods.

  • It targets .NET Standard 2.0 so it can be used in .NET Framework and .NET Core applications.

  • Input Xs and Ys must be the same length but do not need to be ordered.

  • The interpolated curve may have any number of points (not just even multiples of the input length), and may even have fewer points than the original data.

  • Users cannot define start or end slopes so the curve generated is a natural spline.

public static class Cubic
{
    /// <summary>
    /// Generate a smooth (interpolated) curve that follows the path of the given X/Y points
    /// </summary>
    public static (double[] xs, double[] ys) InterpolateXY(double[] xs, double[] ys, int count)
    {
        if (xs is null || ys is null || xs.Length != ys.Length)
            throw new ArgumentException($"{nameof(xs)} and {nameof(ys)} must have same length");

        int inputPointCount = xs.Length;
        double[] inputDistances = new double[inputPointCount];
        for (int i = 1; i < inputPointCount; i++)
        {
            double dx = xs[i] - xs[i - 1];
            double dy = ys[i] - ys[i - 1];
            double distance = Math.Sqrt(dx * dx + dy * dy);
            inputDistances[i] = inputDistances[i - 1] + distance;
        }

        double meanDistance = inputDistances.Last() / (count - 1);
        double[] evenDistances = Enumerable.Range(0, count).Select(x => x * meanDistance).ToArray();
        double[] xsOut = Interpolate(inputDistances, xs, evenDistances);
        double[] ysOut = Interpolate(inputDistances, ys, evenDistances);
        return (xsOut, ysOut);
    }

    private static double[] Interpolate(double[] xOrig, double[] yOrig, double[] xInterp)
    {
        (double[] a, double[] b) = FitMatrix(xOrig, yOrig);

        double[] yInterp = new double[xInterp.Length];
        for (int i = 0; i < yInterp.Length; i++)
        {
            int j;
            for (j = 0; j < xOrig.Length - 2; j++)
                if (xInterp[i] <= xOrig[j + 1])
                    break;

            double dx = xOrig[j + 1] - xOrig[j];
            double t = (xInterp[i] - xOrig[j]) / dx;
            double y = (1 - t) * yOrig[j] + t * yOrig[j + 1] +
                t * (1 - t) * (a[j] * (1 - t) + b[j] * t);
            yInterp[i] = y;
        }

        return yInterp;
    }

    private static (double[] a, double[] b) FitMatrix(double[] x, double[] y)
    {
        int n = x.Length;
        double[] a = new double[n - 1];
        double[] b = new double[n - 1];
        double[] r = new double[n];
        double[] A = new double[n];
        double[] B = new double[n];
        double[] C = new double[n];

        double dx1, dx2, dy1, dy2;

        dx1 = x[1] - x[0];
        C[0] = 1.0f / dx1;
        B[0] = 2.0f * C[0];
        r[0] = 3 * (y[1] - y[0]) / (dx1 * dx1);

        for (int i = 1; i < n - 1; i++)
        {
            dx1 = x[i] - x[i - 1];
            dx2 = x[i + 1] - x[i];
            A[i] = 1.0f / dx1;
            C[i] = 1.0f / dx2;
            B[i] = 2.0f * (A[i] + C[i]);
            dy1 = y[i] - y[i - 1];
            dy2 = y[i + 1] - y[i];
            r[i] = 3 * (dy1 / (dx1 * dx1) + dy2 / (dx2 * dx2));
        }

        dx1 = x[n - 1] - x[n - 2];
        dy1 = y[n - 1] - y[n - 2];
        A[n - 1] = 1.0f / dx1;
        B[n - 1] = 2.0f * A[n - 1];
        r[n - 1] = 3 * (dy1 / (dx1 * dx1));

        double[] cPrime = new double[n];
        cPrime[0] = C[0] / B[0];
        for (int i = 1; i < n; i++)
            cPrime[i] = C[i] / (B[i] - cPrime[i - 1] * A[i]);

        double[] dPrime = new double[n];
        dPrime[0] = r[0] / B[0];
        for (int i = 1; i < n; i++)
            dPrime[i] = (r[i] - dPrime[i - 1] * A[i]) / (B[i] - cPrime[i - 1] * A[i]);

        double[] k = new double[n];
        k[n - 1] = dPrime[n - 1];
        for (int i = n - 2; i >= 0; i--)
            k[i] = dPrime[i] - cPrime[i] * k[i + 1];

        for (int i = 1; i < n; i++)
        {
            dx1 = x[i] - x[i - 1];
            dy1 = y[i] - y[i - 1];
            a[i - 1] = k[i - 1] * dx1 - dy1;
            b[i - 1] = -k[i] * dx1 + dy1;
        }

        return (a, b);
    }
}

Usage

This sample .NET 6 console application uses the class above to create a smoothed (interpolated) curve from a set of random X/Y points. It then plots the original data and the interpolated curve using ScottPlot.

// generate sample data using a random walk
Random rand = new(1268);
int pointCount = 20;
double[] xs1 = new double[pointCount];
double[] ys1 = new double[pointCount];
for (int i = 1; i < pointCount; i++)
{
    xs1[i] = xs1[i - 1] + rand.NextDouble() - .5;
    ys1[i] = ys1[i - 1] + rand.NextDouble() - .5;
}

// Use cubic interpolation to smooth the original data
(double[] xs2, double[] ys2) = Cubic.InterpolateXY(xs1, ys1, 200);

// Plot the original vs. interpolated data
var plt = new ScottPlot.Plot(600, 400);
plt.AddScatter(xs1, ys1, label: "original", markerSize: 7);
plt.AddScatter(xs2, ys2, label: "interpolated", markerSize: 3);
plt.Legend();
plt.SaveFig("interpolation.png");

Additional Interpolation Methods

There are many different methods that can smooth data. Common methods include Bézier splines, Catmull-Rom splines, corner-cutting Chaikin curves, and Cubic splines. I recently implemented these strageies to include with ScottPlot (a MIT-licensed 2D plotting library for .NET). Visit ScottPlot.net to find the source code for that project and search for the Interpolation namespace.

1D Interpolation

A set of X/Y points can be interpolated such that they are evenly spaced on the X axis. This 1D interpolation can be used to change the sampling rate of time series data. For more information about 1D interpolation see my other blog post: Resample Time Series Data using Cubic Spline Interpolation

Resources

Markdown source code last modified on June 24th, 2022
---
Title: Spline Interpolation with C# 
Description: How to smooth X/Y data using spline interpolation in Csharp
Date: 2022-01-22 4:00PM EST
Tags: csharp
---

# Spline Interpolation with C# 

**I recently had the need to create a smoothed curve from a series of X/Y data points in a C# application.** I achieved this using cubic [spline interpolation](https://en.wikipedia.org/wiki/Spline_interpolation). I prefer this strategy because I can control the exact number of points in the output curve, and the generated curve (given sufficient points) will pass through the original data making it excellent for data smoothing applications.

<div class='text-center'>

![](screenshot.gif)

</div>

The code below is an adaptation of original work by Ryan Seghers (links below) that I modified to narrow its scope, support `double` types, use modern language features, and operate statelessly in a functional style with all `static` methods.

* It targets `.NET Standard 2.0` so it can be used in .NET Framework and .NET Core applications.

* Input `Xs` and `Ys` must be the same length but do not need to be ordered.

* The interpolated curve may have any number of points (not just even multiples of the input length), and may even have fewer points than the original data.

* Users cannot define start or end slopes so the curve generated is a _natural_ spline.

```cs
public static class Cubic
{
    /// <summary>
    /// Generate a smooth (interpolated) curve that follows the path of the given X/Y points
    /// </summary>
    public static (double[] xs, double[] ys) InterpolateXY(double[] xs, double[] ys, int count)
    {
        if (xs is null || ys is null || xs.Length != ys.Length)
            throw new ArgumentException($"{nameof(xs)} and {nameof(ys)} must have same length");

        int inputPointCount = xs.Length;
        double[] inputDistances = new double[inputPointCount];
        for (int i = 1; i < inputPointCount; i++)
        {
            double dx = xs[i] - xs[i - 1];
            double dy = ys[i] - ys[i - 1];
            double distance = Math.Sqrt(dx * dx + dy * dy);
            inputDistances[i] = inputDistances[i - 1] + distance;
        }

        double meanDistance = inputDistances.Last() / (count - 1);
        double[] evenDistances = Enumerable.Range(0, count).Select(x => x * meanDistance).ToArray();
        double[] xsOut = Interpolate(inputDistances, xs, evenDistances);
        double[] ysOut = Interpolate(inputDistances, ys, evenDistances);
        return (xsOut, ysOut);
    }

    private static double[] Interpolate(double[] xOrig, double[] yOrig, double[] xInterp)
    {
        (double[] a, double[] b) = FitMatrix(xOrig, yOrig);

        double[] yInterp = new double[xInterp.Length];
        for (int i = 0; i < yInterp.Length; i++)
        {
            int j;
            for (j = 0; j < xOrig.Length - 2; j++)
                if (xInterp[i] <= xOrig[j + 1])
                    break;

            double dx = xOrig[j + 1] - xOrig[j];
            double t = (xInterp[i] - xOrig[j]) / dx;
            double y = (1 - t) * yOrig[j] + t * yOrig[j + 1] +
                t * (1 - t) * (a[j] * (1 - t) + b[j] * t);
            yInterp[i] = y;
        }

        return yInterp;
    }

    private static (double[] a, double[] b) FitMatrix(double[] x, double[] y)
    {
        int n = x.Length;
        double[] a = new double[n - 1];
        double[] b = new double[n - 1];
        double[] r = new double[n];
        double[] A = new double[n];
        double[] B = new double[n];
        double[] C = new double[n];

        double dx1, dx2, dy1, dy2;

        dx1 = x[1] - x[0];
        C[0] = 1.0f / dx1;
        B[0] = 2.0f * C[0];
        r[0] = 3 * (y[1] - y[0]) / (dx1 * dx1);

        for (int i = 1; i < n - 1; i++)
        {
            dx1 = x[i] - x[i - 1];
            dx2 = x[i + 1] - x[i];
            A[i] = 1.0f / dx1;
            C[i] = 1.0f / dx2;
            B[i] = 2.0f * (A[i] + C[i]);
            dy1 = y[i] - y[i - 1];
            dy2 = y[i + 1] - y[i];
            r[i] = 3 * (dy1 / (dx1 * dx1) + dy2 / (dx2 * dx2));
        }

        dx1 = x[n - 1] - x[n - 2];
        dy1 = y[n - 1] - y[n - 2];
        A[n - 1] = 1.0f / dx1;
        B[n - 1] = 2.0f * A[n - 1];
        r[n - 1] = 3 * (dy1 / (dx1 * dx1));

        double[] cPrime = new double[n];
        cPrime[0] = C[0] / B[0];
        for (int i = 1; i < n; i++)
            cPrime[i] = C[i] / (B[i] - cPrime[i - 1] * A[i]);

        double[] dPrime = new double[n];
        dPrime[0] = r[0] / B[0];
        for (int i = 1; i < n; i++)
            dPrime[i] = (r[i] - dPrime[i - 1] * A[i]) / (B[i] - cPrime[i - 1] * A[i]);

        double[] k = new double[n];
        k[n - 1] = dPrime[n - 1];
        for (int i = n - 2; i >= 0; i--)
            k[i] = dPrime[i] - cPrime[i] * k[i + 1];

        for (int i = 1; i < n; i++)
        {
            dx1 = x[i] - x[i - 1];
            dy1 = y[i] - y[i - 1];
            a[i - 1] = k[i - 1] * dx1 - dy1;
            b[i - 1] = -k[i] * dx1 + dy1;
        }

        return (a, b);
    }
}
```

## Usage

This sample .NET 6 console application uses the class above to create a smoothed (interpolated) curve from a set of random X/Y points. It then plots the original data and the interpolated curve using [ScottPlot](https://scottplot.net).

```cs
// generate sample data using a random walk
Random rand = new(1268);
int pointCount = 20;
double[] xs1 = new double[pointCount];
double[] ys1 = new double[pointCount];
for (int i = 1; i < pointCount; i++)
{
    xs1[i] = xs1[i - 1] + rand.NextDouble() - .5;
    ys1[i] = ys1[i - 1] + rand.NextDouble() - .5;
}

// Use cubic interpolation to smooth the original data
(double[] xs2, double[] ys2) = Cubic.InterpolateXY(xs1, ys1, 200);

// Plot the original vs. interpolated data
var plt = new ScottPlot.Plot(600, 400);
plt.AddScatter(xs1, ys1, label: "original", markerSize: 7);
plt.AddScatter(xs2, ys2, label: "interpolated", markerSize: 3);
plt.Legend();
plt.SaveFig("interpolation.png");
```

<div class='text-center'>

![](interpolation.png)

</div>

## Additional Interpolation Methods

There are many different methods that can smooth data. Common methods include [Bézier splines](https://en.wikipedia.org/wiki/B%C3%A9zier_curve), [Catmull-Rom splines](https://www.cs.cmu.edu/~fp/courses/graphics/asst5/catmullRom.pdf), [corner-cutting Chaikin curves](https://www.cs.unc.edu/~dm/UNC/COMP258/LECTURES/Chaikins-Algorithm.pdf), and [Cubic splines](https://en.wikipedia.org/wiki/Spline_interpolation). I recently implemented these strageies to include with ScottPlot (a MIT-licensed 2D plotting library for .NET). Visit [ScottPlot.net](https://ScottPlot.NET) to find the source code for that project and search for the `Interpolation` namespace.

<div class='text-center'>

![](csharp-spline-interpolation.png)

</div>

## 1D Interpolation

A set of X/Y points can be interpolated such that they are evenly spaced on the X axis. This 1D interpolation can be used to change the sampling rate of time series data. For more information about 1D interpolation see my other blog post: [Resample Time Series Data using Cubic Spline Interpolation](https://swharden.com/blog/2022-06-23-resample-interpolation/)

<a href="https://swharden.com/blog/2022-06-23-resample-interpolation/">
<img src="https://swharden.com/blog/2022-06-23-resample-interpolation/2-resample.png" class="mx-auto d-block mb-5">
</a>

## Resources
* [Cubic Spline Interpolation source code](https://github.com/SCToolsfactory/SCJMapper-V2/blob/master/OGL/CubicSpline.cs) by Ryan Seghers (MIT license)
* [C# Cubic Spline Interpolation article](https://www.codeproject.com/Articles/560163/Csharp-Cubic-Spline-Interpolation) by Ryan Seghers (Code Project)
* [Numerical Recipes in C++: Cubic Spline Interpolation
](http://www.foo.be/docs-free/Numerical_Recipe_In_C/c3-3.pdf)
* [Fast Cubic Spline Interpolation
](https://arxiv.org/pdf/2001.09253.pdf) by Haysn Hornbeck
* Download this project from [C# Data Visualization](https://github.com/swharden/Csharp-Data-Visualization) on GitHub
* [Bézier Spline Interpolation](http://scaledinnovation.com/analytics/splines/aboutSplines.html)
October 17th, 2021

NDepend Status Badges

Many project websites and readmes have status badges that display build status, project details, or code metrics. badgen.net and shields.io are popular services for dynamically generating status badges as SVG files using HTTP requests. This article demonstrates how I use C# and Microsoft.Maui.Graphics to build status badges from NDepend static analysis reports. Source code for this project is available on GitHub.

NDepend Trend Data XML

NDepend can analyze a code base at different points in time and display code metric trends. See NDepend: Trend Monitoring for a full description. These metrics are stored in an XML file available in the HTML build folder.

Metric Index

The XML file contains many Root/MetricIndex/Metric elements that describe each metric and its units. This can be parsed to obtain the Name and Unit for each metric.

<Root>
  <MetricIndex>
    <Metric Name="# New Issues since Baseline" Unit="issues" />
    <Metric Name="# Issues Fixed since Baseline" Unit="issues" />
    <Metric Name="# Issues Worsened since Baseline" Unit="issues" />
    <Metric Name="# Issues with severity Blocker" Unit="issues" />
    <Metric Name="# Issues with severity Critical" Unit="issues" />
    <Metric Name="# Issues with severity High" Unit="issues" />
    <Metric Name="# Issues with severity Medium" Unit="issues" />
    ...
  </MetricIndex>
</Root>

Metrics by DateTime

The XML file contains multiple Root/M/R elements that contain the value of each metric at a distinct time point. Numerical metrics have been converted to strings separated by the | character. Metric values for each time point are in the same order as the metric index.

<Root>
  <M>
    <R D="10/16/2021 11:58:04 AM" V="0|0|0|0|1|598|2177|...|19|133" />
    <R D="10/03/2021 04:15:24 PM" V="0|0|0|0|1|593|2160|...|19|132" />
    ...
  </M>
</Root>

Read NDepend Trend XML with C#

To read timestamped metrics from the NDepend XML I started by creating a C# record to hold an individual timestamped metric:

public record Metric
{
    public DateTime DateTime { get; init; }
    public string Name { get; init; }
    public string Unit { get; init; }
    public string Value { get; init; }
}

I then reached for using System.Xml.Linq and using System.Xml.XPath to extract a big list of timestamped metrics from the NDepend XML file:

Metric[] GetMetricsFromXML(string xmlFilePath)
{
    XDocument doc = XDocument.Load(xmlFilePath);
    List<Metric> baseMetrics = new();
    foreach (var el in doc.XPathSelectElement("/Root/MetricIndex").Elements())
    {
        string name = el.Attribute("Name").Value;
        string unit = el.Attribute("Unit").Value;
        baseMetrics.Add(new Metric() { Name = name, Unit = unit });
    }

    List<Metric> allMetrics = new();
    foreach (var runElement in doc.XPathSelectElement("/Root/M").Elements())
    {
        DateTime runDateTime = DateTime.Parse(runElement.Attribute("D").Value);
        string[] values = runElement.Attribute("V").Value.Split("|");

        List<Metric> runMetrics = new();
        for (int i = 0; i < baseMetrics.Count; i++)
            runMetrics.Add(baseMetrics[i] with { DateTime = runDateTime, Value = values[i] });

        allMetrics.AddRange(runMetrics);
    }

    return allMetrics.ToArray();
}

I found it convenient to make a helper function to get only the latest metrics:

Metric[] GetLatestMetrics(Metric[] metrics)
{
    DateTime latestDateTime = metrics.Select(x => x.DateTime).Distinct().OrderBy(x => x).Last();
    return metrics.Where(x => x.DateTime == latestDateTime).ToArray();
}

Generate NDepend Status Badges

I've already written how to make status badges with C# and Maui.Graphics, but that strategy only generates PNG files. For this project I also chose to generate SVG files. Rather than discuss that in detail, I'll just show to the source code for an example SVG file.

It is important to note that in order to know the image width I must measure the string width. In HTML environments this could be done with vanilla Javascript, but in a C# environment I reached for Microsoft.Maui.Graphics (see how to MeasureString() with Maui.Graphics).

<svg xmlns='http://www.w3.org/2000/svg'
    xmlns:xlink='http://www.w3.org/1999/xlink' width='237' height='20' role='img' aria-label='languages: 5'>
    <title>Average # Lines of Code for Types: 25.96</title>
    <linearGradient id='s' x2='0' y2='100%'>
        <!-- linear gradient to use for the background shadow -->
        <stop offset='0' stop-color='#bbb' stop-opacity='.1'/>
        <stop offset='1' stop-opacity='.1'/>
    </linearGradient>
    <clipPath id='r'>
        <!-- clip to a rectangle with rounded edges -->
        <rect width='237' height='20' rx='3' fill='#fff'/>
    </clipPath>
    <g clip-path='url(#r)'>
        <!-- left background -->
        <rect width='195' height='20' fill='#555'/>
        <!-- right background -->
        <rect x='195' width='42' height='20' fill='#007ec6'/>
        <!-- background shadow -->
        <rect width='237' height='20' fill='url(#s)'/>
    </g>
    <g fill='#FFF' text-anchor='center' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='110'>
        <!-- left text semitransparent shadow then white text -->
        <text aria-hidden='true' x='40' y='150' fill='#010101' fill-opacity='.3' transform='scale(.1)' textLength='1854'>Average # Lines of Code for Types</text>
        <text x='40' y='140' transform='scale(.1)' fill='#FFF' textLength='1854'>Average # Lines of Code for Types</text>
        <!-- right text semitransparent shadow then white text -->
        <text aria-hidden='true' x='1994' y='150' fill='#010101' fill-opacity='.3' transform='scale(.1)' textLength='300'>25.96</text>
        <text x='1994' y='140' transform='scale(.1)' fill='#FFF' textLength='300'>25.96</text>
    </g>
</svg>

One day Maui.Graphics may offer SVG export support (issue #103) but for now generating these files discretely isn't too bad.

Badges

After putting it all together these are the badges generated by analyzing the current ScottPlot code base:

SVG

PNG

Resources

Markdown source code last modified on October 17th, 2021
---
Title: NDepend Status Badges
Description: How I used C# and Maui.Graphics to generate status badges for NDepend static analysis metrics
Date: 2021-10-17 1:20PM EST
Tags: csharp, maui
---

# NDepend Status Badges

**Many project websites and readmes have status badges** that display build status, project details, or code metrics. [badgen.net](https://badgen.net/) and [shields.io](https://shields.io/) are popular services for dynamically generating status badges as SVG files using HTTP requests. This article demonstrates how I use C# and `Microsoft.Maui.Graphics` to build status badges from [**NDepend**](https://www.ndepend.com/) static analysis reports. Source code for this project is [available on GitHub](https://github.com/swharden/NDepend-Badges).

<div class='text-center'>

<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/classes.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-types.svg' />

</div>

## NDepend Trend Data XML

NDepend can analyze a code base at different points in time and display code metric trends. See [NDepend: Trend Monitoring](https://www.ndepend.com/features/trend-monitoring#Trend) for a full description. These metrics are stored in an XML file available in the HTML build folder.

### Metric Index

The XML file contains many `Root/MetricIndex/Metric` elements that describe each metric and its units. This can be parsed to obtain the `Name` and `Unit` for each metric.

```xml
<Root>
  <MetricIndex>
    <Metric Name="# New Issues since Baseline" Unit="issues" />
    <Metric Name="# Issues Fixed since Baseline" Unit="issues" />
    <Metric Name="# Issues Worsened since Baseline" Unit="issues" />
    <Metric Name="# Issues with severity Blocker" Unit="issues" />
    <Metric Name="# Issues with severity Critical" Unit="issues" />
    <Metric Name="# Issues with severity High" Unit="issues" />
    <Metric Name="# Issues with severity Medium" Unit="issues" />
    ...
  </MetricIndex>
</Root>
```

### Metrics by DateTime

The XML file contains multiple `Root/M/R` elements that contain the value of each metric at a distinct time point. Numerical metrics have been converted to strings separated by the `|` character. Metric values for each time point are in the same order as the metric index.

```xml
<Root>
  <M>
    <R D="10/16/2021 11:58:04 AM" V="0|0|0|0|1|598|2177|...|19|133" />
    <R D="10/03/2021 04:15:24 PM" V="0|0|0|0|1|593|2160|...|19|132" />
	...
  </M>
</Root>
```

## Read NDepend Trend XML with C# 

To read timestamped metrics from the NDepend XML I started by creating a C# record to hold an individual timestamped metric:

```cs
public record Metric
{
    public DateTime DateTime { get; init; }
    public string Name { get; init; }
    public string Unit { get; init; }
    public string Value { get; init; }
}
```

I then reached for `using System.Xml.Linq` and `using System.Xml.XPath` to extract a big list of timestamped metrics from the NDepend XML file:

```cs
Metric[] GetMetricsFromXML(string xmlFilePath)
{
    XDocument doc = XDocument.Load(xmlFilePath);
    List<Metric> baseMetrics = new();
    foreach (var el in doc.XPathSelectElement("/Root/MetricIndex").Elements())
    {
        string name = el.Attribute("Name").Value;
        string unit = el.Attribute("Unit").Value;
        baseMetrics.Add(new Metric() { Name = name, Unit = unit });
    }

    List<Metric> allMetrics = new();
    foreach (var runElement in doc.XPathSelectElement("/Root/M").Elements())
    {
        DateTime runDateTime = DateTime.Parse(runElement.Attribute("D").Value);
        string[] values = runElement.Attribute("V").Value.Split("|");

        List<Metric> runMetrics = new();
        for (int i = 0; i < baseMetrics.Count; i++)
            runMetrics.Add(baseMetrics[i] with { DateTime = runDateTime, Value = values[i] });

        allMetrics.AddRange(runMetrics);
    }

    return allMetrics.ToArray();
}
```

I found it convenient to make a helper function to get only the latest metrics:

```cs
Metric[] GetLatestMetrics(Metric[] metrics)
{
    DateTime latestDateTime = metrics.Select(x => x.DateTime).Distinct().OrderBy(x => x).Last();
    return metrics.Where(x => x.DateTime == latestDateTime).ToArray();
}
```

## Generate NDepend Status Badges

I've already written [how to make status badges with C# and Maui.Graphics](https://swharden.com/blog/2021-11-16-maui-graphics-badges/), but that strategy only generates PNG files. For this project I also chose to generate SVG files. Rather than discuss that in detail, I'll just show to the source code for an example SVG file. 

It is important to note that in order to know the image width I must measure the string width. In HTML environments this could be done with vanilla Javascript, but in a C# environment I reached for `Microsoft.Maui.Graphics` (see [how to MeasureString() with Maui.Graphics](https://swharden.com/blog/2021-10-16-maui-graphics-measurestring)).

<div class='text-center'>

<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-types.svg' />

</div>

```xml
<svg xmlns='http://www.w3.org/2000/svg'
    xmlns:xlink='http://www.w3.org/1999/xlink' width='237' height='20' role='img' aria-label='languages: 5'>
    <title>Average # Lines of Code for Types: 25.96</title>
    <linearGradient id='s' x2='0' y2='100%'>
        <!-- linear gradient to use for the background shadow -->
        <stop offset='0' stop-color='#bbb' stop-opacity='.1'/>
        <stop offset='1' stop-opacity='.1'/>
    </linearGradient>
    <clipPath id='r'>
        <!-- clip to a rectangle with rounded edges -->
        <rect width='237' height='20' rx='3' fill='#fff'/>
    </clipPath>
    <g clip-path='url(#r)'>
        <!-- left background -->
        <rect width='195' height='20' fill='#555'/>
        <!-- right background -->
        <rect x='195' width='42' height='20' fill='#007ec6'/>
        <!-- background shadow -->
        <rect width='237' height='20' fill='url(#s)'/>
    </g>
    <g fill='#FFF' text-anchor='center' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='110'>
        <!-- left text semitransparent shadow then white text -->
        <text aria-hidden='true' x='40' y='150' fill='#010101' fill-opacity='.3' transform='scale(.1)' textLength='1854'>Average # Lines of Code for Types</text>
        <text x='40' y='140' transform='scale(.1)' fill='#FFF' textLength='1854'>Average # Lines of Code for Types</text>
        <!-- right text semitransparent shadow then white text -->
        <text aria-hidden='true' x='1994' y='150' fill='#010101' fill-opacity='.3' transform='scale(.1)' textLength='300'>25.96</text>
        <text x='1994' y='140' transform='scale(.1)' fill='#FFF' textLength='300'>25.96</text>
    </g>
</svg>
```

One day Maui.Graphics may offer SVG export support ([issue #103](https://github.com/dotnet/Microsoft.Maui.Graphics/issues/103)) but for now generating these files discretely isn't too bad.

## Badges

After putting it all together these are the badges generated by analyzing the current [ScottPlot](https://scottplot.net) code base:

### SVG

<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/new-issues-since-baseline.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-fixed-since-baseline.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-worsened-since-baseline.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-blocker.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-critical.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-high.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-medium.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-low.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/blocker-critical-high-issues.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/suppressed-issues.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/rules.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/rules-violated.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/critical-rules-violated.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/quality-gates.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/quality-gates-warn.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/quality-gates-fail.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/percentage-debt-metric.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/debt-metric.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/annual-interest-metric.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/breaking-point.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/breaking-point-of-blocker-critical-high-issues.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code-justmycode.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code-notmycode.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/source-files.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/il-instructions.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/il-instructions-notmycode.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/assemblies.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/namespaces.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/types.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/public-types.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/classes.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/abstract-classes.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/interfaces.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/structures.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/abstract-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/concrete-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/fields.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-lines-of-code-for-methods-justmycode.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-methods-with-at-least-3-lines-of-code.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-lines-of-code-for-types-justmycode.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-types.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-cyclomatic-complexity-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-cyclomatic-complexity-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-il-cyclomatic-complexity-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-il-cyclomatic-complexity-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-il-nesting-depth-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-il-nesting-depth-for-methods.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-of-methods-for-types.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-methods-for-types.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-of-methods-for-interfaces.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-methods-for-interfaces.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code-uncoverable.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-assemblies-used.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-namespaces-used.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-types-used.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-methods-used.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-fields-used.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/rules-violations.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/critical-rules.svg' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/critical-rules-violations.svg' />

### PNG

<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/new-issues-since-baseline.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-fixed-since-baseline.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-worsened-since-baseline.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-blocker.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-critical.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-high.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-medium.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues-with-severity-low.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/blocker-critical-high-issues.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/issues.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/suppressed-issues.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/rules.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/rules-violated.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/critical-rules-violated.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/quality-gates.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/quality-gates-warn.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/quality-gates-fail.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/percentage-debt-metric.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/debt-metric.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/annual-interest-metric.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/breaking-point.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/breaking-point-of-blocker-critical-high-issues.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code-justmycode.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code-notmycode.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/source-files.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/il-instructions.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/il-instructions-notmycode.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/assemblies.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/namespaces.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/types.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/public-types.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/classes.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/abstract-classes.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/interfaces.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/structures.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/abstract-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/concrete-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/fields.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-lines-of-code-for-methods-justmycode.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-methods-with-at-least-3-lines-of-code.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-lines-of-code-for-types-justmycode.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-lines-of-code-for-types.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-cyclomatic-complexity-for-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-cyclomatic-complexity-for-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-il-cyclomatic-complexity-for-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-il-cyclomatic-complexity-for-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-il-nesting-depth-for-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-il-nesting-depth-for-methods.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-of-methods-for-types.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-methods-for-types.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/max-of-methods-for-interfaces.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/average-methods-for-interfaces.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/lines-of-code-uncoverable.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-assemblies-used.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-namespaces-used.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-types-used.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-methods-used.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/third-party-fields-used.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/rules-violations.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/critical-rules.png' />
<img style='margin-top: .1em; margin-bottom: .1em;' src='https://swharden.com/blog/2021-11-17-ndepend-badges/badges/critical-rules-violations.png' />


## Resources
* Source code on GitHub: https://github.com/swharden/NDepend-Badges
* NDepend website: https://www.ndepend.com/
* NDepend sample reports: https://www.ndepend.com/sample-reports/
* [How to MeasureString() with Maui.Graphics](https://swharden.com/blog/2021-10-16-maui-graphics-measurestring/)
* [Status Badges with Maui.Graphics](https://swharden.com/blog/2021-11-16-maui-graphics-badges/)
* [Draw with Maui.Graphics and Skia in a C# Console Application](https://swharden.com/blog/2021-08-01-maui-skia-console/)
October 16th, 2021

Status Badges with Maui.Graphics

Status badges are popular decorators on GitHub readme pages and project websites. Badgen.net and shields.io are popular HTTP APIs for dynamically generating SVG status badges. In this article we will use the new Microsoft.Maui.Graphics package to generate status badges from a C# console application. This application can be downloaded: BadgeApp.zip

Badge.cs

The Badge class contains all the logic needed to render and save a badge as a PNG file.

This code demonstrates a few advanced topics which are worth considering:

  • image scaling
  • state management (save/restore)
  • clipping
  • rounded rectangles
  • string measurement
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

public class Badge
{
    readonly string Name;
    readonly string Value;
    readonly SizeF NameSize;
    readonly SizeF ValueSize;

    // Customize these to change the style of the button
    public Color BackgroundLeft = Color.FromArgb("#666");
    public Color BackgroundRight = Color.FromArgb("#08C");
    public Color BackgroundLiner = Colors.White.WithAlpha(.15f);
    public Color FontColor = Colors.White;
    public Color FontShadow = Colors.Black.WithAlpha(.5f);
    public Color OverlayTop = Colors.Black.WithAlpha(0);
    public Color OverlayBottom = Colors.Black.WithAlpha(.25f);

    public Badge(string name, string value)
    {
        Name = name;
        Value = value;
        NameSize = MeasureString(name);
        ValueSize = MeasureString(value);
    }

    public void SavePng(string pngFilePath, float scale = 1)
    {
        float totalWidth = NameSize.Width + ValueSize.Width;
        int imageWidth = (int)totalWidth + 22;
        RectangleF imageRect = new(0, 0, imageWidth, 20);

        int scaledWidth = (int)(imageRect.Width * scale);
        int scaledHeight = (int)(imageRect.Height * scale);
        BitmapExportContext bmp = SkiaGraphicsService.Instance.CreateBitmapExportContext(scaledWidth, scaledHeight);
        ICanvas canvas = bmp.Canvas;
        canvas.Scale(scale, scale);

        // left background
        canvas.FillColor = BackgroundLeft;
        canvas.FillRoundedRectangle(imageRect, 5);

        // right background
        float bg2x = 10 + NameSize.Width;
        canvas.SaveState();
        canvas.ClipRectangle(bg2x, 0, bmp.Width, bmp.Height);
        canvas.FillColor = BackgroundRight;
        canvas.FillRoundedRectangle(imageRect, 5);
        canvas.RestoreState();

        // vertical line
        canvas.StrokeColor = BackgroundLiner;
        canvas.DrawLine(bg2x, 0, bg2x, bmp.Height);

        // background overlay shadow
        var pt = new LinearGradientPaint() { StartColor = OverlayTop, EndColor = OverlayBottom };
        canvas.SetFillPaint(pt, new Point(0, 0), new Point(0, bmp.Height));
        canvas.FillRoundedRectangle(imageRect, 5);

        // draw text backgrounds
        canvas.FontSize = 12;
        float offsetY = 14;
        float offsetX1 = 5;
        float offsetX2 = 15;
        float shadowOffset = 1;

        // text shadow
        canvas.FontColor = FontShadow;
        canvas.DrawString(Name, offsetX1 + shadowOffset, offsetY + shadowOffset, HorizontalAlignment.Left);
        canvas.DrawString(Value, offsetX2 + NameSize.Width + shadowOffset, offsetY + shadowOffset, HorizontalAlignment.Left);

        // text foreground
        canvas.FontColor = FontColor;
        canvas.DrawString(Name, offsetX1, offsetY, HorizontalAlignment.Left);
        canvas.DrawString(Value, offsetX2 + NameSize.Width, offsetY, HorizontalAlignment.Left);

        // save the output
        bmp.WriteToFile(pngFilePath);
    }

    SizeF MeasureString(string text, string fontName = "Arial", float fontSize = 12)
    {
        var fontService = new SkiaFontService("", "");
        using SkiaSharp.SKTypeface typeFace = fontService.GetTypeface(fontName);
        using SkiaSharp.SKPaint paint = new() { Typeface = typeFace, TextSize = fontSize };
        float width = paint.MeasureText(text);
        float height = fontSize;
        return new SizeF(width, height);
    }
}

Program.cs

This simple program is all it takes to render and save a badge.

Badge myBadge = new("Maui", "Graphics");
myBadge.SavePng("demo.png");

Customization

You can reach into the Badge class and customize styles as desired.

Badge myBadge = new("Maui", "Graphics")
{
    BackgroundRight = Microsoft.Maui.Graphics.Color.FromArgb("#3cc51d"),
};
myBadge.SavePng("demo1b.png");

Image Scaling

Microsoft.Maui.Graphics natively supports image scaling. This allows you to create large badges without any loss in quality that would come from creating a small badge and resizing the bitmap.

Badge myBadge = new("Maui", "Graphics");
myBadge.SavePng("demo1.png", scale: 1);
myBadge.SavePng("demo2.png", scale: 2);
myBadge.SavePng("demo5.png", scale: 5);

Resources

Markdown source code last modified on May 26th, 2022
---
Title: Status Badges with Maui.Graphics
Description: How to use Microsoft.Maui.Graphics to render status badges
Date: 2021-10-16 8:40PM EST
Tags: csharp, maui, graphics
---

# Status Badges with Maui.Graphics

**Status badges are popular decorators on GitHub readme pages and project websites.** [Badgen.net](https://badgen.net) and [shields.io](https://shields.io) are popular HTTP APIs for dynamically generating SVG status badges. In this article we will use the new `Microsoft.Maui.Graphics` package to generate status badges from a C# console application. This application can be downloaded: [**BadgeApp.zip**](BadgeApp.zip)

<div class="text-center">

![](images/demo1.png)
![](images/demo1b.png)

</div>


<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="check-circle-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
  </symbol>
  <symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
  </symbol>
  <symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
  </symbol>
</svg>

<div class="alert alert-primary d-flex align-items-center" role="alert">
  <svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
  <div>
    <strong>UPDATE:</strong> This article was written when <code>Microsoft.Maui.Graphics</code> was still in preview. See <a href="https://swharden.com/blog/2022-05-25-maui-graphics/" class="fw-bold">Drawing with Maui Graphics (blog post)</a> and <a href="https://swharden.com/csdv/" class="fw-bold">C# Data Visualization (website)</a> for updated code examples and information about using this library.
  </div>
</div>

## Badge.cs

The `Badge` class contains all the logic needed to render and save a badge as a PNG file. 

This code demonstrates a few advanced topics which are worth considering:
* image scaling
* state management (save/restore)
* clipping
* rounded rectangles
* string measurement

```cs
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

public class Badge
{
    readonly string Name;
    readonly string Value;
    readonly SizeF NameSize;
    readonly SizeF ValueSize;

    // Customize these to change the style of the button
    public Color BackgroundLeft = Color.FromArgb("#666");
    public Color BackgroundRight = Color.FromArgb("#08C");
    public Color BackgroundLiner = Colors.White.WithAlpha(.15f);
    public Color FontColor = Colors.White;
    public Color FontShadow = Colors.Black.WithAlpha(.5f);
    public Color OverlayTop = Colors.Black.WithAlpha(0);
    public Color OverlayBottom = Colors.Black.WithAlpha(.25f);

    public Badge(string name, string value)
    {
        Name = name;
        Value = value;
        NameSize = MeasureString(name);
        ValueSize = MeasureString(value);
    }

    public void SavePng(string pngFilePath, float scale = 1)
    {
        float totalWidth = NameSize.Width + ValueSize.Width;
        int imageWidth = (int)totalWidth + 22;
        RectangleF imageRect = new(0, 0, imageWidth, 20);

        int scaledWidth = (int)(imageRect.Width * scale);
        int scaledHeight = (int)(imageRect.Height * scale);
        BitmapExportContext bmp = SkiaGraphicsService.Instance.CreateBitmapExportContext(scaledWidth, scaledHeight);
        ICanvas canvas = bmp.Canvas;
        canvas.Scale(scale, scale);

        // left background
        canvas.FillColor = BackgroundLeft;
        canvas.FillRoundedRectangle(imageRect, 5);

        // right background
        float bg2x = 10 + NameSize.Width;
        canvas.SaveState();
        canvas.ClipRectangle(bg2x, 0, bmp.Width, bmp.Height);
        canvas.FillColor = BackgroundRight;
        canvas.FillRoundedRectangle(imageRect, 5);
        canvas.RestoreState();

        // vertical line
        canvas.StrokeColor = BackgroundLiner;
        canvas.DrawLine(bg2x, 0, bg2x, bmp.Height);

        // background overlay shadow
        var pt = new LinearGradientPaint() { StartColor = OverlayTop, EndColor = OverlayBottom };
        canvas.SetFillPaint(pt, new Point(0, 0), new Point(0, bmp.Height));
        canvas.FillRoundedRectangle(imageRect, 5);

        // draw text backgrounds
        canvas.FontSize = 12;
        float offsetY = 14;
        float offsetX1 = 5;
        float offsetX2 = 15;
        float shadowOffset = 1;

        // text shadow
        canvas.FontColor = FontShadow;
        canvas.DrawString(Name, offsetX1 + shadowOffset, offsetY + shadowOffset, HorizontalAlignment.Left);
        canvas.DrawString(Value, offsetX2 + NameSize.Width + shadowOffset, offsetY + shadowOffset, HorizontalAlignment.Left);

        // text foreground
        canvas.FontColor = FontColor;
        canvas.DrawString(Name, offsetX1, offsetY, HorizontalAlignment.Left);
        canvas.DrawString(Value, offsetX2 + NameSize.Width, offsetY, HorizontalAlignment.Left);

        // save the output
        bmp.WriteToFile(pngFilePath);
    }

    SizeF MeasureString(string text, string fontName = "Arial", float fontSize = 12)
    {
        var fontService = new SkiaFontService("", "");
        using SkiaSharp.SKTypeface typeFace = fontService.GetTypeface(fontName);
        using SkiaSharp.SKPaint paint = new() { Typeface = typeFace, TextSize = fontSize };
        float width = paint.MeasureText(text);
        float height = fontSize;
        return new SizeF(width, height);
    }
}
```

## Program.cs

This simple program is all it takes to render and save a badge.

```cs
Badge myBadge = new("Maui", "Graphics");
myBadge.SavePng("demo.png");
```

<div class="text-center">

![](images/demo1.png)

</div>

### Customization

You can reach into the `Badge` class and customize styles as desired.

```cs
Badge myBadge = new("Maui", "Graphics")
{
    BackgroundRight = Microsoft.Maui.Graphics.Color.FromArgb("#3cc51d"),
};
myBadge.SavePng("demo1b.png");
```

<div class="text-center">

![](images/demo1b.png)

</div>

### Image Scaling

`Microsoft.Maui.Graphics` natively supports image scaling. This allows you to create large badges without any loss in quality that would come from creating a small badge and resizing the bitmap.

```cs
Badge myBadge = new("Maui", "Graphics");
myBadge.SavePng("demo1.png", scale: 1);
myBadge.SavePng("demo2.png", scale: 2);
myBadge.SavePng("demo5.png", scale: 5);
```

<div class="text-center">

![](images/demo1.png)
![](images/demo2.png)
![](images/demo5.png)

</div>

## Resources

* Download this application: [**BadgeApp.zip**](BadgeApp.zip)

* [How to `MeasureString()` with Maui.Graphics](https://swharden.com/blog/2021-10-16-maui-graphics-measurestring/)

* [Maui.Graphics on GitHub](https://github.com/dotnet/Microsoft.Maui.Graphics)

* [Maui.Graphics Issue #103 - SVG support](https://github.com/dotnet/Microsoft.Maui.Graphics/issues/103)

* [Maui.Graphics on NuGet](https://www.nuget.org/packages?q=Maui.Graphics)

* [Maui.Graphics WPF Quickstart](https://maui.graphics/quickstart/wpf/)

* [Maui.Graphics WinForms Quickstart](https://maui.graphics/quickstart/winforms/)
October 16th, 2021

How to MeasureString() with Maui.Graphics

Starting with .NET 6 Microsoft is sunsetting cross-platform support for System.Drawing.Common. Microsoft.Maui.Graphics is emerging as an excellent replacement and it can be used in any app (not just MAUI apps).

Code here demonstrates how I measure the pixel size of a string using Maui.Graphics in a console application.

/* Updated on 2022-02-19 */
using Microsoft.Maui.Graphics; // 6.0.200-preview.13.935
using Microsoft.Maui.Graphics.Skia; // 6.0.200-preview.13.935

// setup a canvas with a blue background
using BitmapExportContext bmp = new SkiaBitmapExportContext(450, 150, 1.0f);
ICanvas canvas = bmp.Canvas;
canvas.FillColor = Colors.Navy;
canvas.FillRectangle(0, 0, bmp.Width, bmp.Height);

// define and measure a string
PointF stringLocation = new(50, 50);
string stringText = "Hello, Maui.Graphics!";
Font font = new();
float fontSize = 32;
SizeF stringSize = canvas.GetStringSize(stringText, font, fontSize);
Rectangle stringRect = new(stringLocation, stringSize);

// draw the string and its outline
canvas.StrokeColor = Colors.White;
canvas.DrawRectangle(stringRect);
canvas.FontColor = Colors.Yellow;
canvas.Font = font;
canvas.FontSize = fontSize - 1; // NOTE: reduce to prevent clipping
canvas.DrawString(
    value: stringText,
    x: stringLocation.X,
    y: stringLocation.Y,
    width: stringSize.Width,
    height: stringSize.Height,
    horizontalAlignment: HorizontalAlignment.Left,
    verticalAlignment: VerticalAlignment.Top,
    textFlow: TextFlow.OverflowBounds,
    lineSpacingAdjustment: 0);

// save the result
string filePath = Path.GetFullPath("Issue279.png");
using FileStream fs = new(filePath, FileMode.Create);
bmp.WriteToStream(fs);
Console.WriteLine(filePath);

Note that in this example GetStringSize() respects font but DrawString() does not.

Resources

Markdown source code last modified on May 26th, 2022
---
Title: MeasureString() with Maui.Graphics
Description: How to measure the pixel dimensions of a string using Microsoft.Maui.Graphics
Date: 2021-10-16 4:15PM EST
tags: csharp, maui, graphics
---

# How to MeasureString() with Maui.Graphics

Starting with .NET 6 Microsoft is [sunsetting cross-platform support](https://github.com/dotnet/designs/blob/main/accepted/2021/system-drawing-win-only/system-drawing-win-only.md) for [`System.Drawing.Common`](https://www.nuget.org/packages/System.Drawing.Common/). [`Microsoft.Maui.Graphics`](https://github.com/dotnet/Microsoft.Maui.Graphics) is emerging as an excellent replacement and it can be used in any app (not just MAUI apps).

Code here demonstrates how I measure the pixel size of a string using `Maui.Graphics` in a console application.


<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
  <symbol id="check-circle-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
  </symbol>
  <symbol id="info-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/>
  </symbol>
  <symbol id="exclamation-triangle-fill" fill="currentColor" viewBox="0 0 16 16">
    <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
  </symbol>
</svg>

<div class="alert alert-primary d-flex align-items-center" role="alert">
  <svg class="bi flex-shrink-0 me-2" width="24" height="24" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>
  <div>
    <strong>UPDATE:</strong> This article was written when <code>Microsoft.Maui.Graphics</code> was still in preview. See <a href="https://swharden.com/blog/2022-05-25-maui-graphics/" class="fw-bold">Drawing with Maui Graphics (blog post)</a> and <a href="https://swharden.com/csdv/" class="fw-bold">C# Data Visualization (website)</a> for updated code examples and information about using this library.
  </div>
</div>

```cs
/* Updated on 2022-02-19 */
using Microsoft.Maui.Graphics; // 6.0.200-preview.13.935
using Microsoft.Maui.Graphics.Skia; // 6.0.200-preview.13.935

// setup a canvas with a blue background
using BitmapExportContext bmp = new SkiaBitmapExportContext(450, 150, 1.0f);
ICanvas canvas = bmp.Canvas;
canvas.FillColor = Colors.Navy;
canvas.FillRectangle(0, 0, bmp.Width, bmp.Height);

// define and measure a string
PointF stringLocation = new(50, 50);
string stringText = "Hello, Maui.Graphics!";
Font font = new();
float fontSize = 32;
SizeF stringSize = canvas.GetStringSize(stringText, font, fontSize);
Rectangle stringRect = new(stringLocation, stringSize);

// draw the string and its outline
canvas.StrokeColor = Colors.White;
canvas.DrawRectangle(stringRect);
canvas.FontColor = Colors.Yellow;
canvas.Font = font;
canvas.FontSize = fontSize - 1; // NOTE: reduce to prevent clipping
canvas.DrawString(
    value: stringText,
    x: stringLocation.X,
    y: stringLocation.Y,
    width: stringSize.Width,
    height: stringSize.Height,
    horizontalAlignment: HorizontalAlignment.Left,
    verticalAlignment: VerticalAlignment.Top,
    textFlow: TextFlow.OverflowBounds,
    lineSpacingAdjustment: 0);

// save the result
string filePath = Path.GetFullPath("Issue279.png");
using FileStream fs = new(filePath, FileMode.Create);
bmp.WriteToStream(fs);
Console.WriteLine(filePath);
```

<div class="text-center">

![](2022-02-19-maui-graphics-measure-string.png)

</div>

Note that in this example `GetStringSize()` respects font but `DrawString()` does not.

## Resources

* [Maui.Graphics on GitHub](https://github.com/dotnet/Microsoft.Maui.Graphics)

* [Maui.Graphics on NuGet](https://www.nuget.org/packages?q=Maui.Graphics)

* [https://Maui.Graphics](https://Maui.Graphics)

* [dotnet/Microsoft.Maui.Graphics issue #279](https://github.com/dotnet/Microsoft.Maui.Graphics/issues/279)
Pages