Is there a thread-safe way to consume GhostScript via ImageMagick (.NET)?

0

I am writing an API using ImageMagick.NET (from here: https://www.nuget.org/packages/Magick.NET-Q8-AnyCPU/) and GhostScript (from here: https://www.ghostscript.com/download.html) to turn a pdf into an image thumbnail:

        var thumbnailStream = new MemoryStream();
        using (var images = new MagickImageCollection())
        {
            // Read the frames into the collection
            resource.ResourceStream.Seek(0, SeekOrigin.Begin);
            var settings = new MagickReadSettings() { FrameIndex = 0, FrameCount = 1 };
            images.Read(resource.ResourceStream, settings); // WARNING - NOT THREAD SAFE!!

            // Take the first page and make a thumbnail out of it
            var image = images[0];

            // Scale thumbnail to proper width
            var geometry = new MagickGeometry(){ Width = ThumbnailWidth };
            image.Scale(geometry);

            // Write the thumbnail to a MemoryStream
            image.Write(thumbnailStream);
        }

Just running the code as-is results intermittently in:

  HResult=0x80131500
  Message=FailedToExecuteCommand `...gswin64c.exe" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pngalpha" -dTextAlphaBits=4 -dGraphicsAlphaBits=4 "-r300x300" -dFirstPage=1 -dLastPage=1 "-sOutputFile=... 
(The system cannot find the file specified.) @ error/delegate.c/ExternalDelegateCommand/475

This is the same error one would get if GhostScript isn't installed at all. It appears that the ghostscript executable (gswin64c.exe) is intermittently unavailable. However, if I wrap the "Read" call in a lock like so:

            private static readonly object ImageMagickReadLock = new object();

            // LOCK THE THREAD-UNSAFE CODE!!
            lock (ImageMagickReadLock)
            {
                images.Read(resource.ResourceStream, settings);
            }

...everything works as expected! However, this is not a scalable solution (load performance testing demonstrated resource starvation that can cause this average-time 1.5s process to spike to over 20s!!), so I am looking for a thread-safe way to consume GhostScript via ImageMagick (.NET).

There is a 7 year old thread here that indicates it is possible to get the source for GhostScript and compile it oneself with a special flag. (No idea why thread-safety is not the default, but that is a separate discussion). However I'm left skeptical due to the age of the thread and since the solution was never accepted - in fact, the questioner reported issues with the solution. In addition, I don't believe that user was using ImageMagick so I'm not even sure if I were to get it to work if it would even help me.

So, I ask again here: Is there a thread-safe way to consume GhostScript via ImageMagick (.NET)?

Update:

After some testing and help from dlemstra, I'm still seeing an issue though it is a different one than before (which I'm hoping means I'm closer!).

One of my issues was that I was relying on the ghostscript .dll to handle multithreaded requests in the non-lock case, and when the thread using the .dll is occupied, ImageMagick looks for the .exe to fill in with any subsequent concurrent threads. This explains the "file not found" error I describe above - even though the .dll was present, both the .dll and the .exe ghostscipts are needed. So I added the .exe.

However, I am still getting FailedToExecuteCommand, but this time it is able to find the file and the details say error/ghostscript-private.h/InvokeGhostscriptDelegate/143.

Full error message:

ImageMagick.MagickDelegateErrorException
  HResult=0x80131500
  Message=FailedToExecuteCommand `".../GhostScript/gswin64c.exe" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pngalpha" -dTextAlphaBits=4 -dGraphicsAlphaBits=4 "-r300x300" -dFirstPage=1 -dLastPage=1 "-sOutputFile=.../temp/magick-10252ZvAOzMMOsCXz%d" "-f...temp/magick-10252gRfuzynqR0Pe" "-f...temp/magick-10252Bp3xSsSeS1Ol"' (1) @ error/ghostscript-private.h/InvokeGhostscriptDelegate/143
  Source=Magick.NET-Q8-AnyCPU
  StackTrace:
   at ImageMagick.MagickImageCollection.NativeMagickImageCollection.ReadBlob(MagickSettings settings, Byte[] data, Int32 offset, Int32 length)
   at ImageMagick.MagickImageCollection.AddImages(Byte[] data, Int32 offset, Int32 count, MagickReadSettings readSettings, Boolean ping)
   at ImageMagick.MagickImageCollection.AddImages(Stream stream, MagickReadSettings readSettings, Boolean ping)
c#
imagemagick
ghostscript
imagemagick.net
asked on Stack Overflow Mar 24, 2020 by Luke W • edited Mar 25, 2020 by Luke W

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0