runtime: Server-side generation of images from a Canvas does not work in IIS but does in IIS Express

Issue Title

Server-side generation of images from a Canvas does not work in IIS (Release) but does in IIS Express (both Debug & Release)

General

This is a controller class used to generate a PNG file on the server side and return it to the client (browser). The image is made through the creation of a System.Windows.Controls.Canvas object first (along with some operations on it), which is then rendered to a System.Windows.Media.Imaging.RenderTargetBitmap object, finally to a System.Drawing.Bitmap in PNG format and then streamed to the client.

This piece of code is a migration to .NET Core from another project that was originally made for .NET Framework 4.5.2 and worked fine in both the development computer and the production server (though it was a Windows Server 2012R2 instead of a Windows Server 2019).

Sample Code

using System.IO;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Microsoft.AspNetCore.Mvc;

namespace myNameSpace.Controllers
{
    public partial class drawController : Controller
    {
        private const string MEDIA_TYPE_PNG = "image/png";

        [RequireHttps]
        [HttpGet()]
        public ActionResult image()
        {
            byte[] theImage = CreateImage(96, 320);
            if (theImage != null)
            {
                var stream = new MemoryStream(theImage);
                FileStreamResult result = new FileStreamResult(stream, new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(MEDIA_TYPE_PNG));
                return result as FileResult;
            }
            else
            {
                return new BadRequestObjectResult("Error");
            }
        }

        private byte[] CreateImage(double screen_dpi, double image_size)
        {
            byte[] outByteArr = null;

            ThreadStart ts = new ThreadStart(delegate () { CreateImageSTA(screen_dpi, image_size, out outByteArr); });
            Thread thread = new Thread(ts);
            thread.IsBackground = true;
            if (thread.TrySetApartmentState(ApartmentState.STA)) // Since we will use UI components in the server side
            {
                thread.Start();
                thread.Join();
            }
            return outByteArr;
        }

        [System.STAThread]
        private static void CreateImageSTA(double screen_dpi, double image_size, out byte[] outByteArr)
        {
            double OuterDiameter = 100;
            double Thickness = 3;
            // Measure and arrange the surface
            System.Windows.Controls.Canvas canvas = CreateCanvas(OuterDiameter, Thickness);

            double fake_dpi = screen_dpi * (image_size / OuterDiameter);

            Size size = new Size(image_size, image_size);
            canvas.Measure(size);
            canvas.Arrange(new Rect(size));

            // Create a render bitmap and push the part to it
            RenderTargetBitmap renderBitmap = new RenderTargetBitmap((int)size.Width, (int)size.Height, fake_dpi, fake_dpi, PixelFormats.Pbgra32);
            Rect bounds = VisualTreeHelper.GetDescendantBounds(canvas);
            DrawingVisual dv = new DrawingVisual();
            using (DrawingContext ctx = dv.RenderOpen())
            {
                VisualBrush vb = new VisualBrush(canvas);
                ctx.DrawRectangle(vb, null, new Rect(new Point(), bounds.Size));
            }
            renderBitmap.Render(dv);

            // Create a file stream for saving image
            using (MemoryStream outStream = new MemoryStream())
            using (MemoryStream tempStream = new MemoryStream())
            {
                var encoder = new PngBitmapEncoder(); // Use png encoder for our data
                encoder.Frames.Add(BitmapFrame.Create(renderBitmap)); // push the rendered bitmap to it
                encoder.Save(tempStream); // save the data to the stream

                using (System.Drawing.Bitmap bitmap = (System.Drawing.Bitmap)System.Drawing.Image.FromStream(tempStream))
                {
                    using (System.Drawing.Bitmap newBitmap = new System.Drawing.Bitmap(bitmap))
                    {
                        double scale = System.Math.Max(size.Width, size.Height) / 1200D; // regular size
                        newBitmap.SetResolution((float)screen_dpi, (float)screen_dpi);
                        newBitmap.Save(outStream, System.Drawing.Imaging.ImageFormat.Png);
                    }
                }
                outByteArr = outStream.ToArray();
            }
        }

        private static System.Windows.Controls.Canvas CreateCanvas(double ShellOuterDiameter, double ShellThickness)
        {
            Canvas myCanvas = new Canvas();

            double NormalizedThickness = ShellOuterDiameter * (0.2 / 100D);
            double ShellInnerDiameter = ShellOuterDiameter - (2 * ShellThickness);

            // The real Canvas is much more complex. For the sake of simplicity, let's just draw a Circle

            if ((ShellOuterDiameter > 0) && (ShellInnerDiameter > 0))
            {
                Ellipse ShellOuterFill = new Ellipse();
                ShellOuterFill.StrokeThickness = NormalizedThickness;
                ShellOuterFill.Stroke = Brushes.Black;
                ShellOuterFill.Fill = Brushes.LightGray;
                Canvas.SetLeft(ShellOuterFill, -(ShellOuterDiameter / 2));
                Canvas.SetTop(ShellOuterFill, -(ShellOuterDiameter / 2));
                ShellOuterFill.Width = ShellOuterDiameter;
                ShellOuterFill.Height = ShellOuterDiameter;
                myCanvas.Children.Add(ShellOuterFill);
            }

            return myCanvas;
        }
    }
}

Behaviour

When the url “/draw/image” is called from both Debug/Release within Visual Studio 2019 and IIS Express, the resulting image is as expected (a PNG with a circle in it):

image

However, when this is deployed to a Windows Server 2019 with IIS, the resulting image is an empty PNG file (full transparency, no circle there):

image-2

The development is done in a Windows 10 v1909 (18363.592). Microsoft Visual Studio Community 2019 Versión 16.4.3 .NET Core 3.1.1

The production server is a Windows Server 2019 hosted in Azure. Also with .NET Core 3.1.1

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 22 (11 by maintainers)

Most upvoted comments

I found the solution to the problem.

Github compatibility Service/Non-interactive Window Stations Rendering will continue by default, irrespective of the presence of display devices. Unless the WPF API’s being used are short-lived (like rendering to a bitmap), it can lead to a CPU spike. If an application running inside a service would like to receive the ‘default’ WPF behavior, i.e., no rendering in the absence of display devices, then it should set to true

In short, add a new file “runtimeconfig.template.json” to your project with the content

{
"configProperties": {
  "Switch.System.Windows.Media.ShouldRenderEvenWhenNoDisplayDevicesAreAvailable": true
  }
}

The caution in this MSDN article says that

Classes within the System.Drawing namespace are not supported for use within a Windows or ASP.NET service. Attempting to use these classes from within one of these application types may produce unexpected problems, such as diminished service performance and run-time exceptions. For a supported alternative, see Windows Imaging Components.

Should this be in the Future milestone? A lot of effort went in to finding a solution but nobody explained that this isn’t a supported use case.