Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

Recreating Gdiplus Hatches with SkiaSharp

5.00/5 (5 votes)
28 Dec 2022CPOL5 min read 13.7K   299  
A simple technique to quickly port hatch effects from Gdiplus to SkiaSharp
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
Percent20 example DiagonalBrick example DiagonalCross example

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
Percent20 example DiagonalBrick example DiagonalCross example

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:

C#
// Size of the bitmap that will hold the pattern.
// It's 8x8 pixels, since all patterns are based on an 8x8 matrix.
readonly static int _matrixSize = 8;

static List<SKPoint> GetHatchPattern(HatchStyle hatch, bool inverted)
{
    // Build a list of filled points that define the pattern.
    List<SKPoint> ret = new();

    // Build an 8x8 bitmap.
    var bitmap = new Bitmap(_matrixSize, _matrixSize);
    using Graphics graphics = Graphics.FromImage(bitmap);
    graphics.SmoothingMode = SmoothingMode.Default; // No antialias

    // Define colors.
    Color color1 = inverted ? Color.White : Color.Black;
    Color color2 = inverted ? Color.Black : Color.White;

    // Draw the hatch pattern.
    using HatchBrush brush = new(hatch, color1, color2);
    Rectangle rect = new(0, 0, _matrixSize, _matrixSize);
    graphics.FillRectangle(brush, rect);

    // Define black threshold.
    int blackValue = hatch switch
    {
        HatchStyle.ForwardDiagonal or
        HatchStyle.BackwardDiagonal or
        HatchStyle.DiagonalCross => -15395563,// Special cases due to antialias.
        _ => Color.Black.ToArgb(),
    };

    // Read the pixels.
    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 HatchStyles, 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:

C#
// Get fill rectangle.
SKRect rect = canvas.DeviceClipBounds;

// Create fill path.
using SKPath fillPath = new();
fillPath.AddRect(rect);

// Build an SKPath containing the pattern.
// _currentPattern contains the results of a call to GetHatchPattern
SKPath patternPath = new();
foreach (var point in _currentPattern)
    patternPath.AddRect(new(point.X, point.Y, point.X + 1, point.Y + 1));

// Give it padding.
rect.Inflate(-_padding, -_padding);

// Translate the pattern to coordinate 0, 0 of the fill rectangle.
// This is the equivalent of Gdiplus Graphics.RenderingOrigin.
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));

// Build paint with a 2-color gradient.
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)
};

// Build path effect.
SKMatrix matrix = SKMatrix.CreateScale(_matrixSize, _matrixSize);
using SKPathEffect effect = SKPathEffect.Create2DPath(matrix, patternPath);
if (effect == null) return;
paint.PathEffect = effect;

// Cleanup previous run.
canvas.Clear(SKColors.White);

// Add clipping.
canvas.Save();
canvas.ClipRect(rect);

// Fill.
canvas.DrawPath(fillPath, paint);

// Remove clipping.
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:

Screenshot

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)