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
Comments
With $zf(-100) the command and its individual flags are separate arguments to the function rather than being concatenated in one string - I'd recommend trying something more like:
set sc = $ZF(-100,"/STDIN="""_imageFile_""" /STDOUT="""_tempFile_"""","magick","fd:0","-resize","640x480","fd:1")
For more details see https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cl…
@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.