1

We've been getting this issue for a long time now, and tried a lot of different fixes from the net. So far nothing worked.

Issue: The Base Image is saving just fine, thumbnails are failing on the save. Before the solution below, I have tried creating separate streams for all images (base image, 600x600 and 300x300 thumb) and this didn't work either. All of the streams were constructed from the same byte array. Keep this in mind: This works just fine on Development environment, Test environment and Test2 environment however fails to work on Production environment. I have checked all the settings / environment variables / permissions on folders and everything is setup the same as the Test environments.

Paths are coming in as follows:

  • Base Path: "~/images/imageUpl/"
  • Thumbnail Add. Path: "thumbM/"
  • Thumbnail Add. Path: "thumbS/"
  • Image name struct: "X_YYYYMMDD_HHMMSS_XX.png"

The paths are all correct - as it works Dev/Test/Test2 environments.

Any help on this is much appreciated!

EDIT: What we tried so far:

  • Set permissions for Network and IISUser
  • Use separate streams for each image constructed from original source data
  • Adding thread.sleep(30+) as per other examples
  • Creating a fresh Bitmap from the resized one and saving that
  • Different path to test if its directory issue on production

EDIT 2: For reference, this is a ASP.NET MVC5 Web Application, running of .NET Framework 4.7.2.

Image Processor Class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Drawing.Drawing2D;
using Newtonsoft.Json;
using System.Threading;

namespace CMS_MVC.Classes
{
    public class ImageProcessor : IDisposable
    {
        public enum PathType : int
        {
            Relative = 1,
            Static = 2
        }

        private Stream _ImageStream { get; set; }
        private HttpContext _Context { get; set; }
        public Image BaseImage { get; set; }
        private int _instanceId { get; set; }

        public ImageProcessor(int instanceId, Stream imageStream, HttpContext context)
        {
            this._ImageStream = imageStream;
            this._Context = context;
            this._instanceId = instanceId;

            this.BaseImage = Image.FromStream(this._ImageStream);
        }

        public ImageProcessor(int instanceId, byte[] imageByteArray, HttpContext context)
        {
            this._Context = context;
            this._instanceId = instanceId;

            this._ImageStream = new MemoryStream(imageByteArray);
            this.BaseImage = Image.FromStream(this._ImageStream);
        }

        public ImageProcessor(int instanceId, string imagePath, PathType pathType, HttpContext context)
        {
            this._Context = context;
            this._instanceId = instanceId;

            if (pathType == PathType.Relative)
            {
                this._ImageStream = new MemoryStream(File.ReadAllBytes(this._Context.Server.MapPath(imagePath)));
                this.BaseImage = Image.FromStream(this._ImageStream);
            }
            else
            {
                this._ImageStream = new MemoryStream(File.ReadAllBytes(imagePath));
                this.BaseImage = Image.FromStream(this._ImageStream);
            }
        }

        public Dictionary<string, bool> SaveImages(string baseImageSavePath, string imageName, Dictionary<string, Tuple<int, int>> thumbnails = null)
        {
            Dictionary<string, bool> results = new Dictionary<string, bool>();
            string lastResult = "main";
            results.Add(lastResult, true);

            try
            {
                this.BaseImage.Save(this._Context.Server.MapPath(Path.Combine(baseImageSavePath, imageName)), ImageFormat.Png);

                if (thumbnails != null)
                {
                    foreach (var thumbnail in thumbnails)
                    {
                        lastResult = thumbnail.Value.Item1.ToString() + "_" + thumbnail.Value.Item2.ToString();
                        results.Add(lastResult, true);

                        using (Bitmap thumbBitmap = this.ResizeImage(thumbnail.Value.Item1, thumbnail.Value.Item2))
                        {
                            Thread.Sleep(50);
                            thumbBitmap.Save(this._Context.Server.MapPath(Path.Combine(baseImageSavePath + thumbnail.Key, imageName)), ImageFormat.Png);
                            Thread.Sleep(50);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                results[lastResult] = false;

                // Log event
            }

            return results;
        }

        private Bitmap ResizeImage(int targetWidth, int targetHeight)
        {
            Tuple<int, int> destSize = this.CalculateThumbnailSizeAspectRatio(targetWidth, targetHeight);

            var destRect = new Rectangle(0, 0, destSize.Item1, destSize.Item2);
            var destImage = new Bitmap(destSize.Item1, destSize.Item2);

            destImage.SetResolution(this.BaseImage.HorizontalResolution, this.BaseImage.VerticalResolution);

            using (var graphics = Graphics.FromImage(destImage))
            {
                graphics.CompositingMode = CompositingMode.SourceCopy;
                graphics.CompositingQuality = CompositingQuality.HighQuality;
                graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
                graphics.SmoothingMode = SmoothingMode.HighQuality;
                graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;

                using (var wrapMode = new ImageAttributes())
                {
                    wrapMode.SetWrapMode(WrapMode.TileFlipXY);
                    graphics.DrawImage(this.BaseImage, destRect, 0, 0, this.BaseImage.Width, this.BaseImage.Height, GraphicsUnit.Pixel, wrapMode);
                }
            }

            return destImage;
        }

        private Tuple<int, int> CalculateThumbnailSizeAspectRatio(int targetWidth, int targetHeight)
        {
            // Resize calculations
        }

        public void Dispose()
        {
            if (this._ImageStream != null) this._ImageStream.Dispose();
            if (this.BaseImage != null) this.BaseImage.Dispose();
        }
    }
}
BartWD
  • 140
  • 1
  • 10
  • 1
    Related: https://stackoverflow.com/q/56027141/60761 – H H May 08 '19 at 08:45
  • 4
    You [should not use System.Drawing on a server](https://learn.microsoft.com/en-us/dotnet/api/system.drawing?view=netframework-4.8#remarks) – H H May 08 '19 at 08:45
  • _"tried a lot of different fixes from the net"_ - but you didn't find any advice to stay away from System.Drawing on a server? Basically and simplistically, GDI+ is for user interfaces, and Windows Services have none. This causes various problems when using GDI+ from a Windows Service, which IIS is as well, and therefore your ASP.NET code runs in a service. Find a proper alternative. – CodeCaster May 08 '19 at 08:57
  • @CodeCaster I have honestly never seen that warning or heard about it, this is the first time I saw it. I can also add this that when running through a manual image uploader that uses the same code, it works on that production box. – BartWD May 08 '19 at 09:00
  • I have had code that was succesfully tested under very heavy load on Win10 desktop. Installed on a server it failed after a few hours of much lighter load. You have to test it as a Service on a Server. – H H May 08 '19 at 09:03
  • @HenkHolterman I understand that, however this is not running as a service, its a web application. I have a separate image uploader using this code as well and it works just fine. It seems to fail when there's multiple images being fired at it. I will read up on your suggestion to use ImageSharp. – BartWD May 08 '19 at 09:07
  • 1
    A Web Application ususally runs as or under a service (IIS). Anyway, ASP.NET is explicitly included in the warning. – H H May 08 '19 at 09:11

1 Answers1

2

From the documentation site:

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.

That site then directs you to WICs but that is not a good idea. The WPF Media classes are also not supported on a server (but an equally clear statement about this is missing).

Various alternatives have come up, here is an older SO question.

Your best bet is to look for a fully managed solution, a.o. ImageSharp.

I'm still looking for a similar statement or insight about System.Drawing.Common, a .net core library.

H H
  • 263,252
  • 30
  • 330
  • 514