Skip to main content Skip to footer

Enhancing JPEG, PNG, Text Using C# .NET Part 1 - Shadow Effect

In various scenarios, such as magazine media, e-commerce product photos, or invitations and flyers, digital image manipulation proves to be both essential and beneficial. During the image processing stage, modifications to size, color, and effects can improve and clarify the final outcome. Additionally, custom figures and shapes like rectangles or ellipses can also be incorporated as part of this process.

An imaging API can help achieve image processing. This article describes how the Document Solutions for Imaging (DsImaging, previously GcImaging), server-side API allows you to draw an image with a shadow effect to create high-production quality images for .NET applications.

Creating an image with shadow involves the following stages:

  • drawing the source image to GcBitmap with some offset
  • creating a transparency mask from that image
  • applying a Gaussian blur to the mask
  • converting the transparency mask to a background image
  • drawing the source image to the background without offset

Ready to Get Started? Download Document Solutions for Imaging Today

GrayscaleBitmap is handy for storing and manipulating a transparency mask. New methods added to the GrayscaleBitmap class make that process straightforward.

For example, let’s create a C# Console App for .NET 7.0, add a reference to GrapeCity.Documents.Imaging NuGet package and the following code drawing an image with some text and graphics:

using System.Drawing;
using System.Numerics;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Imaging;
using GrapeCity.Documents.Text;

using var bmp = new GcBitmap(800, 600, false);
using (var g = bmp.CreateGraphics(Color.AliceBlue))
{
    Draw(g, 0, 0);
}
bmp.SaveAsPng("image1.png");

static void Draw(GcGraphics g, float offsetX, float offsetY)
{
    var baseT = Matrix3x2.CreateTranslation(offsetX, offsetY);
    g.Transform = baseT;
    g.DrawEllipse(new RectangleF(100, 100, 300, 200),
        new Pen(Color.Orange, 20f));
    g.DrawLine(new PointF(50, 400), new PointF(500, 50),
        new Pen(Color.RoyalBlue, 20f)
        {
            LineCap = PenLineCap.Round
        });
    g.DrawString("Howl's Moving Castle",
        new TextFormat
        {
            FontName = "Segoe UI",
            FontSize = 40,
            ForeColor = Color.MistyRose,
            StrokePen = new Pen(Color.DarkRed, 1f)
        },
        new PointF(200, 150));
    g.Transform = Matrix3x2.CreateRotation((float)(Math.PI / 6)) *
        (Matrix3x2.CreateTranslation(50, 250) * baseT);
    g.DrawString("The quick brown fox jumps over the lazy dog.",
        new TextFormat
        {
            FontName = "Times New Roman",
            FontSize = 18,
            ForeColor = Color.CornflowerBlue
        },
        new PointF(0, 0));
    g.DrawRectangle(new RectangleF(-15, -10, 470, 50),
        new Pen(Color.Salmon, 1f));
}

The image saved to image1.png looks like this:

JPEG/PNG/Text Shadow Effect C#

Now let's see how to add a shadow to that image. Firstly, we draw the image to the transparent background with a small offset for shadow:

using var bmp = new GcBitmap(800, 600, false);
using (var g = bmp.CreateGraphics(Color.Transparent))
{
    Draw(g, 20, 50);
}

If we save the resulting GcBitmap (bmp) to a PNG file, it will look like this:

JPEG/PNG/Text Shadow Effect C#

The next step is extracting the alpha channel from GcBitmap to a GrayscaleBitmap:

using var gs = bmp.ToGrayscaleBitmap(ColorChannel.Alpha);

If we draw 0xFF values (opaque pixels) as black and 0x00 values (transparent pixels) as white, the GrayscaleBitmap looks like this:

JPEG/PNG/Text Shadow Effect C#

Now let's apply a Gaussian blur to GrayscaleBitmap using the new ApplyGaussianBlur method:

gs.ApplyGaussianBlur(9);

JPEG/PNG/Text Shadow Effect C#

The next step is converting the transparency mask to a GcBitmap, filling the opaque pixels with the specified shadow color. We can also apply an additional opacity factor to the resulting image:

gs.ToShadowBitmap(bmp, Color.CadetBlue, 0.4f);

The new ToShadowBitmap method can draw the transparency mask into an existing bitmap; no need to create another GcBitmap instance. The image in GcBitmap (bmp) looks like this:

JPEG/PNG/Text Shadow Effect C#

Now we can substitute the transparent background with an opaque background color:

bmp.ConvertToOpaque(Color.AliceBlue);

JPEG/PNG/Text Shadow Effect C#

The background with a shadow is ready. Now we can draw the original image without offset:

using (var g = bmp.CreateGraphics())
{
    Draw(g, 0f, 0f);
}

As you can see, we don’t pass any color to the CreateGraphics method because we want to preserve the current background with shadow in the GcBitmap. We also don’t pass any offset to the Draw method. The resulting image looks like this:

JPEG/PNG/Text Shadow Effect C#

The full source code:

using System.Drawing;
using System.Numerics;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Imaging;
using GrapeCity.Documents.Text;

using var bmp = new GcBitmap(800, 600, false);
using (var g = bmp.CreateGraphics(Color.Transparent))
{
    Draw(g, 20, 50);
}
using (var gs = bmp.ToGrayscaleBitmap(ColorChannel.Alpha))
{
    gs.ApplyGaussianBlur(9);
    gs.ToShadowBitmap(bmp, Color.CadetBlue, 0.4f);
}
bmp.ConvertToOpaque(Color.AliceBlue);
using (var g = bmp.CreateGraphics())
{
    Draw(g, 0f, 0f);
}
bmp.SaveAsPng("image2.png");

static void Draw(GcGraphics g, float offsetX, float offsetY)
{
    var baseT = Matrix3x2.CreateTranslation(offsetX, offsetY);
    g.Transform = baseT;
    g.DrawEllipse(new RectangleF(100, 100, 300, 200),
        new Pen(Color.Orange, 20f));
    g.DrawLine(new PointF(50, 400), new PointF(500, 50),
        new Pen(Color.RoyalBlue, 20f)
        {
            LineCap = PenLineCap.Round
        });
    g.DrawString("Howl's Moving Castle",
        new TextFormat
        {
            FontName = "Segoe UI",
            FontSize = 40,
            ForeColor = Color.MistyRose,
            StrokePen = new Pen(Color.DarkRed, 1f)
        },
        new PointF(200, 150));
    g.Transform = Matrix3x2.CreateRotation((float)(Math.PI / 6)) *
        (Matrix3x2.CreateTranslation(50, 250) * baseT);
    g.DrawString("The quick brown fox jumps over the lazy dog.",
        new TextFormat
        {
            FontName = "Times New Roman",
            FontSize = 18,
            ForeColor = Color.CornflowerBlue
        },
        new PointF(0, 0));
    g.DrawRectangle(new RectangleF(-15, -10, 470, 50),
        new Pen(Color.Salmon, 1f));
}

Two important methods were added to the GrayscaleBitmap class in v6.0.1 of GcDocs:

The ApplyGaussianBlur method has two overloads:

/// <summary>
/// Applies a Gaussian blur to this <see cref="GrayscaleBitmap"/>.
/// </summary>
/// <param name="radius">The radius of the blur, in pixels.</param>
/// <param name="borderMode">The mapping mode for the pixels outside of the border.</param>
public void ApplyGaussianBlur(int radius = 9, GaussianBlurBorderMode borderMode = GaussianBlurBorderMode.Default)

/// <summary>
/// Applies a Gaussian blur to this <see cref="GrayscaleBitmap"/>.
/// </summary>
/// <param name="borderColor">The color used to blend with the edge pixels of the image.</param>
/// <param name="radius">The radius of the blur, in pixels.</param>
/// <param name="borderMode">The mapping mode for the pixels outside of the border.</param>
public void ApplyGaussianBlur(byte borderColor, int radius = 9,
    GaussianBlurBorderMode borderMode = GaussianBlurBorderMode.Default)

The second overload accepts a borderColor as the first argument. In the case of a transparency mask in GrayscaleBitmap, the border color is not a color but the assumed level of opacity (from 0 to 255) for the pixels surrounding the image.

The new ToShadowBitmap method simplifies moving a transparency mask from GrayscaleBitmap to a GcBitmap. There are a couple of overloads:

/// <summary>
/// Creates a semi-transparent <see cref="GcBitmap"/> from the current <see cref="GrayscaleBitmap"/>.
/// <para>
/// This method treats the current <see cref="GrayscaleBitmap"/> as a transparency mask, regardless of the <see cref="TransparencyMask"/> property value.
/// </para>
/// </summary>
/// <param name="shadowColor">The color to fill opaque pixels of the target bitmap.</param>
/// <param name="opacityFactor">Additional factor to scale the alpha channel.</param>
/// <param name="zeroIsOpaque">Specifies whether zero values correspond to fully opaque (<see langword="true"/>)
/// or fully transparent (<see langword="false"/>) pixels.</param>
/// <returns>The newly created <see cref="GcBitmap"/>.</returns>
public GcBitmap ToShadowBitmap(Color shadowColor, float opacityFactor = 1f, bool zeroIsOpaque = false)

/// <summary>
/// Converts an existing instance of <see cref="GcBitmap"/> to a semi-transparent bitmap
/// representing the current <see cref="GrayscaleBitmap"/>.
/// <para>
/// This method treats the current <see cref="GrayscaleBitmap"/> as a transparency mask, regardless of the <see cref="TransparencyMask"/> property value.
/// </para>
/// </summary>
/// <param name="bmp">The target <see cref="GcBitmap"/> object.</param>
/// <param name="shadowColor">The color to fill opaque pixels of the target bitmap.</param>
/// <param name="opacityFactor">Additional factor to scale the alpha channel.</param>
/// <param name="zeroIsOpaque">Specifies whether zero values correspond to fully opaque (<see langword="true"/>)
/// or fully transparent (<see langword="false"/>) pixels.</param>
public void ToShadowBitmap(GcBitmap bmp, Color shadowColor, float opacityFactor = 1f, bool zeroIsOpaque = false)

The main difference from the previously added GrayscaleBitmap.ToGcBitmap method is the ability to specify a shadow color and the additional opacity factor. Also, the ToShadowBitmap method always treats the GrayscaleBitmap as a transparency mask, even if it was created from other color channels, not necessarily the alpha channel.

An example of shadow with perspective:

JPEG/PNG/Text Shadow Effect C#

using System.Drawing;
using System.Numerics;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Imaging;

using var bmp = new GcBitmap(600, 500, false);

// prepare the shadow
using (var g = bmp.CreateGraphics(Color.Transparent))
{
    const float DegToRad = (float)(Math.PI / 180.0);

    var m = Matrix3x2.CreateTranslation(125, 430);
    m = Matrix3x2.CreateScale(1f, -0.23f) * m;
    m = Matrix3x2.CreateSkew(-13.33f * DegToRad, 0f) * m;
    g.Transform = m;

    Draw(g);
}
using var gs = bmp.ToGrayscaleBitmap(ColorChannel.Alpha);
gs.ApplyGaussianBlur(10);
gs.ToShadowBitmap(bmp, Color.CadetBlue, 0.8f);

// fill the background
bmp.ConvertToOpaque(Color.LightGoldenrodYellow);

// draw the main image
using (var g = bmp.CreateGraphics())
{
    g.Transform = Matrix3x2.CreateTranslation(50, 50);
    Draw(g);
}

bmp.SaveAsPng("image3.png");

static void Draw(GcBitmapGraphics g)
{
    var rect = new RectangleF(0, 0, 400, 300);
    var hole = new RectangleF(270, 50, 80, 80);
    var stroke = Color.FromArgb(unchecked((int)0xFF2E75B6));
    var fill = Color.FromArgb(unchecked((int)0xFFED7D31));

    var reg1 = new Region(new RectangularFigure(rect));
    var reg2 = new Region(new EllipticFigure(hole));
    reg1.CombineWithRegion(reg2, RegionCombineMode.Exclude);
    g.Renderer.FillRegion(reg1, fill);

    var pen = new Pen(stroke, 10);
    g.DrawRectangle(rect, pen);
    g.DrawEllipse(hole, pen);
}

MESCIUS' .NET Imaging API Library

This article only scratches the surface of the full capabilities of Document Solutions for Imaging (DsImaging). Review our documentation to see the many available features, and our demos to see the features in action with downloadable sample projects.

Integrating this .NET imaging server-side API into a desktop or web-based application allows developers to programmatically create and manipulate images at scale and load, process, and save images across many different formats. 

Ready to Get Started? Download Document Solutions for Imaging Today

comments powered by Disqus