This article is aimed at developers who are porting projects based on the System.Drawing.Drawing2D Namespace to the newer SkiaSharp graphic library. If your project makes use of Gidplus HatchBrushes and you need to re-create them with SkiaSharp, read on.
This sample project was written using Visual Studio 2022. The project is a WindowsForm app based on the .NET Core 6 framework and written with C# version 10. It's compatible with Windows 7 and above.
Introduction
I'm in the process of re-writing a Windows application that originally generated graphic output based on Gdiplus. The goal is to move to SkiaSharp but maintain the same features, for backward compatibility. This article details the approach I've taken to be able to recreate the same hatch effects that were based on Gdiplus.
What is a HatchBrush
If you are not familiar with the concept, in Gdiplus, a HatchBrush is a special type of brush that will enable you to fill an area with a particular pattern, called HatchStyle. Gdiplus has 52 built-in patterns that can be used to create a brush.
Here's a few examples:
Percent20 HatchStyle | DiagonalBrick HatchStyle | DiagonalCross HatchStyle |
| | |
What is a HatchStyle
Each one of the 52 built-in Gdiplus HatchStyle is a pattern based on an 8-by-8 matrix of alternating on/off pixels (in some cases, there are antialias pixels, more later). If you look closely at each style, you'll see the basic grid that makes up the pattern.
Here are a few enlarged examples:
Percent20 pattern | DiagonalBrick pattern | DiagonalCross pattern |
| | |
In order to be able to recreate them using SkiaSharp, the first step is to be able to gather the information needed for each one of the patterns.
Hacking HatchStyles
Rather than manually analyzing each HatchStyle one by one (read: lose eye-sight) and create my arrays (or Lists) of 8-by-8 points, I decided to let the machine do the work using the following approach:
- Create an 8-by-8 bitmap and fill it with the pattern.
- Read pixel values and build arrays of "on" points.
- Use that information to recreate the pattern in SkiaSharp.
Here's the code for the method that reads pattern pixels:
readonly static int _matrixSize = 8;
static List<SKPoint> GetHatchPattern(HatchStyle hatch, bool inverted)
{
List<SKPoint> ret = new();
var bitmap = new Bitmap(_matrixSize, _matrixSize);
using Graphics graphics = Graphics.FromImage(bitmap);
graphics.SmoothingMode = SmoothingMode.Default;
Color color1 = inverted ? Color.White : Color.Black;
Color color2 = inverted ? Color.Black : Color.White;
using HatchBrush brush = new(hatch, color1, color2);
Rectangle rect = new(0, 0, _matrixSize, _matrixSize);
graphics.FillRectangle(brush, rect);
int blackValue = hatch switch
{
HatchStyle.ForwardDiagonal or
HatchStyle.BackwardDiagonal or
HatchStyle.DiagonalCross => -15395563,
_ => Color.Black.ToArgb(),
};
for (int row = 0; row < _matrixSize; row++)
{
for (int col = 0; col < _matrixSize; col++)
{
Color pixel = bitmap.GetPixel(col, row);
if (pixel.ToArgb() == blackValue)
{
ret.Add(new(col, row));
}
}
}
return ret;
}
The GetHatchPattern
method takes a parameter of type HatchStyle
and generates a list of SKPoint
(s) that we can later use in SkiaSharp.
The method also takes the inverted
parameter so that the list of points can be inverted in terms of background/foreground color.
Note that for 3 of the 52 HatchStyle
s, Gdiplus creates a pattern with antialias pixels, regardless of the SmoothingMode
setting in the destination Graphic
object. We will disregard such information and just convert everything to simple on/off pixels, as the resulting effect doesn't seem to need any antialiasing in SkiaSharp.
Once the information is gathered, we can move to SkiaSharp and build the same effect.
Draw a Hatch Effect with SkiaSharp
SkiaSharp is based on Skia, an open source, cross-platform graphic library currently owned and maintained by Google. Have a look at this great tutorial for more information on how to fill paths with effects using SkiaSharp.
SkiaSharp uses an SKPaint object to define the way paths and lines are drawn onto a surface. To be able to create a hatch effect, we'll need set the PathEffect member of the SKPaint
object to an effect created via the Create2DPath static
method of the SKPathEffect class.
The most important bit of data here is that we need to pass an SKPath
object to this method that contains the pattern that we want to draw, so we'll create a path and add a rectangle (or rather a square) for each pixel we read from the source hatch bitmap created with Gdiplus.
Here's the code:
SKRect rect = canvas.DeviceClipBounds;
using SKPath fillPath = new();
fillPath.AddRect(rect);
SKPath patternPath = new();
foreach (var point in _currentPattern)
patternPath.AddRect(new(point.X, point.Y, point.X + 1, point.Y + 1));
rect.Inflate(-_padding, -_padding);
float halfMtx = _matrixSize / 2;
float modX = rect.Left % _matrixSize;
float modY = rect.Top % _matrixSize;
if (modX >= halfMtx) modX -= _matrixSize;
if (modY >= halfMtx) modY -= _matrixSize;
modX -= halfMtx;
modY -= halfMtx;
patternPath.Transform(SKMatrix.CreateTranslation(modX, modY));
var colors = new SKColor[] { SKColors.Black, SKColors.Blue };
using SKPaint paint = new()
{
Shader = SKShader.CreateRadialGradient(new SKPoint
(rect.MidX, rect.MidY), rect.Width / 2, colors, SKShaderTileMode.Clamp)
};
SKMatrix matrix = SKMatrix.CreateScale(_matrixSize, _matrixSize);
using SKPathEffect effect = SKPathEffect.Create2DPath(matrix, patternPath);
if (effect == null) return;
paint.PathEffect = effect;
canvas.Clear(SKColors.White);
canvas.Save();
canvas.ClipRect(rect);
canvas.DrawPath(fillPath, paint);
canvas.Restore();
In this snippet, the variable canvas
is the drawing surface obtained from the destination control (normally from the PaintSurface
event).
A few steps are required to have the effect drawn at a specific origin point and all the way to the end of the area. First, a translation is in order to ensure that the point of origin of the pattern matches that of the top/left corner of the area we are filling. Second, the target path contains a rectangle larger then the destination area, but clipped to it, to ensure that the pattern is drawn up to the lower/right corner of the area. This is needed since SkiaSharp will truncate any rows/columns that don't entirely fit the area.
Here's the side-by-side resulting comparison, generated by the sample program attached to this article:
Note that the SkiaSharp version of the hatch image was drawn using a radial gradient color, going from black to blue, something not achievable with Gdiplus, which only takes solid colors for foreground and background.
Optimizing the Arrays, or Maybe Not
In many cases, the array of "on" points might be simplified to include larger rectangles that will make up for several "on" points at once. Also, for patterns comprising of straight lines only, you could also try the Create2DLine PathEffect
, to simplify the pattern path.
Well, in my experimenting, none of these approaches gave good results. For reasons beyond my knowledge, once an "optimized" pattern is applied using either of these 2 techniques, bad results were produced, due to artifacts in the resulting rendering.
Even though in terms of resources and performance, it might not make sense to create an 8 pixel line with 8 squares one after the other, it turned out to be the most reliable approach.
Real World Usage
In a real world scenario, you will not want to recreate HatchStyles
on the fly as the attached example shows, but rather have a pre-compiled list of points somewhere in your project, since HatchStyles
are invariable patterns.
In the attached source code for the project, you will find a Helper
region at the bottom of Form1.cs with two methods that will return the proper array of SKPoint
(s) for each one of the 52 HatchStyle
, PathPointsNormal
and PathPointsReverse
.
Pros and Cons of using SkiaSharp
Here are a few bullet points to sum up my experience with SkiaSharp.
Pros
- It's cross-platform, a big plus.
- Can be faster than Gdiplus, especially if used with OpenGL controls.
Cons
- Documentation is weak. The good people at Xamarin did a great job and wrote a few tutorials that will get you started, after that, you're on your own.
- Paths can often be generated with unwanted artifacts, especially the ones returned by the
SKPaint
class. - Lack of compound lines.
History
- 28th December, 2022: Date of first publication