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):

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):

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)
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
The caution in this MSDN article says that
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.