Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
April 4, 2025

Creating Image Markup Tools Part 2

Let's continue the toolmaking process from the last few articles. I want to create a tool that will let me insert text into an image. Ultimately, I want to update the image I am inserting into a tag on an MP3 file for one of my compositions created with MuseScore Studio. I hope you've noticed that the code I've share thus far has nothing to do with my goal. I've been writing PowerShell code to insert text into any image. I don't want to restrict the function I plan on writing. Later, I will show you how I can leverage the function to meet my MuseScore Studio needs.

We ended last time with a set of PowerShell commands that insert text into an image.

$text = "Behind the PowerShell Pipeline"
$image = "C:\work\blue-robot-ps.jpg"
$imageObject = [System.Drawing.Image]::FromFile($image)
$rect = [System.Drawing.RectangleF]::new(0, 0, $imageObject.Width, $imageObject.Height)
$graphics = [System.Drawing.Graphics]::FromImage($imageObject)
$size = "36"
$font = "Cascadia Code"
[System.Drawing.FontStyle]$FontStyle = 'Italic'
$fontObject = New-Object System.Drawing.Font($Font, $Size, $FontStyle)
[System.Drawing.Color]$FontColor = 'DarkSlateGray'
$brush = New-Object System.Drawing.SolidBrush($FontColor)
$stringFormat = New-Object System.Drawing.stringFormat
$stringFormat.Alignment = "Center"
$stringFormat.LineAlignment = "Center"
$stringFormat.FormatFlags = [System.Drawing.stringFormatFlags]::NoClip
$graphics.DrawString($text, $fontObject, $brush, $rect, $stringFormat)
$output = "c:\temp\preview.jpg"
$imageObject.Save($output)

I can't stress enough the importance of validating your code. If your commands won't work in an interactive PowerShell console session, creating a script or function around them will be more difficult.

Creating a Function

Creating a function lets me take the code I have and make it reusable.

Naming

The first decision is what to call it. One easy approach is to complete the sentence, "This function will...". Ideally, the action will be a standard verb. However, sometimes you need to be creative. When I complete the sentence, I get "This function will insert text into an image." But Insert is not a valid verb. Running Get-Verb I would say Add is the best choice. I can use the noun component to describe what I am adding. Thus, a name like Add-TextToImage seems reasonable. I can also create an alias of "Insert-TextToImage which satisfies the way I'm thinking about the function. And because I don't like to type any more than I have to, I can also define a short alias of ati.

Function Add-TextToImage {
    [CmdletBinding(SupportsShouldProcess)]
    [Alias("Insert-TextToImage","ati")]

Because my function will be changing something, it needs to support -WhatIf. That's why I added the SupportsShouldProcess attribute.

Parameters

Next, I want to parameterize my code. This should be self-evident. Obviously, I want to allow the user to specify the image and the text. Looking at my console code, any variable where I am assigning value, other than an object, is a good candidate for a parameter.

These would make good parameters:

$size = "36"
$font = "Cascadia Code"
[System.Drawing.FontStyle]$FontStyle = 'Italic'
$output = "c:\temp\preview.jpg"
[System.Drawing.Color]$FontColor = 'DarkSlateGray'

I should have the flexibility to specify these settings, but not be forced to. I can set default values which will make the function easier to run. But if I need to make a change to a setting like the font, I can do that too.

The variable name in my console code can become the parameter name.

Param(
[string]$Path,
[string]$Text,
[string]$Font = 'Arial',
[int]$Size = 72,
[System.Drawing.Color]$FontColor = 'CornSilk',
[System.Drawing.StringAlignment]$VerticalAlign = 'Center',
[System.Drawing.StringAlignment]$HorizontalAlign = 'Center',
[System.Drawing.FontStyle]$FontStyle = 'Regular',
[string]$PreviewPath,

You'll notice that I am casting some of the parameters to a specific type like System.Drawing.StringAlignment. I could have made this a String and passed the value later in my code. However. would need to validate any value the user entered. Using Get-TypeMember, I can check if the class is an enum or has a set of valid values.

PS C:\> Get-TypeMember System.Drawing.StringAlignment

   Type: System.Drawing.StringAlignment

Name     MemberType ResultType                     IsStatic IsEnum
----     ---------- ----------                     -------- ------
Center   Field      System.Drawing.StringAlignment     True
Far      Field      System.Drawing.StringAlignment     True
Near     Field      System.Drawing.StringAlignment     True
GetType  Method     Type
HasFlag  Method     Boolean
ToString Method     String

By using the type instead of the more generic String, PowerShell will automatically validate the value and provide tab completion! I don't have to do anything else. I see this is also true for the font style and color.

PS C:\> Get-TypeMember system.drawing.fontstyle

   Type: System.Drawing.FontStyle

Name      MemberType ResultType               IsStatic IsEnum
----      ---------- ----------               -------- ------
Bold      Field      System.Drawing.FontStyle     True
Italic    Field      System.Drawing.FontStyle     True
Regular   Field      System.Drawing.FontStyle     True
Strikeout Field      System.Drawing.FontStyle     True
Underline Field      System.Drawing.FontStyle     True
GetType   Method     Type
HasFlag   Method     Boolean
ToString  Method     String

The other way you can test if you can use a type as a parameter is to try to cast a string to that type. If it works, you can use it as a parameter type.

PS C:\> "violet" -as [system.drawing.color]

R             : 238
G             : 130
B             : 238
A             : 255
IsKnownColor  : True
IsEmpty       : False
IsNamedColor  : True
IsSystemColor : False
Name          : Violet

PS C:\> "italic" -as [system.drawing.fontstyle]
Italic

Although this isn't foolproof. You can also type the class name followed by :: and press Ctrl+Space to see if PSReadline can provide tab completion.

Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.