Through a bit of fiddling with PSWordCloud I've found some
rather neat features of System.Drawing
that are a little on the weird side, and also not the most
discoverable, so I figured I should talk a little about them.
Disclaimers
As I have myself only recently learned, what I've been able to do here, while available in
System.Drawing.Common
, do not work especially well in Linux or Mac.
Tyler has been road testing them for me just a little bit here
and there, and it appears that in both Linux and Mac they have a hard dependency on an external Mono
library called libgdiplus
that does not usually come packaged with PowerShell or .NET Core by
default.
Even after this library is installed:
- On Linux, it appears that attempting to instantiate the
System.Drawing.Region
class completely crashes PowerShell. - On Mac, it seems to work but I cannot guarantee its accuracy; it seems to have trouble with rendering what I asked of it quite properly.
Some Fun Possibilities
So what I've been toying with for PSWordCloud has been the ability to construct complex structures in the abstract, and then my process has basically been to "store" them by adding them to a Region object, and on each iteration I check that any new additions don't interfere or overlap with pre-existing items.
This is sort of what it looks like:
using namespace System.Drawing
Add-Type -AssemblyName System.Drawing # Specify System.Drawing.Common on PS Core
# Init base image and graphics drawing objects
[Image] $Image = [Bitmap]::new(1024, 1024) # Width, Height
[Graphics] $DrawingSurface = [Graphics]::FromImage($Image)
# Initialize an empty region
$FilledSpace = [Region]::new()
$FilledSpace.MakeEmpty()
# Pens for outlines, brushes for fills
$Brush [SolidBrush]::new([Color]::Black)
$PenWidth = 4.5
$Pen = [Pen]::new([Brush] $Brush, [double] $PenWidth)
foreach ($Word in $WordList) {
[PointF]::new([double] $x, [double] $y)
$WordPath = [Drawing2d.GraphicsPath]::new()
$WordPath.AddString(
[string] $Word, # The text to render as a path
[FontFamily] $FontFamily, # [System.Drawing.FontFamily] object, mostly has the font name
[int] $FontStyle, # Bold, italic, whatever, [System.Drawing.FontStyle] enum value as an int
[double] $FontSize, # Em-size of the font
[PointF] $DrawLocation, # Location of the path in the graphics space
[StringFormat] $Format # [System.Drawing.StringFormat] object with format flags etc.
)
$DrawingSurface.DrawPath([Pen] $Pen, $WordPath) # Outline text
$DrawingSurface.FillPath([Brush] $Brush, $WordPath) # Fill text
# Add path to region
$FilledSpace.Union($WordPath)
}
There's a lot going on that I'm going to sort of skip over here, because we can be here for hours before it's all fully explained. Thankfully, the Microsoft docs on System.Drawing are fairly complete, with some useful examples. Explore all you like, and experiment — that's the fun way to learn, in my opinion!
Checking Collisions and Overlaps
Now, you'll notice one interesting thing — a little bit missing in the above example. That's the
sort of "collision checking" or intersection checking that I'm doing lots of in PSWordCloud.
To do that, we have a fairly simple process we can follow, with one small caveat.
When we're drawing our first item, we need to ignore the collision checking, because the libraries
do some funny things when trying to check collisions against an empty Region
.
With that mentioned, we can check for collisions based on the Region
object using the
$Region.IsVisible()
method.
This method has a bunch of overloads,
for working with a variety of other graphical objects, both with and without reference to an
existing graphics surface object.
Using this and just retrying a bunch of times as we scan across the image space, we can check any
given area for empty space.
You will, however, notice the somewhat tricky lack of an exact overload for checking the visibility
of a GraphicsPath
.
This stymied me for a moment until I realised that the GraphicsPath
object has a
$Path.GetBounds()
method, which returns a [RectangleF]
object.
Using this, we can check if a given path's bounds intersect with (are visible when drawn over) the
existing Region object.
$WordBounds = $WordPath.GetBounds()
# Returns $true if the rectangle is within the region .IsVisible() is called on
$WordIsColliding = $FilledSpace.IsVisible($WordBounds)
With that, we have the framework for figuring out whether or not objects collide, and there
are a whole lot of different objects you can work with in System.Drawing
— lines, curves,
ellipses, polygons, paths, etc. And with the GraphicsPath
objects, you can create literally
anything you want or need using its methods.
Some of this stuff is really handy for creating images, and it's also so completely outside the normal bent for PowerShell folks that working with it is utterly alien for myself, and I'm sure for many others out there.
Go wild, have fun, and see what crazy things you can come up with!
Thanks for reading!