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)
Luke W
  • 63
  • 9
  • I do not know. But if you can run Ghostscript with some arguments that make it thread-safe, then you can modify the delegates.xml file for ImageMagick and change or add those arguments. You might inquire further with dlemstra, the ImageMagick.Net developer on the ImageMagick server at https://imagemagick.org/discourse-server/viewforum.php?f=27 or at https://github.com/dlemstra/Magick.NET – fmw42 Mar 24 '20 at 01:05
  • The discussion thread (and the comment about building Ghostscript) refer to using Ghostscript as a part of an application running mutliple threads, which isn't the way ImageMagick works with it. ImageMagick (I believe) spawns a heavywight process executing the gs executable, it does not build Ghostscript into the application (which is what the discussion refers to) and then use multiple threads of execution in that application. – KenS Mar 24 '20 at 08:24
  • It sounds like IM can't run start another shell execution of the Ghostscript binary while the previous one is still running. That's not actually a Ghostscript problem. I can't think why it would happen, but I can easily run concurrent copies of Ghostscript in Linux or Windows, provided I don't do something silly like try to write to the same file. – KenS Mar 24 '20 at 08:25
  • @fmw42 My understanding is the thread-safety arguments are compile-time flags, not runtime flags. Though it's good to know about the delegates.xml file and I'll try to follow up with dlemstra anyway. – Luke W Mar 24 '20 at 16:39
  • @KenS That's interesting. I had been assuming that GS was the thread-safety gap because all the resources I looked up regarding IM said IM was itself thread-safe, leaving me with only one alternative: it's GS's fault. But it could be that merely IM internally is thread-safe and not how it manages other processes. – Luke W Mar 24 '20 at 16:39
  • I am not sure, but I do not think GS is multi-threaded. So perhaps disabling OpenMP in ImageMagick would help or at least telling it to use only 1 thread. But ask dlemstra. – fmw42 Mar 24 '20 at 18:34
  • Thanks. I have "cross-posted" this thread as an issue in Magick.NET to dlemstra here: https://github.com/dlemstra/Magick.NET/issues/596 – Luke W Mar 24 '20 at 18:36

0 Answers0