The personal website of Scott W Harden
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 January 23rd, 2022
---
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)


![](https://youtu.be/P5Y-bCfKUrU)

</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`
October 9th, 2021

Managing Secrets in .NET Console Apps

Sometimes your code needs to work with secrets that you don't want to risk accidentally leaking on the internet. There are many strategies for solving this problem, and here I share my preferred approach. I see a lot of articles about how to manage user secrets in ASP.NET and other web applications, but not many focusing on console or desktop applications.

User Secrets in C# Applications

  • The dotnet user-secrets command manages secrets

  • Secrets are stored as plain-text key/value pairs in JSON format in %AppData%\Microsoft\UserSecrets

  • This isn't totally secure, but may be an improvement over .env and .json files stored inside your project folder which can accidentally get committed to source control if your .gitignore file isn't meticulously managed

1. Create a New .NET App

dotnet new console

2. Set the Secrets ID

  • right-click the project and manage user secrets

OR

  • dotnet user-secrets init

OR

  • Edit the .csproj file to add a unique UserSecretsId
  • Generate a unique ID with uuidtools.com
<PropertyGroup>
  <UserSecretsId>ee35bcf4-291d-11ec-9dc7-7f3593499a27</UserSecretsId>
</PropertyGroup>

3. Add Secrets Locally

dotnet user-secrets set username me@example.com
dotnet user-secrets set password mySecretPass123

4. Install the UserSecrets Package

dotnet add package Microsoft.Extensions.Configuration.UserSecrets

💡 CAREFUL: If you accidentally install Microsoft.Extensions.Configuration instead of Microsoft.Extensions.Configuration.UserSecrets you won't have access to AddUserSecrets()

5. Access Secrets in Code

using Microsoft.Extensions.Configuration;
var config = new ConfigurationBuilder().AddUserSecrets<Program>().Build();
string username = config["username"];
string password = config["password"];

NOTE: If a key does not exist its value will be null

Environment Variables

Cloud platforms (GitHub Actions, Azure, etc.) often use environment variables to manage secrets. Using local user secrets to populate environment variables is a useful way to locally develop applications that will run in the cloud.

This example shows how to populate environment variables from user secrets:

using Microsoft.Extensions.Configuration;

class Program
{
    static void Main()
    {
        SetEnvironmentVariablesFromUserSecrets();
        string username = Environment.GetEnvironmentVariable("username");
        string password = Environment.GetEnvironmentVariable("password");
    }

    static void SetEnvironmentVariablesFromUserSecrets()
    {
        var config = new ConfigurationBuilder().AddUserSecrets<Program>().Build();
        foreach (var child in config.GetChildren())
        {
            Environment.SetEnvironmentVariable(child.Key, child.Value);
        }
    }
}

GitHub Actions

GitHub Actions makes it easy to load repository secrets into environment variables. See GitHub / Encrypted secrets for more information about how to add secrets to your repository.

This example snippet of a GitHub action loads two GitHub repository secrets (USERNAME and PASSWORD) as environment variables (username and password) that can be read by my unit tests using Environment.GetEnvironmentVariable() as shown above.

    steps:
      ...
      - name: 🧪 Run Tests
        env:
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
        run: dotnet test ./src

Conclusions

Using these strategies I am able to write code that seamlessly accesses secrets locally on my dev machine and from environment variables when running in the cloud. Since this strategy does not store secrets inside my project folder, the chance of accidentally committing a .env or other secrets file to source control approaches zero.

Resources

Markdown source code last modified on January 30th, 2022
---
Title: Managing Secrets in .NET Console Apps
Description: How to use dotnet user-secrets to store and retrieve secrets and set environment variables in .NET applications.
Date: 2021-10-09 1:15PM EST
tags: csharp
---

# Managing Secrets in .NET Console Apps

**Sometimes your code needs to work with secrets that you don't want to risk accidentally leaking on the internet.** There are many strategies for solving this problem, and here I share my preferred approach. I see a lot of articles about how to manage user secrets in ASP.NET and other web applications, but not many focusing on console or desktop applications.

## User Secrets in C# Applications

* The `dotnet user-secrets` command manages secrets

* Secrets are stored as plain-text key/value pairs in JSON format in `%AppData%\Microsoft\UserSecrets`

* This isn't totally secure, but may be an improvement over `.env` and `.json` files stored inside your project folder which can accidentally get committed to source control if your `.gitignore` file isn't meticulously managed


### 1. Create a New .NET App

```text
dotnet new console
```

### 2. Set the Secrets ID

* right-click the project and `manage user secrets`

OR

* `dotnet user-secrets init`

OR

* Edit the `.csproj` file to add a unique `UserSecretsId`
* Generate a unique ID with [uuidtools.com](https://www.uuidtools.com/)

```xml
<PropertyGroup>
  <UserSecretsId>ee35bcf4-291d-11ec-9dc7-7f3593499a27</UserSecretsId>
</PropertyGroup>
```

### 3. Add Secrets Locally

```text
dotnet user-secrets set username me@example.com
dotnet user-secrets set password mySecretPass123
```

### 4. Install the UserSecrets Package

```text
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
```

> **💡 CAREFUL:** If you accidentally install [`Microsoft.Extensions.Configuration`](https://www.nuget.org/packages/Microsoft.Extensions.Configuration) instead of [`Microsoft.Extensions.Configuration.UserSecrets`](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.UserSecrets) you won't have access to `AddUserSecrets()`

### 5. Access Secrets in Code

```cs
using Microsoft.Extensions.Configuration;
```

```cs
var config = new ConfigurationBuilder().AddUserSecrets<Program>().Build();
string username = config["username"];
string password = config["password"];
```

_NOTE: If a key does not exist its value will be `null`_

## Environment Variables

**Cloud platforms (GitHub Actions, Azure, etc.) often use environment variables to manage secrets.** Using local user secrets to populate environment variables is a useful way to locally develop applications that will run in the cloud.

This example shows how to populate environment variables from user secrets:

```cs
using Microsoft.Extensions.Configuration;

class Program
{
    static void Main()
    {
        SetEnvironmentVariablesFromUserSecrets();
        string username = Environment.GetEnvironmentVariable("username");
        string password = Environment.GetEnvironmentVariable("password");
    }

    static void SetEnvironmentVariablesFromUserSecrets()
    {
        var config = new ConfigurationBuilder().AddUserSecrets<Program>().Build();
        foreach (var child in config.GetChildren())
        {
            Environment.SetEnvironmentVariable(child.Key, child.Value);
        }
    }
}
```

## GitHub Actions

**GitHub Actions makes it easy to load repository secrets into environment variables.** See [GitHub / Encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) for more information about how to add secrets to your repository.

This example snippet of a GitHub action loads two GitHub repository secrets (`USERNAME` and `PASSWORD`) as environment variables (`username` and `password`) that can be read by my unit tests using `Environment.GetEnvironmentVariable()` as shown above.

```yaml
    steps:
      ...
      - name: 🧪 Run Tests
        env:
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
        run: dotnet test ./src
```

## Conclusions

**Using these strategies I am able to write code that seamlessly accesses secrets locally on my dev machine and from environment variables when running in the cloud.** Since this strategy does not store secrets inside my project folder, the chance of accidentally committing a `.env` or other secrets file to source control approaches zero.

## Resources

* NuGet [`Microsoft.Extensions.Configuration.UserSecrets`](https://www.nuget.org/packages/Microsoft.Extensions.Configuration.UserSecrets) 

* [Using .env in .NET](https://dusted.codes/dotenv-in-dotnet) by [Dustin Moris Gorski](https://github.com/dustinmoris)

* Microsoft Documentation for [`Environment.GetEnvironmentVariable()`](https://docs.microsoft.com/en-us/dotnet/api/system.environment.getenvironmentvariable)

* [GitHub / Encrypted secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)
September 10th, 2021

Drawing with Maui.Graphics

.NET MAUI (Multi-Platform Application User Interface) is a new framework for creating cross-platform apps using C#. MAUI will be released as part of .NET 6 in November 2021 and it is expected to come with Maui.Graphics, a cross-platform drawing library superior to System.Drawing in many ways. Although System.Drawing.Common currently supports rendering in Linux and MacOS, cross-platform support for System.Drawing will sunset over the next few releases and begin throwing a PlatformNotSupportedException in .NET 6.

By creating a graphics model using only Maui.Graphics dependencies, users can share drawing code across multiple rendering technologies (GDI, Skia, SharpDX, etc.), operating systems (Windows, Linux, MacOS, etc.), and application frameworks (WinForms, WPF, Maui, WinUI, etc.). This page demonstrates how to create a platform-agnostic graphics model and render it using Windows Forms and WPF. Resources in the Maui.Graphics namespace can be used by any modern .NET application (not just Maui apps).

⚠️ WARNING: Maui.Graphics is still a pre-release experimental library (as noted on their GitHub page). Although the code examples on this page work presently, the API may change between now and the official release.

1. Create a new Project

For this example I will start by creating a a .NET 5.0 WinForms project from scratch. Later we will extend the solution to include a WPF project that uses the same graphics model.

2. Add References to Maui.Graphics

We need to get the windows forms MAUI control and all its dependencies. At the time of writing (September, 2021) these packages are not yet available on NuGet, but they can be downloaded from the Microsoft.Maui.Graphics GitHub page.

💡 Tip: If you're developing a desktop application you can improve the "rebuild all" time by editing the csproj files of your dependencies so TargetFrameworks only includes .NET Standard targets.

  • Add the Microsoft.Maui.Graphics project to your solution and add a reference to it from your project.

  • Windows Forms: Add the Microsoft.Maui.Graphics.GDI.Winforms project to your solution and add a reference to it in your WinForms project. A GDIGraphicsView control should appear in the toolbox.

  • WPF: Add the Microsoft.Maui.Graphics.Skia.WPF project to your solution and add a reference to it in your WPF project. A WDSkiaGraphicsView control should appear in the toolbox.

3. Create a Drawable Object

A drawable is a class that implements IDrawable, has a Draw() method, and can be rendered anywhere Maui.Graphics is supported. By only depending on Maui.Graphics it's easy to create a graphics model that can be used on any operating system using any supported graphical framework.

using Microsoft.Maui.Graphics;

This drawable fills the image blue and renders 1,000 randomly-placed anti-aliased semi-transparent white lines on it. Note that the location of the lines depends on the size of the render field (passed-in as an argument).

public class RandomLines : IDrawable
{
    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        canvas.FillColor = Color.FromArgb("#003366");
        canvas.FillRectangle(dirtyRect);

        canvas.StrokeSize = 1;
        canvas.StrokeColor = Color.FromRgba(255, 255, 255, 100);
        Random Rand = new();
        for (int i = 0; i < 1000; i++)
        {
            canvas.DrawLine(
                x1: (float)Rand.NextDouble() * dirtyRect.Width,
                y1: (float)Rand.NextDouble() * dirtyRect.Height,
                x2: (float)Rand.NextDouble() * dirtyRect.Width,
                y2: (float)Rand.NextDouble() * dirtyRect.Height);
        }
    }
}

4. Add a GraphicsView Control

Drag/drop a GraphicsView control from the Toolbox onto your application and assign your graphics model to its Drawable field.

For animations you can use a timer to invalidate the control (forcing a redraw) automatically every 20 ms.

Windows Forms (Rendering with GDI)

Windows Forms applications may also want to intercept SizeChanged events to force redraws as the window is resized.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        gdiGraphicsView1.Drawable = new RandomLines();
    }

    private void GdiGraphicsView1_SizeChanged(object? sender, EventArgs e) =>
        gdiGraphicsView1.Invalidate();

    private void timer1_Tick(object sender, EventArgs e) =>
        gdiGraphicsView1.Invalidate();
}

I found performance to be quite adequate. On my system 1,000 lines rendered on an 800x600 window at ~60 fps. Like System.Drawing this system slows down as a function of image size, so full-screen 1920x1080 animation was much slower (~10 fps).

WPF (Rendering with SkiaSharp)

<Skia:WDSkiaGraphicsView Name="MyGraphicsView" />
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        MyGraphicsView.Drawable = new RandomLines();

        DispatcherTimer timer = new();
        timer.Interval = TimeSpan.FromMilliseconds(20);
        timer.Tick += Timer_Tick; ;
        timer.Start();
    }

    private void Timer_Tick(object sender, EventArgs e) =>
        MyGraphicsView.Invalidate();
}

Extend the Graphics Model

Since our project is configured to display the same graphics model with both WinForms and WPF, it's easy to edit the model in one place and it's updated everywhere. We can replace the random lines model with one that manages randomly colored and sized balls that bounce off the edges of the window as the model advances.

BallField.cs

public class BallField : IDrawable
{
    private readonly Ball[] Balls;

    public BallField(int ballCount)
    {
        Balls = new Ball[ballCount];
    }

    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        canvas.FillColor = Colors.Navy;
        canvas.FillRectangle(dirtyRect);

        foreach (Ball ball in Balls)
        {
            ball?.Draw(canvas);
        }
    }

    public void Randomize(double width, double height)
    {
        Random rand = new();
        for (int i = 0; i < Balls.Length; i++)
        {
            Balls[i] = new()
            {
                X = rand.NextDouble() * width,
                Y = rand.NextDouble() * height,
                Radius = rand.NextDouble() * 5 + 5,
                XVel = rand.NextDouble() - .5,
                YVel = rand.NextDouble() - .5,
                R = (byte)rand.Next(50, 255),
                G = (byte)rand.Next(50, 255),
                B = (byte)rand.Next(50, 255),
            };
        }
    }

    public void Advance(double timeDelta, double width, double height)
    {
        foreach (Ball ball in Balls)
        {
            ball?.Advance(timeDelta, width, height);
        }
    }
}

Ball.cs

public class Ball
{
    public double X;
    public double Y;
    public double Radius = 5;
    public double XVel;
    public double YVel;
    public byte R, G, B;

    public void Draw(ICanvas canvas)
    {
        canvas.FillColor = Color.FromRgb(R, G, B);
        canvas.FillCircle((float)X, (float)Y, (float)Radius);
    }

    public void Advance(double timeDelta, double width, double height)
    {
        MoveForward(timeDelta);
        Bounce(width, height);
    }

    private void MoveForward(double timeDelta)
    {
        X += XVel * timeDelta;
        Y += YVel * timeDelta;
    }

    private void Bounce(double width, double height)
    {
        double minX = Radius;
        double minY = Radius;
        double maxX = width - Radius;
        double maxY = height - Radius;

        if (X < minX)
        {
            X = minX + (minX - X);
            XVel = -XVel;
        }
        else if (X > maxX)
        {
            X = maxX - (X - maxX);
            XVel = -XVel;
        }

        if (Y < minY)
        {
            Y = minY + (minY - Y);
            YVel = -YVel;
        }
        else if (Y > maxY)
        {
            Y = maxY - (Y - maxY);
            YVel = -YVel;
        }
    }
}

Download This Project

To build this project from source code you currently have to download Maui.Graphics source from GitHub and edit the solution file to point to the correct directory containing these projects. This will get a lot easier after Microsoft puts their WinForms and WPF controls on NuGet.

Coding Challenge

Can you recreate this classic screensaver using Maui.Graphics? Bonus points if the user can customize the number of shapes, the number of corners each shape has, and the number of lines drawn in each shape's history. It's a fun problem and I encourage you to give it a go! Here's how I did it: mystify-maui.zip

Resources

Markdown source code last modified on May 26th, 2022
---
Title: Drawing with Maui.Graphics
Description: How to use Maui.Graphics to draw and create animations in a Windows Forms and WPF applications
Date: 2021-09-10 10:30PM EST
Tags: csharp, maui, graphics
---

# Drawing with Maui.Graphics

**[.NET MAUI](https://docs.microsoft.com/en-us/dotnet/maui/what-is-maui) (Multi-Platform Application User Interface) is a new framework for creating cross-platform apps using C#.** MAUI will be released as part of .NET 6 in November 2021 and it is expected to come with [`Maui.Graphics`](https://github.com/dotnet/Microsoft.Maui.Graphics), a cross-platform drawing library superior to [`System.Drawing`](https://docs.microsoft.com/en-us/dotnet/api/system.drawing?view=net-5.0#remarks) in many ways. Although [`System.Drawing.Common`](https://www.nuget.org/packages/System.Drawing.Common) currently supports rendering in Linux and MacOS, [cross-platform support for System.Drawing will sunset](https://github.com/dotnet/designs/blob/main/accepted/2021/system-drawing-win-only/system-drawing-win-only.md) over the next few releases and begin throwing a `PlatformNotSupportedException` in .NET 6.

By creating a graphics model using only `Maui.Graphics` dependencies, users can share drawing code across multiple rendering technologies (GDI, Skia, SharpDX, etc.), operating systems (Windows, Linux, MacOS, etc.), and application frameworks (WinForms, WPF, Maui, WinUI, etc.). **This page demonstrates how to create a platform-agnostic graphics model and render it using Windows Forms and WPF.** Resources in the `Maui.Graphics` namespace can be used by any modern .NET application (not just Maui apps).

<div class="text-center">

![](maui-graphics-balls.gif)

</div>

> ⚠️ **WARNING:** `Maui.Graphics` is still a pre-release experimental library (as noted on [their GitHub page](https://github.com/dotnet/Microsoft.Maui.Graphics)). Although the code examples on this page work presently, the API may change between now and the official release.


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

## 1. Create a new Project

For this example I will start by creating a a .NET 5.0 WinForms project from scratch. Later we will extend the solution to include a WPF project that uses the same graphics model.

## 2. Add References to Maui.Graphics

We need to get the windows forms MAUI control and all its dependencies. At the time of writing (September, 2021) these packages are not yet available on NuGet, but they can be downloaded from the [Microsoft.Maui.Graphics GitHub page](https://github.com/dotnet/Microsoft.Maui.Graphics).

> 💡 **Tip:** If you're developing a desktop application you can improve the "rebuild all" time by editing the csproj files of your dependencies so `TargetFrameworks` only includes .NET Standard targets.

* Add the `Microsoft.Maui.Graphics` project to your solution and add a reference to it from your project.

* **Windows Forms:** Add the `Microsoft.Maui.Graphics.GDI.Winforms` project to your solution and add a reference to it in your WinForms project. A `GDIGraphicsView` control should appear in the toolbox.

* **WPF:** Add the `Microsoft.Maui.Graphics.Skia.WPF` project to your solution and add a reference to it in your WPF project. A `WDSkiaGraphicsView` control should appear in the toolbox.

<div class="text-center img-border">

![](vs-maui.png)

</div>

## 3. Create a Drawable Object

**A _drawable_ is a class that implements `IDrawable`, has a `Draw()` method, and can be rendered anywhere Maui.Graphics is supported.** By only depending on `Maui.Graphics` it's easy to create a graphics model that can be used on any operating system using any supported graphical framework.

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

This _drawable_ fills the image blue and renders 1,000 randomly-placed anti-aliased semi-transparent white lines on it. Note that the location of the lines depends on the size of the render field (passed-in as an argument).

```cs
public class RandomLines : IDrawable
{
    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        canvas.FillColor = Color.FromArgb("#003366");
        canvas.FillRectangle(dirtyRect);

        canvas.StrokeSize = 1;
        canvas.StrokeColor = Color.FromRgba(255, 255, 255, 100);
        Random Rand = new();
        for (int i = 0; i < 1000; i++)
        {
            canvas.DrawLine(
                x1: (float)Rand.NextDouble() * dirtyRect.Width,
                y1: (float)Rand.NextDouble() * dirtyRect.Height,
                x2: (float)Rand.NextDouble() * dirtyRect.Width,
                y2: (float)Rand.NextDouble() * dirtyRect.Height);
        }
    }
}
```

## 4. Add a GraphicsView Control

Drag/drop a GraphicsView control from the Toolbox onto your application and assign your graphics model to its `Drawable` field.

For animations you can use a timer to invalidate the control (forcing a redraw) automatically every 20 ms.

### Windows Forms (Rendering with GDI)

Windows Forms applications may also want to intercept SizeChanged events to force redraws as the window is resized.

```cs
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        gdiGraphicsView1.Drawable = new RandomLines();
    }

    private void GdiGraphicsView1_SizeChanged(object? sender, EventArgs e) =>
        gdiGraphicsView1.Invalidate();

    private void timer1_Tick(object sender, EventArgs e) =>
        gdiGraphicsView1.Invalidate();
}
```

<div class="text-center">

![](maui-graphics-winforms.gif)

</div>

**I found performance to be quite adequate.** On my system 1,000 lines rendered on an 800x600 window at ~60 fps. Like `System.Drawing` this system slows down as a function of image size, so full-screen 1920x1080 animation was much slower (~10 fps).

### WPF  (Rendering with SkiaSharp)

```xml
<Skia:WDSkiaGraphicsView Name="MyGraphicsView" />
```

```cs
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        MyGraphicsView.Drawable = new RandomLines();

        DispatcherTimer timer = new();
        timer.Interval = TimeSpan.FromMilliseconds(20);
        timer.Tick += Timer_Tick; ;
        timer.Start();
    }

    private void Timer_Tick(object sender, EventArgs e) =>
        MyGraphicsView.Invalidate();
}
```

<div class="text-center">

![](maui-graphics-wpf.gif)

</div>

## Extend the Graphics Model

Since our project is configured to display the same graphics model with both WinForms and WPF, it's easy to edit the model in one place and it's updated everywhere. We can replace the random lines model with one that manages randomly colored and sized balls that bounce off the edges of the window as the model advances.

<div class="text-center">

![](maui-graphics-balls.gif)

</div>

### BallField.cs

```cs
public class BallField : IDrawable
{
    private readonly Ball[] Balls;

    public BallField(int ballCount)
    {
        Balls = new Ball[ballCount];
    }

    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        canvas.FillColor = Colors.Navy;
        canvas.FillRectangle(dirtyRect);

        foreach (Ball ball in Balls)
        {
            ball?.Draw(canvas);
        }
    }

    public void Randomize(double width, double height)
    {
        Random rand = new();
        for (int i = 0; i < Balls.Length; i++)
        {
            Balls[i] = new()
            {
                X = rand.NextDouble() * width,
                Y = rand.NextDouble() * height,
                Radius = rand.NextDouble() * 5 + 5,
                XVel = rand.NextDouble() - .5,
                YVel = rand.NextDouble() - .5,
                R = (byte)rand.Next(50, 255),
                G = (byte)rand.Next(50, 255),
                B = (byte)rand.Next(50, 255),
            };
        }
    }

    public void Advance(double timeDelta, double width, double height)
    {
        foreach (Ball ball in Balls)
        {
            ball?.Advance(timeDelta, width, height);
        }
    }
}
```

### Ball.cs

```cs
public class Ball
{
    public double X;
    public double Y;
    public double Radius = 5;
    public double XVel;
    public double YVel;
    public byte R, G, B;

    public void Draw(ICanvas canvas)
    {
        canvas.FillColor = Color.FromRgb(R, G, B);
        canvas.FillCircle((float)X, (float)Y, (float)Radius);
    }

    public void Advance(double timeDelta, double width, double height)
    {
        MoveForward(timeDelta);
        Bounce(width, height);
    }

    private void MoveForward(double timeDelta)
    {
        X += XVel * timeDelta;
        Y += YVel * timeDelta;
    }

    private void Bounce(double width, double height)
    {
        double minX = Radius;
        double minY = Radius;
        double maxX = width - Radius;
        double maxY = height - Radius;

        if (X < minX)
        {
            X = minX + (minX - X);
            XVel = -XVel;
        }
        else if (X > maxX)
        {
            X = maxX - (X - maxX);
            XVel = -XVel;
        }

        if (Y < minY)
        {
            Y = minY + (minY - Y);
            YVel = -YVel;
        }
        else if (Y > maxY)
        {
            Y = maxY - (Y - maxY);
            YVel = -YVel;
        }
    }
}
```

## Download This Project 

* **Source Code:** [**Balls.zip**](2021-09-12-Balls.zip) (10 kB)

* **WinForms (GDI) Demo:** [**Balls-WinForms.exe**](2021-09-12-Balls.exe-WinForms.zip) (237 kB)

* **WPF (Skia) Demo:** [**Balls-WPF.exe**](2021-09-12-Balls.exe-WPF.zip) (13 MB)

> To build this project from source code you currently have to download [Maui.Graphics source from GitHub](https://github.com/dotnet/Microsoft.Maui.Graphics) and edit the solution file to point to the correct directory containing these projects. This will get a lot easier after Microsoft puts their WinForms and WPF controls on NuGet.

## Coding Challenge

**Can you recreate this classic screensaver using Maui.Graphics?** Bonus points if the user can customize the number of shapes, the number of corners each shape has, and the number of lines drawn in each shape's history. It's a fun problem and I encourage you to give it a go! Here's how I did it: [mystify-maui.zip](2021-09-13-mystify-maui.zip)

![](maui-mystify.mp4)

## Resources

* [Draw with Maui.Graphics and Skia in a C# Console Application](https://swharden.com/blog/2021-08-01-maui-skia-console/)

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

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

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

* [C# Data Visualization](https://swharden.com/CsharpDataVis)

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

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

* [Maui.Graphics WinForms Quickstart](https://maui.graphics/quickstart/winforms)
August 1st, 2021

Draw with Maui.Graphics and Skia in a C# Console Application

Microsoft's System.Drawing.Common package is commonly used for cross-platform graphics in .NET Framework and .NET Core applications, but according to the dotnet roadmap System.Drawing will soon only support Windows. As Microsoft sunsets cross-platform support for System.Drawing they will be simultaneously developing Microsoft.Maui.Graphics, a cross-platform graphics library for iOS, Android, Windows, macOS, Tizen and Linux completely in C#.

The Maui.Graphics library can be used in any .NET application (not just MAUI applications). This page documents how I used the Maui.Drawing package to render graphics in memory (using a Skia back-end) and save them as static images from a console application.

I predict Maui.Graphics will eventually evolve to overtake System.Drawing in utilization. It has many advantages for performance and memory management (discussed extensively elsewhere on the internet), but it is still early in development. As of today (July 2021) the Maui.Graphics GitHub page warns "This is an experimental library ... There is no official support. Use at your own Risk."

Maui Graphics Skia Console Quickstart

This program will create an image, fill it with blue, add 1,000 random lines, then draw some text. It is written as a .NET 5 top-level console application and requires the Microsoft.Maui.Graphics and Microsoft.Maui.Graphics.Skia NuGet packages (both are currently in preview).

We use SkiaSharp to create a canvas, but importantly that canvas implements Microsoft.Maui.Graphics.ICanvas (it's not Skia-specific) so all the methods that draw on it can be agnostic to which rendering system was used. This makes it easy to write generic rendering methods now and have the option to switch the rendering system later.

Program.cs

using System;
using System.IO;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

// Use Skia to create a Maui graphics context and canvas
BitmapExportContext bmpContext = SkiaGraphicsService.Instance.CreateBitmapExportContext(600, 400);
SizeF bmpSize = new(bmpContext.Width, bmpContext.Height);
ICanvas canvas = bmpContext.Canvas;

// Draw on the canvas with abstract methods that are agnostic to the renderer
ClearBackground(canvas, bmpSize, Colors.Navy);
DrawRandomLines(canvas, bmpSize, 1000);
DrawBigTextWithShadow(canvas, "This is Maui.Graphics with Skia");
SaveFig(bmpContext, Path.GetFullPath("quickstart.jpg"));

static void ClearBackground(ICanvas canvas, SizeF bmpSize, Color bgColor)
{
    canvas.FillColor = Colors.Navy;
    canvas.FillRectangle(0, 0, bmpSize.Width, bmpSize.Height);
}

static void DrawRandomLines(ICanvas canvas, SizeF bmpSize, int count = 1000)
{
    Random rand = new();
    for (int i = 0; i < count; i++)
    {
        canvas.StrokeSize = (float)rand.NextDouble() * 10;

        canvas.StrokeColor = new Color(
            red: (float)rand.NextDouble(),
            green: (float)rand.NextDouble(),
            blue: (float)rand.NextDouble(),
            alpha: .2f);

        canvas.DrawLine(
            x1: (float)rand.NextDouble() * bmpSize.Width,
            y1: (float)rand.NextDouble() * bmpSize.Height,
            x2: (float)rand.NextDouble() * bmpSize.Width,
            y2: (float)rand.NextDouble() * bmpSize.Height);
    }
}

static void DrawBigTextWithShadow(ICanvas canvas, string text)
{
    canvas.FontSize = 36;
    canvas.FontColor = Colors.White;
    canvas.SetShadow(offset: new SizeF(2, 2), blur: 1, color: Colors.Black);
    canvas.DrawString(text, 20, 50, HorizontalAlignment.Left);
}

static void SaveFig(BitmapExportContext bmp, string filePath)
{
    bmp.WriteToFile(filePath);
    Console.WriteLine($"WROTE: {filePath}");
}

MauiGraphicsDemo.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Maui.Graphics" Version="6.0.100-preview.6.299" />
    <PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="6.0.100-preview.6.299" />
  </ItemGroup>

</Project>

Resources

Markdown source code last modified on May 26th, 2022
---
Title: Draw with Maui.Graphics and Skia in a C# Console Application
Description: This page describes how to draw graphics in a console application with Maui Graphics and Skia
Date: 2021-08-01 7:15PM EST
Tags: csharp, maui
---

# Draw with Maui.Graphics and Skia in a C# Console Application

**Microsoft's `System.Drawing.Common` package is commonly used for cross-platform graphics in .NET Framework and .NET Core applications, but according to the dotnet roadmap [System.Drawing will soon only support Windows](https://github.com/dotnet/designs/blob/main/accepted/2021/system-drawing-win-only/system-drawing-win-only.md).** As Microsoft sunsets cross-platform support for `System.Drawing` they will be simultaneously developing [`Microsoft.Maui.Graphics`](https://github.com/dotnet/Microsoft.Maui.Graphics), a cross-platform graphics library for iOS, Android, Windows, macOS, Tizen and Linux completely in C#.

**The `Maui.Graphics` library can be used in any .NET application (not just MAUI applications).** This page documents how I used the Maui.Drawing package to render graphics in memory (using a Skia back-end) and save them as static images from a console application.

**I predict `Maui.Graphics` will eventually evolve to overtake `System.Drawing` in utilization.** It has many advantages for performance and memory management (discussed extensively elsewhere on the internet), but it is still early in development. As of today (July 2021) [the Maui.Graphics GitHub page](https://github.com/dotnet/Microsoft.Maui.Graphics) warns "This is an experimental library ... There is no official support. Use at your own Risk."

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

## Maui Graphics Skia Console Quickstart

This program will create an image, fill it with blue, add 1,000 random lines, then draw some text. It is written as a .NET 5 top-level console application and requires the [Microsoft.Maui.Graphics](https://www.nuget.org/packages/Microsoft.Maui.Graphics) and [Microsoft.Maui.Graphics.Skia](https://www.nuget.org/packages/Microsoft.Maui.Graphics.Skia) NuGet packages (both are currently in preview). 

We use SkiaSharp to create a canvas, but importantly that canvas implements `Microsoft.Maui.Graphics.ICanvas` (it's not Skia-specific) so all the methods that draw on it can be agnostic to which rendering system was used. This makes it easy to write generic rendering methods now and have the option to switch the rendering system later.

<div class="text-center">

![](maui-graphics-quickstart.jpg)

</div>

### Program.cs
```cs
using System;
using System.IO;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Skia;

// Use Skia to create a Maui graphics context and canvas
BitmapExportContext bmpContext = SkiaGraphicsService.Instance.CreateBitmapExportContext(600, 400);
SizeF bmpSize = new(bmpContext.Width, bmpContext.Height);
ICanvas canvas = bmpContext.Canvas;

// Draw on the canvas with abstract methods that are agnostic to the renderer
ClearBackground(canvas, bmpSize, Colors.Navy);
DrawRandomLines(canvas, bmpSize, 1000);
DrawBigTextWithShadow(canvas, "This is Maui.Graphics with Skia");
SaveFig(bmpContext, Path.GetFullPath("quickstart.jpg"));

static void ClearBackground(ICanvas canvas, SizeF bmpSize, Color bgColor)
{
    canvas.FillColor = Colors.Navy;
    canvas.FillRectangle(0, 0, bmpSize.Width, bmpSize.Height);
}

static void DrawRandomLines(ICanvas canvas, SizeF bmpSize, int count = 1000)
{
    Random rand = new();
    for (int i = 0; i < count; i++)
    {
        canvas.StrokeSize = (float)rand.NextDouble() * 10;

        canvas.StrokeColor = new Color(
            red: (float)rand.NextDouble(),
            green: (float)rand.NextDouble(),
            blue: (float)rand.NextDouble(),
            alpha: .2f);

        canvas.DrawLine(
            x1: (float)rand.NextDouble() * bmpSize.Width,
            y1: (float)rand.NextDouble() * bmpSize.Height,
            x2: (float)rand.NextDouble() * bmpSize.Width,
            y2: (float)rand.NextDouble() * bmpSize.Height);
    }
}

static void DrawBigTextWithShadow(ICanvas canvas, string text)
{
    canvas.FontSize = 36;
    canvas.FontColor = Colors.White;
    canvas.SetShadow(offset: new SizeF(2, 2), blur: 1, color: Colors.Black);
    canvas.DrawString(text, 20, 50, HorizontalAlignment.Left);
}

static void SaveFig(BitmapExportContext bmp, string filePath)
{
    bmp.WriteToFile(filePath);
    Console.WriteLine($"WROTE: {filePath}");
}
```

### MauiGraphicsDemo.csproj
```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Maui.Graphics" Version="6.0.100-preview.6.299" />
    <PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="6.0.100-preview.6.299" />
  </ItemGroup>

</Project>
```

## Resources
* [Animated Rendering with SkiaSharp and OpenGL](https://swharden.com/CsharpDataVis/)
* [Microsoft.Maui.Graphics on GitHub](https://github.com/dotnet/Microsoft.Maui.Graphics)
* [Microsoft.Maui.Graphics on NuGet](https://www.nuget.org/packages/Microsoft.Maui.Graphics/)
* [SkiaSharp Graphics in Xamarin.Forms](https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/)
* [Maui.Graphics](https://maui.graphics)
Pages