The personal website of Scott W Harden
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 October 17th, 2021
---
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
---

# 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>

## 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.

/// <summary>
/// Return the pixel size for the given text/font/size combination
/// </summary>
SizeF MeasureString(string text, string fontName, float fontSize)
{
    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);
}

NOTE: 💡 This method creates a font service (needed for console applications) but if you are developing a graphical application there may already be an existing global font service instance you can reference.

The following code uses MeasureString() to generate the image above:

using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

// start with a blue canvas
BitmapExportContext bmp = SkiaGraphicsService.Instance.CreateBitmapExportContext(400, 100);
ICanvas canvas = bmp.Canvas;
canvas.FillColor = Colors.Navy;
canvas.FillRectangle(0, 0, bmp.Width, bmp.Height);

// measure the text to determine its bounding rectangle
string text = "Hello, MAUI!";
float padding = 25;
string fontName = "Arial"; // WARNING: see note below
float fontSize = 36;
SizeF textSize = MeasureString(text, fontName, fontSize);
RectangleF textRect = new(padding, padding, textSize.Width, textSize.Height);

// fill a rectangle behind the text
canvas.FillColor = Colors.Maroon;
canvas.FillRectangle(textRect);

// draw the text inside a bounding box
canvas.FontSize = fontSize;
canvas.FontName = fontName;
canvas.FontColor = Colors.White;
canvas.DrawString(text, padding, padding + textRect.Height, HorizontalAlignment.Left);

// draw a rectangle above the text
canvas.StrokeColor = Colors.Yellow;
canvas.DrawRectangle(textRect);

// save the result
bmp.WriteToFile("demo.png");

WARNING: ⚠️ At the time of writing (October 16, 2021) the Maui.Graphics.Skia package is still in preview and DrawString() is not fully supported - it only draws strings using the default system font. The MeasureString() code above will measure strings of any font.

Resources

Markdown source code last modified on October 16th, 2021
---
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
---

# 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.

```cs
/// <summary>
/// Return the pixel size for the given text/font/size combination
/// </summary>
SizeF MeasureString(string text, string fontName, float fontSize)
{
    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);
}
```

> **NOTE:** 💡 This method creates a font service (needed for console applications) but if you are developing a graphical application there may already be an existing global font service instance you can reference.

<div class="text-center">

![](demo.png)

</div>

The following code uses `MeasureString()` to generate the image above:

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

// start with a blue canvas
BitmapExportContext bmp = SkiaGraphicsService.Instance.CreateBitmapExportContext(400, 100);
ICanvas canvas = bmp.Canvas;
canvas.FillColor = Colors.Navy;
canvas.FillRectangle(0, 0, bmp.Width, bmp.Height);

// measure the text to determine its bounding rectangle
string text = "Hello, MAUI!";
float padding = 25;
string fontName = "Arial"; // WARNING: see note below
float fontSize = 36;
SizeF textSize = MeasureString(text, fontName, fontSize);
RectangleF textRect = new(padding, padding, textSize.Width, textSize.Height);

// fill a rectangle behind the text
canvas.FillColor = Colors.Maroon;
canvas.FillRectangle(textRect);

// draw the text inside a bounding box
canvas.FontSize = fontSize;
canvas.FontName = fontName;
canvas.FontColor = Colors.White;
canvas.DrawString(text, padding, padding + textRect.Height, HorizontalAlignment.Left);

// draw a rectangle above the text
canvas.StrokeColor = Colors.Yellow;
canvas.DrawRectangle(textRect);

// save the result
bmp.WriteToFile("demo.png");
```

> **WARNING:** ⚠️ At the time of writing (October 16, 2021) the [`Maui.Graphics.Skia`](https://www.nuget.org/packages/Microsoft.Maui.Graphics.Skia/) package is still in preview and `DrawString()` is not fully supported - it only draws strings using the default system font. The `MeasureString()` code above will measure strings of any font.

## Resources

* Download this project: [**ConsoleApp30.zip**](ConsoleApp30.zip)

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

* [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 12th, 2021

My Life with a Spiny Leaf Insect

I spent 2016 new years eve celebrating the occasion with a few entomologists. Shortly after watching the ball drop in New York we changed the TV back to David Attenborough's Life of Insects, and I was amazed at what I saw! There was a short segment on leaf insects and I was impressed by the camouflage of these small creatures. I wanted to see more so I google image-searched and clicked on the websites that came up. One of the websites was an ebay listing for ten leaf insect eggs (for about $10 USD) and I couldn't avoid that "BUY IT NOW" button. I remember pointing to the TV and announcing to the room, "Aren't those things cool? I just bought ten."

WARNING: This listing may have been illegal. Leaf insects are invasive in some parts of the world, and transferring non-native species between countries can be dangerous to the local ecosystem and is often prohibited by law.

It turns out what I purchased was a spiny leaf insect (Extatosoma tiaratum) endemic to Australia. These insects are sexually dimorphic, with males being thinner and capable of flight. Females have false wings but grow larger and can live longer. These insects eat plants and are capable of asexual reproduction (parthenogenesis).

Hatching

Two weeks later I received an envelope in my mailbox. I opened it and it had a milk cap in it with 10 little egs inside, wrapped in clear tape. I was surprised at the packaging, but unpacked the eggs according to the directions. I put them in a ventilated container on a moist paper towel.

Four months later one of the eggs hatched! I almost gave up on the eggs because nothing happened for so long. I noticed it within several hours of emerging, but one of its legs was dried to the egg shell.

I transferred the insect to an enclosure and gave it access to dry and wet spaces. After a day the egg was still attached to the leg so I gave it a gentle pull and it fell off (the egg, not the leg).

Feeding

I added some oak branches to the insect's enclosure and this turned out to be a great choice. Over the next few months the only native plants this leaf insect would eat were oak leaves. I tried every type of plant I found find around my North Florida apartment, but this thing only had a taste for oak. Luckily that plant is everywhere around here, so I had no trouble supplying food throughout its life.

Growing

The leaf insect didn't grow gradually, but instead drastically changed its shape and size abruptly after molting every few weeks. I never observed it molting, so I assume it typically molted at night. After its first molt its legs were shaped much more like leaves, its back was wider and thicker, and its head and tail demonstrated little spines. These photos are of the insect 2-3 months after hatching.

Enclosure

After a few weeks the leaf insect began to outgrow her enclosure so repurposed a 20-gallon aquarium by adding mulch at the bottom (coconut fiber substrate from the pet store) and filling it with oak branches.

The insect loved her new home, but her camouflage was pretty good so it often took a couple minutes of searching to find her in there...

I misted the tank with a spray bottle once a day. Often I'd see the insect drinking the water that beaded on the leaves. Misting would often trigger feeding shortly after.

Adulthood

After 3 months the leaf insect grew considerably! The spikes along her body were more prominent than ever, and facilicated excellent camoflague when walking along branches.

Final Form

These photos were taken after the insect molted for the last time. She's huge! Her tail has a little hook near the end which I later learned was used to hold onto eggs as they're deposited.

Egg Laying

I was watching TV one night and I kept hearing these "pinging" sounds. Eventually I figured-out it was coming from the leaf insect, who was laying eggs and flinging them across the tank. They'd hit the glass, bounce off, and land in the mulch. The camouflage of the eggs was spectacular, and aside from spotting an egg here and there I had no idea how many she was actually laying. The leaf insect was 6 months old by now.

Last Photos

The leaf insect lived a little over seven months and these are the last photos I have of her. From what I read females can live for about a year and a half, so suspect mine was not well as she got older. She started to move sluggishly, and one day she was on the bottom of the tank (uncharacteristic behavior previously) and upon closer inspection I realized she wasn't moving. She had a good life, and I enjoyed sharing my home with this amazing insect for the last several months!

Egg Collection

After the insect died I removed all the branches sifted the substrate to isolate and save the eggs. After removing the branches I was surprised by how much frass the insect left behind! I dumped the substrate on wax paper and sorted it piece by piece. I collected 166 eggs! I read that leaf insects may lay over a thousand eggs over the lifespan. They are designed to resemble seeds and they have a little tasty nub on them that encourages ants to cary them to their nest.

Leaf insects are parthenogenic and are capable of laying fertile eggs without a male (although their offspring will all be female). I tried to incubate these eggs on a moist paper towel like the original eggs I got. I maintained these eggs for about a year, but none ever hatched. Later I read that spiny leaf insects may remain dormant for several years before hatching.

Final Thoughts

Raising this leaf insect was a fantastic experience! I may try it again some day. After this one finished her life cycle I turned its tank into an aquarium used for rearing baby freshwater angelfish. Maybe some day in the future I'll try to raise leaf insects again!

Resources

Markdown source code last modified on October 14th, 2021
---
Title: My Life with a Spiny Leaf Insect
Description: Photographs of the life cycle of a spiny leaf insect raised as a pet
Date: 2021-10-12 8:45 PM EST
---

# My Life with a Spiny Leaf Insect

**I spent 2016 new years eve celebrating the occasion with a few entomologists.** Shortly after watching the ball drop in New York we changed the TV back to David Attenborough's [Life of Insects](https://www.youtube.com/watch?v=Cs1Xs3Eheag), and I was amazed at what I saw! There was a short segment on _leaf insects_ and I was impressed by the camouflage of these small creatures. I wanted to see more so I google image-searched and clicked on the websites that came up. One of the websites was an ebay listing for ten leaf insect eggs (for about $10 USD) and I couldn't avoid that "BUY IT NOW" button. I remember pointing to the TV and announcing to the room, "Aren't those things cool? I just bought ten."

<div class="text-center img-border">
<img src="ebay.png" />
</div>

> **WARNING:** This listing may have been illegal. Leaf insects are invasive in some parts of the world, and transferring non-native species between countries can be dangerous to the local ecosystem and is often prohibited by law.

It turns out what I purchased was a [spiny leaf insect (Extatosoma tiaratum)](https://en.wikipedia.org/wiki/Extatosoma_tiaratum) endemic to Australia. These insects are sexually dimorphic, with males being thinner and capable of flight. Females have false wings but grow larger and can live longer. These insects eat plants and are capable of asexual reproduction (parthenogenesis).

<div class="text-center">
<img src='leaf-insect-male-female3.jpg' />
</div>

<div class="text-center">

![](https://youtu.be/Cs1Xs3Eheag)

</div>

## Hatching

**Two weeks later I received an envelope in my mailbox.** I opened it and it had a milk cap in it with 10 little egs inside, wrapped in clear tape. I was surprised at the packaging, but unpacked the eggs according to the directions. I put them in a ventilated container on a moist paper towel.

<div class="text-center img-border">
<a href='images-med/2016-04-14_18.33.18.jpg'><img src='images-med/2016-04-14_18.33.18.jpg' /></a>
</div>

<div class="text-center img-small img-border">
<a href='images-med/2016-04-14_18.35.19.jpg'><img src='images-sml/2016-04-14_18.35.19.jpg' /></a>
<a href='images-med/2016-04-14_18.32.54.jpg'><img src='images-sml/2016-04-14_18.32.54.jpg' /></a>
</div>

**Four months later one of the eggs hatched!** I almost gave up on the eggs because nothing happened for so long. I noticed it within several hours of emerging, but one of its legs was dried to the egg shell.

<div class="text-center img-border">
<a href='images-med/2016-04-11_20.26.39.jpg'><img src='images-med/2016-04-11_20.26.39.jpg' /></a>
</div>

**I transferred the insect to an enclosure and gave it access to dry and wet spaces.** After a day the egg was still attached to the leg so I gave it a gentle pull and it fell off (the egg, not the leg).

<div class="text-center img-small img-border">
<a href='images-med/2016-04-11_21.45.19.jpg'><img src='images-sml/2016-04-11_21.45.19.jpg' /></a>
<a href='images-med/2016-04-11_23.40.01.jpg'><img src='images-sml/2016-04-11_23.40.01.jpg' /></a>
</div>

## Feeding

I added some oak branches to the insect's enclosure and this turned out to be a great choice. Over the next few months ***the only native plants this leaf insect would eat were oak leaves***. I tried every type of plant I found find around my North Florida apartment, but this thing only had a taste for oak. Luckily that plant is everywhere around here, so I had no trouble supplying food throughout its life.

<div class="text-center img-border">
<a href='images-med/2016-04-14_18.26.30.jpg'><img src='images-med/2016-04-14_18.26.30.jpg' /></a>
</div>

## Growing

**The leaf insect didn't grow gradually, but instead drastically changed its shape and size abruptly after molting every few weeks.** I never observed it molting, so I assume it typically molted at night. After its first molt its legs were shaped much more like leaves, its back was wider and thicker, and its head and tail demonstrated little spines. These photos are of the insect 2-3 months after hatching.

<div class="text-center img-border">
<a href='images-med/2016-05-19_20.18.22.jpg'><img src='images-med/2016-05-19_20.18.22.jpg' /></a>
<a href='images-med/2016-05-19_20.20.59.jpg'><img src='images-med/2016-05-19_20.20.59.jpg' /></a>
</div>

<div class="text-center img-micro img-border">
<a href='images-med/2016-05-07_15.44.14.jpg'><img src='images-sml/2016-05-07_15.44.14.jpg' /></a>
<a href='images-med/2016-05-07_15.45.31.jpg'><img src='images-sml/2016-05-07_15.45.31.jpg' /></a>
<a href='images-med/2016-05-07_15.46.01.jpg'><img src='images-sml/2016-05-07_15.46.01.jpg' /></a>
<a href='images-med/2016-05-19_20.20.07.jpg'><img src='images-sml/2016-05-19_20.20.07.jpg' /></a>
<a href='images-med/2016-06-08_20.50.45.jpg'><img src='images-sml/2016-06-08_20.50.45.jpg' /></a>
<a href='images-med/2016-06-19_01.00.53.jpg'><img src='images-sml/2016-06-19_01.00.53.jpg' /></a>
<a href='images-med/2016-06-19_01.01.26.jpg'><img src='images-sml/2016-06-19_01.01.26.jpg' /></a>
<a href='images-med/2016-07-03_13.23.58.jpg'><img src='images-sml/2016-07-03_13.23.58.jpg' /></a>
<a href='images-med/2016-07-03_13.25.34.jpg'><img src='images-sml/2016-07-03_13.25.34.jpg' /></a>
</div>

## Enclosure

After a few weeks the leaf insect began to outgrow her enclosure so repurposed a 20-gallon aquarium by adding mulch at the bottom (coconut fiber substrate from the pet store) and filling it with oak branches. 

<div class="text-center img-border">
<a href='images-med/2016-06-04_20.08.25.jpg'><img src='images-med/2016-06-04_20.08.25.jpg' /></a>
</div>

The insect loved her new home, but her camouflage was pretty good so it often took a couple minutes of searching to find her in there...

<div class="text-center img-border">
<a href='images-med/2016-06-04_20.07.20.jpg'><img src='images-med/2016-06-04_20.07.20.jpg' /></a>
</div>

<div class="text-center img-border img-small">
<a href='images-med/2016-06-04_00.16.12.jpg'><img src='images-sml/2016-06-04_00.16.12.jpg' /></a>
<a href='images-med/2016-06-07_23.29.18.jpg'><img src='images-sml/2016-06-07_23.29.18.jpg' /></a>
</div>

**I misted the tank with a spray bottle once a day.** Often I'd see the insect drinking the water that beaded on the leaves. Misting would often trigger feeding shortly after.

<div class="text-center img-border">
<a href='images-med/2016-06-25_11.56.40.jpg'><img src='images-med/2016-06-25_11.56.40.jpg' /></a>
<a href='images-med/2016-06-22_00.20.08.jpg'><img src='images-med/2016-06-22_00.20.08.jpg' /></a>
<a href='images-med/2016-06-24_00.41.24.jpg'><img src='images-med/2016-06-24_00.41.24.jpg' /></a>
</div>

<div class="text-center img-border img-micro">
<a href='images-med/2016-06-08_21.57.20.jpg'><img src='images-sml/2016-06-08_21.57.20.jpg' /></a>
<a href='images-med/2016-06-21_22.17.34.jpg'><img src='images-sml/2016-06-21_22.17.34.jpg' /></a>
<a href='images-med/2016-06-24_00.41.46.jpg'><img src='images-sml/2016-06-24_00.41.46.jpg' /></a>
<a href='images-med/2016-06-24_00.44.13.jpg'><img src='images-sml/2016-06-24_00.44.13.jpg' /></a>
<a href='images-med/2016-06-28_22.55.52.jpg'><img src='images-sml/2016-06-28_22.55.52.jpg' /></a>
<a href='images-med/2016-07-15_07.37.02.jpg'><img src='images-sml/2016-07-15_07.37.02.jpg' /></a>
</div>

## Adulthood

**After 3 months the leaf insect grew considerably!** The spikes along her body were more prominent than ever, and facilicated excellent camoflague when walking along branches.

<div class="text-center img-border">
<a href='images-med/2016-07-15_07.39.53.jpg'><img src='images-med/2016-07-15_07.39.53.jpg' /></a>
</div>

<div class="text-center img-micro img-border">
<a href='images-med/2016-07-15_07.38.45.jpg'><img src='images-sml/2016-07-15_07.38.45.jpg' /></a>
<a href='images-med/2016-07-15_07.39.16.jpg'><img src='images-sml/2016-07-15_07.39.16.jpg' /></a>
<a href='images-med/2016-07-15_07.39.24.jpg'><img src='images-sml/2016-07-15_07.39.24.jpg' /></a>
<a href='images-med/2016-07-15_07.39.44.jpg'><img src='images-sml/2016-07-15_07.39.44.jpg' /></a>
<a href='images-med/2016-07-15_07.40.04.jpg'><img src='images-sml/2016-07-15_07.40.04.jpg' /></a>
</div>

## Final Form

**These photos were taken after the insect molted for the last time.** She's huge! Her tail has a little hook near the end which I later learned was used to hold onto eggs as they're deposited.

<div class="text-center img-border">
<a href='images-med/2016-08-22_22.30.10.jpg'><img src='images-med/2016-08-22_22.30.10.jpg' /></a>
<a href='images-med/2016-09-20_22.07.38.jpg'><img src='images-med/2016-09-20_22.07.38.jpg' /></a>
<a href='images-med/2016-08-23_18.04.04.jpg'><img src='images-med/2016-08-23_18.04.04.jpg' /></a>
</div>

<div class="text-center img-micro img-border">
<a href='images-med/2016-08-03_22.55.21.jpg'><img src='images-sml/2016-08-03_22.55.21.jpg' /></a>
<a href='images-med/2016-08-04_23.12.14.jpg'><img src='images-sml/2016-08-04_23.12.14.jpg' /></a>
<a href='images-med/2016-08-04_23.13.26.jpg'><img src='images-sml/2016-08-04_23.13.26.jpg' /></a>
<a href='images-med/2016-08-22_22.33.18.jpg'><img src='images-sml/2016-08-22_22.33.18.jpg' /></a>
<a href='images-med/2016-08-23_07.16.48.jpg'><img src='images-sml/2016-08-23_07.16.48.jpg' /></a>
<a href='images-med/2016-08-23_07.16.54.jpg'><img src='images-sml/2016-08-23_07.16.54.jpg' /></a>
</div>

## Egg Laying

**I was watching TV one night and I kept hearing these "pinging" sounds.** Eventually I figured-out it was coming from the leaf insect, who was laying eggs and flinging them across the tank. They'd hit the glass, bounce off, and land in the mulch. The camouflage of the eggs was spectacular, and aside from spotting an egg here and there I had no idea how many she was actually laying. The leaf insect was 6 months old by now.

<div class="text-center img-border">
<a href='images-med/2016-10-09_23.04.05.jpg'><img src='images-med/2016-10-09_23.04.05.jpg' /></a>
<a href='images-med/2016-10-26_17.21.38.jpg'><img src='images-med/2016-10-26_17.21.38.jpg' /></a>
<a href='images-med/2016-10-09_23.05.48.jpg'><img src='images-med/2016-10-09_23.05.48.jpg' /></a>
</div>

## Last Photos
The leaf insect lived a little over seven months and these are the last photos I have of her. From what I read females can live for about a year and a half, so suspect mine was not well as she got older. She started to move sluggishly, and one day she was on the bottom of the tank (uncharacteristic behavior previously) and upon closer inspection I realized she wasn't moving. She had a good life, and I enjoyed sharing my home with this amazing insect for the last several months!

<div class="text-center img-border">
<a href='images-med/2016-12-30_15.11.10.jpg'><img src='images-med/2016-12-30_15.11.10.jpg' /></a>
<a href='images-med/2016-12-30_15.10.57.jpg'><img src='images-med/2016-12-30_15.10.57.jpg' /></a>
</div>

<div class="text-center img-micro img-border">
<a href='images-med/2016-11-30_23.08.25.jpg'><img src='images-med/2016-11-30_23.08.25.jpg' /></a>
<a href='images-med/2016-11-30_23.07.56.jpg'><img src='images-sml/2016-11-30_23.07.56.jpg' /></a>
<a href='images-med/2016-11-30_23.08.03.jpg'><img src='images-sml/2016-11-30_23.08.03.jpg' /></a>
<a href='images-med/2016-11-30_23.09.45.jpg'><img src='images-sml/2016-11-30_23.09.45.jpg' /></a>
<a href='images-med/2016-11-30_23.10.19.jpg'><img src='images-sml/2016-11-30_23.10.19.jpg' /></a>
<a href='images-med/2016-12-30_15.11.00.jpg'><img src='images-sml/2016-12-30_15.11.00.jpg' /></a>
</div>

## Egg Collection
**After the insect died I removed all the branches sifted the substrate to isolate and save the eggs.** After removing the branches I was surprised by how much frass the insect left behind! I dumped the substrate on wax paper and sorted it piece by piece. I collected 166 eggs! I read that leaf insects may lay over a thousand eggs over the lifespan. They are designed to resemble seeds and they have a little tasty nub on them that encourages ants to cary them to their nest.

<div class="text-center img-micro img-border">
<a href='images-med/2017-01-01_12.08.08.jpg'><img src='images-sml/2017-01-01_12.08.08.jpg' /></a>
<a href='images-med/2017-01-01_12.08.27.jpg'><img src='images-sml/2017-01-01_12.08.27.jpg' /></a>
<a href='images-med/2017-01-01_12.09.13.jpg'><img src='images-sml/2017-01-01_12.09.13.jpg' /></a>
<a href='images-med/2017-01-01_17.17.23.jpg'><img src='images-sml/2017-01-01_17.17.23.jpg' /></a>
<a href='images-med/2017-01-01_17.20.31.jpg'><img src='images-sml/2017-01-01_17.20.31.jpg' /></a>
<a href='images-med/2017-01-01_19.17.04.jpg'><img src='images-sml/2017-01-01_19.17.04.jpg' /></a>
</div>

**Leaf insects are parthenogenic** and are capable of laying fertile eggs without a male (although their offspring will all be female). I tried to incubate these eggs on a moist paper towel like the original eggs I got. I maintained these eggs for about a year, but none ever hatched. Later I read that spiny leaf insects may remain dormant for several years before hatching. 

## Final Thoughts

**Raising this leaf insect was a fantastic experience!** I may try it again some day. After this one finished her life cycle I turned its tank into an aquarium used for rearing baby freshwater angelfish. Maybe some day in the future I'll try to raise leaf insects again!

## Resources

* [The Short End of the Stick - Cloning and Costly Sex in the Spiny Leaf Insect](http://www.bonduriansky.net/Burke_2017_The_Short_End_of_The_Stick.pdf)

* [Keeping Insects - Parthenogenesis](https://www.keepinginsects.com/stick-insect/parthenogenesis/)

* [Stick/Leaf Insects - Care](https://www.pkpets.com.au/images/pdf-care-list/Info%20Sheet%2011%20-%20Stick%20Leaf%20Insects.pdf)

* [Life of Insects (Video)](https://www.youtube.com/watch?v=uppwVyUd5S0)
October 12th, 2021

Rename Image Files by Capture Date with C#

I have hundreds of folders containing thousands of photographs I wish to move to a single directory. Images are all JPEG format, but unfortunately they have filenames like DSC_0123.JPG. This means there are many identical filenames (e.g., every folder has a DSC_0001.JPG) and also the sorted list of filenames would not be in chronological order.

JPEG files have an Exif header and most cameras store the acquisition date of photographs as metadata. I wrote a C# application to scan a folder of images and rename them all by the date and time in their header. This problem has been solved many ways (googling reveals many solutions), but I thought I'd see what it looks like to solve this problem with C#. I reached for the MetadataExtractor package on NuGet. My solution isn't fancy but it gets the job done.

foreach (string sourceImage in System.IO.Directory.GetFiles("../sourceFolder", "*.jpg"))
{
    DateTime dt = GetJpegDate(sourceImage);
    string fileName = $"{dt:yyyy-MM-dd HH.mm.ss}.jpg";
    string filePath = Path.Combine("../outputFolder", fileName);
    System.IO.File.Copy(sourceImage, filePath, true);
    Console.WriteLine($"{sourceImage} -> {filePath}");
}

DateTime GetJpegDate(string filePath)
{
    var directories = MetadataExtractor.ImageMetadataReader.ReadMetadata(filePath);

    foreach (var directory in directories)
    {
        foreach (var tag in directory.Tags)
        {
            if (tag.Name == "Date/Time Original")
            {
                if (string.IsNullOrEmpty(tag.Description))
                    continue;
                string d = tag.Description.Split(" ")[0].Replace(":", "-");
                string t = tag.Description.Split(" ")[1];
                return DateTime.Parse($"{d} {t}");
            }
        }
    }

    throw new InvalidOperationException($"Date not found in {filePath}");
}
> dotnet run
DSC_0121.JPG -> 2016-09-30 21.51.25.jpg
DSC_0122.JPG -> 2016-10-03 21.42.05.jpg
DSC_0123.JPG -> 2016-10-09 23.04.05.jpg
DSC_0124.JPG -> 2016-10-09 23.05.48.jpg
DSC_0125.JPG -> 2016-11-30 23.07.56.jpg
DSC_0126.JPG -> 2017-01-01 19.16.56.jpg
DSC_0127.JPG -> 2017-01-01 19.17.09.jpg

Resources

  • MetadataExtractor
  • ExifTool seems to be a common solution
    • exiftool -d '%Y%m%d-%H%M%%-03.c.%%e' '-filename<CreateDate' .
  • JHead is another common solution
    • jhead -n%Y%m%d-%H%M%S *.jpg
Markdown source code last modified on October 16th, 2021
---
title: Rename Image Files by Capture Date with C#
description: How to rename a folder of JPEGs by the creation date stored in their metadata
date: 2021-10-12 8:28 PM EST
tags: csharp
---

# Rename Image Files by Capture Date with C# 

**I have hundreds of folders containing thousands of photographs I wish to move to a single directory.** Images are all JPEG format, but unfortunately they have filenames like `DSC_0123.JPG`. This means there are many identical filenames (e.g., every folder has a `DSC_0001.JPG`) and also the sorted list of filenames would not be in chronological order.

**JPEG files have an [Exif header](https://en.wikipedia.org/wiki/Exif) and most cameras store the acquisition date of photographs as metadata.** I wrote a C# application to scan a folder of images and rename them all by the date and time in their header. This problem has been solved many ways (googling reveals many solutions), but I thought I'd see what it looks like to solve this problem with C#. I reached for the [MetadataExtractor package](https://www.nuget.org/packages/MetadataExtractor/) on NuGet. My solution isn't fancy but it gets the job done.

```cs
foreach (string sourceImage in System.IO.Directory.GetFiles("../sourceFolder", "*.jpg"))
{
    DateTime dt = GetJpegDate(sourceImage);
    string fileName = $"{dt:yyyy-MM-dd HH.mm.ss}.jpg";
    string filePath = Path.Combine("../outputFolder", fileName);
    System.IO.File.Copy(sourceImage, filePath, true);
    Console.WriteLine($"{sourceImage} -> {filePath}");
}

DateTime GetJpegDate(string filePath)
{
    var directories = MetadataExtractor.ImageMetadataReader.ReadMetadata(filePath);

    foreach (var directory in directories)
    {
        foreach (var tag in directory.Tags)
        {
            if (tag.Name == "Date/Time Original")
            {
                if (string.IsNullOrEmpty(tag.Description))
                    continue;
                string d = tag.Description.Split(" ")[0].Replace(":", "-");
                string t = tag.Description.Split(" ")[1];
                return DateTime.Parse($"{d} {t}");
            }
        }
    }

    throw new InvalidOperationException($"Date not found in {filePath}");
}
```

```text
> dotnet run
DSC_0121.JPG -> 2016-09-30 21.51.25.jpg
DSC_0122.JPG -> 2016-10-03 21.42.05.jpg
DSC_0123.JPG -> 2016-10-09 23.04.05.jpg
DSC_0124.JPG -> 2016-10-09 23.05.48.jpg
DSC_0125.JPG -> 2016-11-30 23.07.56.jpg
DSC_0126.JPG -> 2017-01-01 19.16.56.jpg
DSC_0127.JPG -> 2017-01-01 19.17.09.jpg
```

## Resources
* [MetadataExtractor](https://www.nuget.org/packages/MetadataExtractor/)
* [ExifTool](https://exiftool.org/) seems to be a common solution
  * `exiftool -d '%Y%m%d-%H%M%%-03.c.%%e' '-filename<CreateDate' .`
* [JHead](https://www.sentex.ca/~mwandel/jhead/) is another common solution
  * `jhead -n%Y%m%d-%H%M%S *.jpg`
Pages