Question
Daniel Bertozzi · Mar 9, 2021

Converting images using IRIS and image magick

Hi all

I've been looking into how to convert some images with IRIS and saw some recommendations on the forums around using image magick.

However I can't seem to get this working using ZF100 and on windows server and I'm wondering if I'm missing something really obvious as I've not used IRIS to call third party applications before.

If I run via a Windows CLI on the server, it works fine, e.g:

magick "D:\1221701739_20190716134351965_1_0.jpg" -resize 640x480 "D:\1221701739_20190716134351965_1_TEST.jpg

But I keep getting NOTOPEN when I attempt this in IRIS. I'm admin on my dev box so I think my permissions should be ok.

 set sc = $ZF(-100,"/STDIN="""_imageFile_""" /STDOUT="""_tempFile_"""","magick fd:0 -resize 640x480 fd:1")

I've tried all sort of variations and logged the queries to /LOGCMD but I can't see if there is issue with the syntax

Also I think fd:0 and fd:1 are image magicks on internal flags for using STDIN / STDOUT but I've tried with the paths also.

Does any one have any examples or provide any tips how to use IRIS in this way?

Thanks


Dan

Product version: IRIS 2019.1
$ZV: IRIS for Windows (x86-64) 2019.1.1 (Build 612U) Mon Oct 28 2019 11:29:24 EDT
00
3 1 8 116
Log in or sign up to continue

Replies

@Daniel Bertozzi , following up - I downloaded ImageMagick and the following works just fine for me (though I'm a little surprised at how slow it is):

Class DC.Demo.ImageMagick
{

ClassMethod Convert(inFile As %String = "C:\Temp\ImageMagick\inFile.jpg", outFile As %String = "C:\Temp\ImageMagick\outFile.jpg")
{
    Do $zf(-100,"","magick",inFile,"-resize","640x480",outFile)
}

}

I think the likely issue is that ImageMagick isn't on your PATH. You'll need to restart your instance for it to pick up PATH changes, so this might be the root cause if you just installed ImageMagick. Could also be interesting to run with the /SHELL flag and see if that works.

Hopefully this helps!

So it turns out my original code and your suggested version worked after all, just needed a server reboot for the PATH to take affect! I just need to sort out user permissions around the account that can use $ZF(-100) and I think I'm all good!

Anyway, I thought i'd just write up my a way of doing this in case it's helpful to anyone.

Below is a REST class that takes a list of files in array and it will convert them to Basic64 using a temp path. Example payload in the code, there's few parameters you can mess around with.

In my actual code I get paths from a SOAP interfaces and need to convert the files back to website. Therefore I am attempting to do this at the last point so it doesn't produce audits/messages. I also don't want exceptions to ever go back to the client so I will always force 1x1 image to rendered but I've added the errors back into this example.

Include %occInclude /// <PRE style="font-size:0.8em;">  
/// ************************************************************************************************************************************
/// Class Name  : Demo.REST.Server
/// Description : Demo REST Service   
/// ************************************************************************************************************************************
/// </PRE>
/// 
/// Please note there are some limitations as string max is 3,641,144 characters. This is addressed in newer IRIS versions, see links below:
/// https://community.intersystems.com/post/size-limitation-tojson-large-stream
/// https://community.intersystems.com/post/cache-how-change-image-file-size-when-saving-cache-database-table
/// https://community.intersystems.com/post/how-resize-image-classmethod
/// 
Class Demo.REST.Server Extends %CSP.REST
{ Parameter CONVERTINPUTSTREAM = 1; Parameter CHARSET = "utf-8"; Parameter CONTENTTYPE = "application/json"; XData UrlMap
{
<Routes>
<Route Url="/api/v2/published/images/toBase64" Method="POST" Call="PostConvertImages"/>
</Routes>
} /// Example:
/// {
///    "Images": [
///        "D:\\PATH\1221701739_20190716134351965_1_0.jpg"
///    ],
///    "TempOutputDir": "D:\\PATH\\TEMP\\",
///    "UseResize": true,
///    "RemoveTempFiles": true,
///    "Dimensions":
///    {
///        "x": "640",
///        "y": "480"
///    }
/// }
/// 
ClassMethod PostConvertImages() As %Status
{
    set tSC=$$$OK
    try
    {
        //read input from REST    
        set sourceJson = {}.%FromJSON(%request.Content)         //setup response
        set tmpStream = ##class(%Stream.GlobalCharacter).%New()
        
        //initial output
        set targetJson = {}
        set targetJson.Images = []         //check tokens
        if (sourceJson.Dimensions.x'="")
        {
            set x = sourceJson.Dimensions.x
        }
        else
        {
            set x = "640"
        }         if (sourceJson.Dimensions.y'="")
        {
            set y = sourceJson.Dimensions.y
        }
        else
        {
            set y = "480"
        }
        
        set dimensions = x_"x"_y         if (sourceJson.TempOutputDir'="")
        {
            set tempDirectory = sourceJson.TempOutputDir
        }
        else
        {
            set tempDirectory = ""
        }         if (sourceJson.UseResize="")
        {
            set useResize = 0
        }
        else
        {
            set useResize = sourceJson.UseResize
        }         if (sourceJson.RemoveTempFiles="")
        {
            set removeTempFiles = 1
        }
        else
        {
            set removeTempFiles = sourceJson.RemoveTempFiles
        }         //Check for null
        if (sourceJson.Images '= "")
        {
            //setup image array 
            set imageArray = []             //iterate through images
            set iterArray = sourceJson.Images.%GetIterator()
            while iterArray.%GetNext(.key, .value)
            {
                try
                {
                    //get file details
                    set fileExtension = $PIECE(value,".",2) 
                    set imageFile = $REPLACE(value,"\\","\") //For escaped paths                     //get file size
                    set checkFile = ##class(%File).%New(imageFile)
                    set sc = checkFile.Open("R") 
                    $$$ThrowOnError(sc)
                    set fileSize = checkFile.SizeGet()
                    do checkFile.Close()                     if (fileSize > 0)
                    {
                        try
                        {
                            //work out what will be in base64 - if over certain number of bytes force resize regardless
                            if ((4 * fileSize / 3) > 300000)
                            {
                                set useResize = 1
                            }                             //check whether to rencode image
                            if ((useResize=1)&&(tempDirectory'=""))
                            {
                                //update flag
                                set tempFile = tempDirectory_$System.Util.CreateGUID()_fileExtension
                                set sc = $ZF(-100,"/STDIN="""_imageFile_""" /STDOUT="""_tempFile_"""","magick","fd:0","-resize",dimensions,"fd:1")
        
                                set convertedImageStream=##class(%Stream.FileBinary).%New()
                                set sc=convertedImageStream.LinkToFile(tempFile)                                 if ((sc = 1)&&(convertedImageStream.Size > 0))
                                {
                                    //encode to base64
                                    do ..Base64EncodeStream(convertedImageStream,.encodedStream)
                                    set line = ""
                                    do encodedStream.Rewind()
                                    while (encodedStream.AtEnd = 0) {
                                        set len = 5700
                                        set line = line_encodedStream.Read(.len)
                                    }
                                    set image = {}
                                    set image.src ="<img src=""data:image/"_$ZSTRIP(fileExtension,"*P")_";charset=utf-8;base64,"_line_""">"
                                }
                                else
                                {
                                    //create smallest jpg image 1x1
                                    set image = {}
                                    set image.src ="<img src=""data:image/jpg;charset=utf-8;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA="">"    
                                }
                                
                                //delete from disk
                                if (removeTempFiles=1)
                                {
                                    set deleteTmpFile = ##class(%File).%New(tempFile)
                                    set sc = deleteTmpFile.Delete(tempFile)
                                }
                            }
                            else
                            {
                                //encode to base64
                                set stream=##class(%Stream.FileBinary).%New()
                                set sc=stream.LinkToFile(imageFile)
                                do ..Base64EncodeStream(stream,.encodedStream)
                                set line = ""
                                do encodedStream.Rewind()
                                while (encodedStream.AtEnd = 0) {
                                    set len = 5700
                                    set line = line_encodedStream.Read(.len)
                                }
                                set image = {}
                                set image.src ="<img src=""data:image/"_$ZSTRIP(fileExtension,"*P")_";charset=utf-8;base64,"_line_""">"
                            }
                        }
                        catch ex
                        {
                            //ignore exception, create smallest jpg image 1x
                            set image = {}
                            set image.error = ex.DisplayString()
                            set image.src ="<img src=""data:image/jpg;charset=utf-8;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA="">"
                        }
                    }
                    else
                    {
                        //create smallest jpg image 1x1
                        set image = {}
                        set image.error = ex.DisplayString()
                        set image.src ="<img src=""data:image/jpg;charset=utf-8;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA="">"
                    }
                    do imageArray.%Push(image)
                }
                catch ex
                {
                    set image = {}
                    set image.error = ex.DisplayString()
                    do imageArray.%Push(image)
                }
                //Write images to string
                set targetJson.Images = imageArray
            }
        }           //write to stream
        do targetJson.%ToJSON(tmpStream)
        set ns = ##class(%Stream.GlobalCharacter).%New()
        do ns.CopyFrom(tmpStream)
        $$$ThrowOnError(tSC)
        set %response.ContentType = "application/json"
        set %response.Status = 200
        do ns.Rewind()
        return ns.OutputToDevice()
    }
    catch ex 
    {    
        set tSC = ex.AsStatus()
    }
    return tSC
} /// Encode a stream as BASE64, (based off intersystems exampk but with CRLF off)
ClassMethod Base64EncodeStream(pStream As %Stream, Output pEncoded As %Stream) As %Status
{
  s tSC=$$$OK
  try {
    s tSC=pStream.Rewind()
    q:$$$ISERR(tSC)     s pEncoded=##class(%Stream.TmpCharacter).%New()
    while 'pStream.AtEnd {
      s tLen=5700 
      s tSC=pEncoded.Write($system.Encryption.Base64Encode(pStream.Read(.tLen),1))
      q:$$$ISERR(tSC)
    }
    q:$$$ISERR(tSC)
        
    s tSC=pEncoded.Rewind()
  } catch (e) {
    s tSC=e.AsStatus()
  }
  q tSC
}

I am using read queue to be able to monitor the output from Image like this :
 

Parameter INSTALLDIR = "c:\ImageMagick\";

/// Do ##class(Image.Utils).Resize(file, newFile500, "500X500", .msg)
ClassMethod Resize(fileOrig As %String, fileNew As %String, newSize As %String, ByRef output as %String) As %Boolean
{
  Quit ..Convert(fileOrig, "-resize "_newSize_"^> """_fileNew_"""", .output)
}

ClassMethod Convert(file, options, ByRef output as %String) As %Boolean
{
  Do ..Cmd("convert """_file_""" "_options, .output)
  Quit 1
}

ClassMethod Cmd(command As %String, ByRef outputStr As %String)
{
  Kill outputStr
  Try {
    Set cmd=..#INSTALLDIR_command
    Open cmd:("RQ")
    For {
      Use cmd Read line If $ZEOF=-1 Quit
      Set outputStr($i(outputStr))=line
    }
catch {
  }
  Close cmd
}

Thanks very much for replying with some suggestions. It's really helpful.

@Timothy Leavitt 
Thanks for the advice. I did try separate arguments in a previous version of my code, so it's nice to see I was on the right track originally so I will go back that way. With the $ZF(-100) approach I am getting 0k output file which suggest the STDOUT is being creating something (if somewhat unusable), so it could be something on the Windows side that is throwing the NOTOPEN.

@Danny Wijnschenk 
I've not thought of using OPEN. It's interesting option. I was exploring this briefly just now. What would you usually expect to see in the output normally. I've just been getting a 2.

It might be helpful to see the value of $zu(56,2) after the error occurs (if you continue to get <NOTOPEN>) - of course you probably don't really need to use STDIN/STDOUT and it might be cleaner to not.

Hi Daniel, did you do a zwrite output ? It is an array, so if output=2 there should be more data in it.

(usually output is empty when the command was executed successfully)
I like using using OPEN because any shell output message is captured and can be logged.
e.g.

do ##class(Image.Utils).Convert("a.jpg","-resize 10X10", .output)
zwrite output
output=2
output(1)="convert: UnableToOpenBlob 'a.jpg': No such file or directory @ error/blob.c/OpenBlob/3109."
output(2)="convert: MissingAnImageFilename `10X10' @ error/convert.c/ConvertImageCommand/3272."

Ok I got your method working now also. I can see the benefit of having the shell logs captured. It's great to have two options! Thanks.