Search code examples
c#azureasp.net-coregdi

Graphics generation in csharp with Graphics.DrawString renders different, depending on platform. (GDI)


For a Belgian sports community project, built in ASP.Net Core, I am dynamically rendering an image in C#. The image is based on a "base picture", in which I dynamically add Text and then return the image to the client.

It works well, and I am quite happy with the results.

However, when I compare the image that is generated client side, on my dev environment (which is MacOS), that image renders more beautiful, compared to my production environment (a Linux VM in the Azure platform).

As a comparison, you can see two pictures, and you will see the difference, when looking at the texts. The text strings look much more pixelated, in the second picture.

Is there something I can do, to avoid this? Could this be related to the (un)availability of a GPU, or something like that?

Any insights are welcome.

(I also have added the Nuget package runtime.osx.10.10-x64.CoreCompat.System.Drawing to my project, which was needed to run successfully on my MacOS)

For reference: the relevant code snippet:

public async Task<byte[]> CreateImageAsync(YearReview summary)
{
    var fonts = new PrivateFontCollection();
    fonts.AddFontFile(Path.Combine(_hostingEnvironment.WebRootPath, "fonts/bold.ttf"));
    fonts.AddFontFile(Path.Combine(_hostingEnvironment.WebRootPath, "fonts/light.otf"));
    var boldFont = fonts.Families[0];
    var lightFont = fonts.Families[1];

    var assembly = Assembly.GetAssembly(typeof(YearImageGenerator));
    var resourceName = "CotacolApp.StaticData.year-a.png";
    // Leaving out some variable/constants, irrelevant to the sample
    await using (var stream = assembly.GetManifestResourceStream(resourceName))
    {
        var bitmap = new Bitmap(stream);
        using (var gfx = Graphics.FromImage(bitmap))
        {
                using (Font f = new Font(boldFont, 58, FontStyle.Bold, GraphicsUnit.Pixel))
                {
                    gfx.DrawString($"{summary?.UniqueColsInYear.Number()}", f, Brushes.Black,
                        new RectangleF(0, startHeightYear, yearValuesRightBorder, 100f), rightAlignFormat);
                }
        }

        using (var imgStream = new MemoryStream())
        {
            bitmap.Save(imgStream, System.Drawing.Imaging.ImageFormat.Png);
            return imgStream.ToArray();
        }
    }
}

If you compare the pictures, look at the black numbers on the left , and the black text on the right side of the picture.

Picture, rendered on the dev environment (MacOS): Good pic

Picture, rendered in the Azure cloud: Bad pic


Solution

  • Don't use System.Drawing. Microsoft itself warns against this in the documentation. It exists in .NET Core only for compatibility

    In .NET 6 and later versions, the System.Drawing.Common package, which includes this type, is only supported on Windows operating systems. Use of this type in cross-platform apps causes compile-time warnings and run-time exceptions. For more information, see System.Drawing.Common only supported on Windows.

    The linked article explains what's wrong and offers several cross-platform alternatives like ImageSharp and SkiaSharp.

    System.Drawing's primary job is to draw the UI on the screen anyway, not manipulate images. On Windows it's just a very thin wrapper over GDI+. The equivalent cross-platform technology is MAUI, which hasn't been released yet.

    The equivalent code in ImageSharp could be:

    using SixLabors.ImageSharp;
    using SixLabors.ImageSharp.Processing;
    using SixLabors.ImageSharp.Drawing.Processing;
    using SixLabors.Fonts;
    ...
    //Load the fonts
    FontCollection fonts = new FontCollection();
    var boldPath = Path.Combine(_hostingEnvironment.WebRootPath, "fonts/bold.ttf");
    var lightPath = Path.Combine(_hostingEnvironment.WebRootPath, "fonts/light.ttf");
    var boldFont = fonts.Install(boldPath);
    var lightFont = fonts.Install(lightPath);
    
    //Load the image
    var image = await Image.LoadAsync(stream);
    
    //Draw the text
    string yourText = $"{summary?.UniqueColsInYear.Number()}";
    
    Font font = boldFont.CreateFont(58, FontStyle.Bold);
    
    DrawingOptions options = new DrawingOptions()
    {
        TextOptions = new TextOptions
        {
            WrapTextWidth = yearValuesRightBorder,
            HorizontalAlignment = HorizontalAlignment.Right
        }
    };
    
    var point = PointF(0, startHeightYear);
    image.Mutate(x => x.DrawText(options, yourText, font, Color.Black, point));
    
    //Save as PNG to a buffer
    using var ms = new MemoryStream();
    await image.SaveAsPngAsync(ms);
    return ms.ToArray();
    

    You can check the Getting Started section in the docs. SaveAsPngAsync is a convenience extension method. Drawing Text shows how to draw text on the image and the Fonts section shows how to handle fonts.