Use an IStream object to avoid disk access


 


Programs need to read and write data. Sometimes the data storage is only used temporarily. If the storage medium is a file, temporary files need to be deleted. Some Windows APIs allow reading and writing data to an object that implements IStream. IStream has methods that read and write to the underlying storage, which can be a file, memory, or even a named pipe: whatever the object chooses. However, IStream doesn’t inherit from IDispatch, so you can’t use Automation directly. Instead, I’ve created a simple COM wrapper that can read/write a stream, but uses the Library Construction Kit APIs to read and write Fox variables and fields.


 


Using memory for the stream instead of a disk file can be much faster, and releasing the stream can free the used memory.


 


The code below subclasses the GDIPlus wrapper foundation classes (which wraps the GDI+ Flat API ) and adds a new method that calls GdipSaveImageToStream in the GDI+ Flat API which will write to a stream. It loads a jpg into GDIPlus, writes some text and a timestamp on it, then saves it to a stream. The stream is then written to a VFP blob field and displayed on a form.


 


CreateStreamOnHGlobal is used to create a stream which uses memory as the storage.


 


Download the COM server (save it somewhere on your machine and regsvr32 it) here.  


 


Let me know if you do any performance comparisons between file and stream I/O, or if you have other ideas on how to use streams with VFP.


 


See also: Draw text or graphics directly onto a JPG file


Create thumbnails of all your digital photos in a single table


Use Named Pipes to communicate between processes or machines


 


 


 


 


CLEAR


SET SAFETY OFF


SET CLASSLIB TO HOME()+“ffc\_gdiplus”


cFile=LOCFILE(“d:\kids.jpg”,“jpg”,“JPG file”)        && Some jpg file


DECLARE integer CreateStreamOnHGlobal IN ole32 integer hGlobal,integer fDeleteOnRelease, integer @ nSrm


nStm=0        && the stream is represented as an integer. It’s actually the IStream pointer, which does not inherit from IDispatch


IF .f.


*some test code to create, write, then read a stream


          LOCAL ox as “VFPStream.Cstream”


          ox=CREATEOBJECT(“VFPStream.Cstream”)


          CreateStreamOnHGlobal(0,1,@nStm)        && Create stream nStm


          nStm=INT(nStm)


          sFileContents=FILETOSTR(cFile)      && A VFP Variable which has the file contents


          nLen=LEN(sFileContents)


          ?“Write”,ox.WriteStream(nStm,“sFileContents”,nLen)       && Write the contents to the stream


          *ox.StreamSeek(nStm,0,0,0,0,0)   && Seek to beginning of stream


          buf=0 && Create a VFP Variable


          ?“Read”,ox.ReadStream(nStm,“buf”,0)      && Read the stream contents into var “buf”. nBytesToRead=0 means all of it


          ?“Stream length=”,ox.GetStreamLen(nStm)         && we can get the length


          ?“BufLen=”,LEN(buf)


          ox.ReleaseStream(nStm)


          RETURN


ENDIF


LOCAL oImage as gpimage OF HOME()+“ffc\_gdiplus”,oThumb as gpimage


LOCAL oGraphics as gpGraphics


LOCAL orect as gprectangle


oImage=CREATEOBJECT(“mygpimage”,cFile)        && create my subclass of gpimage, with some jpg


oGraphics=CREATEOBJECT(“gpgraphics”)


IF !oGraphics.CreateFromImage(oImage)


          THROW “CreateFromImage “+cFilename


ENDIF


*Now modify the JPG by writing some text on it


oGraphics.DrawStringA(“Hi There From Stream!”+TRANSFORM(DATETIME()),;


          CREATEOBJECT(“gpfont”,“Verdana”,40,1,3),;


          CREATEOBJECT(“gprectangle”,0,0,1000,500),;


          CREATEOBJECT(“gpstringformat”),;


          CREATEOBJECT(“gpsolidbrush”,0xffffffff)) && change the solidbrush color


 


CREATE CURSOR PicCursor (name c(10), pic blob) && A cursor into which we can store the blob


APPEND BLANK


IF .f.   && If we’re using a temp file, we’d write then read the file


          cOutFile=“c:\t.jpg”


          oImage.SaveToFile(cOutFile,“image/jpeg”)


          REPLACE pic WITH FILETOSTR(cOutFile)


ELSE   && write to a stream: avoid the file I/O


          LOCAL ox as “VFPStream.Cstream”


          ox=CREATEOBJECT(“VFPStream.Cstream”)


          nStm=ox.CreateStream(1)  && Create stream nStm


          IF !oImage.SaveToStream(nStm,“image/jpeg”)    && Call the new method to save new jpg to stream


                   ?“Err saving stream”


          ENDIF


          ox.ReadStream(nStm,“pic”,0)        && Read stream into blob field directly


          ox.ReleaseStream(nStm)


ENDIF


*Now create a form and display the modified blob, which was never written to disk


PUBLIC oForm as Form


oForm=CREATEOBJECT(“form”)


oForm.Left=250


oForm.AddObject(“img”,“image”)


oForm.Visible=1


oForm.img.Visible=1


oForm.img.width=oForm.Width


oForm.img.height = oForm.Height


oForm.img.Stretch= 1


oForm.img.PictureVal=pic    && use the blob field directly


 


 


DEFINE CLASS MyGpImage as gpimage OF HOME()+“ffc\_gdiplus”


          *This method is just a copy of SaveToFile with a minor mod to save to stream


          PROCEDURE SaveToStream(nStream as Integer,tvCLSIDEncoder, rvEncoderParams)


                   local lqCLSIDEncoder


                   * Encoder may be varbinary, or string


                   do case


                   case vartype(m.tvCLSIDEncoder)==‘Q’


                             lqCLSIDEncoder = m.tvCLSIDEncoder


                   case vartype(m.tvCLSIDEncoder)==‘C’


                             if left(m.tvCLSIDEncoder,1)==‘{‘


                                      lqCLSIDEncoder = This.StringToGUID( m.tvCLSIDEncoder)


                             else


                                      lqCLSIDEncoder = This.GetEncoderCLSID(m.tvCLSIDEncoder)


                             endif


                   otherwise


                             error 11


                   endcase


 


                   declare integer GlobalFree in kernel32.dll integer nHandle


                   local lnEncoderParamsPtr


                   do case


                   case type(“rvEncoderParams[1]”)!=‘U’


                             lnEncoderParamsPtr = This.getEncoderParamsFromArray( m.lqCLSIDEncoder, @rvEncoderParams )


                   case vartype(m.rvEncoderParams)==‘C’


                             lnEncoderParamsPtr = This.getEncoderParamsFromString( m.lqCLSIDEncoder, @rvEncoderParams )


                   case vartype(m.rvEncoderParams)==‘L’ and !m.rvEncoderParams


                             lnEncoderParamsPtr = 0


                   otherwise


                             error 11        && function argument


                             return .F.


                   endcase


 


                   DECLARE integer GdipSaveImageToStream IN gdiplus integer nImage,integer nStream,string qEncoder, integer  nEncoderParamsPtr


                   This.gdipStatus = GdipSaveImageToStream  ( ;


                             This.gdipHandle ;


                   ,         nStream ;


                   ,         m.lqCLSIDEncoder ;


                   ,         m.lnEncoderParamsPtr )


                   if m.lnEncoderParamsPtr!=0


                             GlobalFree(m.lnEncoderParamsPtr)


                   endif


 


          return 0 == This.gdipStatus


 


ENDDEFINE


 


 


 


 


 

Comments (8)

  1. Craig Boyd says:

    Awesome Calvin! I kept picking images with white backgrounds at first and couldn’t see the text. But then I looked at your code a little closer and saw that the text color was white… changed that and it worked like a charm. Really cool idea… thanks for sharing.

  2. Timo Zuidema says:

    Hi Calvin, great idea to use streams. It’s usefull to be able to access them via your dll.

    I did some performance testing with performance counters; streams in this case aren’t faster though. Using SaveToFile or SaveToStream both take about 55 ms on my machine, with a small bitmap. I guess the most time is in determining the data streaming an in-memory bitmap to a file format.

    Doing raw comparison of writing data (using your testcode with a small bitmap) shows a small advantage for streams.

  3. Cesar Chalom says:

    This is very cool indeed.

    Can you provide a better explanation when you say "IStream doesn’t inherit from IDispatch, so you can’t use Automation directly." ???

    From what I understood, you created 5 methods:

    ReadStream(nStm,"buf",0)

    GetStreamLen(nStm)

    WriteStream(nStm,"sFileContents",nLen)ReleaseStream(nStm)

    Another question:

    How should I use the StreamSeek function?

    ox.StreamSeek(nStm,0,0,0,0,0)

    Streams provide some great performance improvements in some cases, it would be terrific if you could write some more words on this subject.

    Thanks

    Cesar

  4. rstrahl says:

    Cesar, IStream does not implement IDispatch and so you can’t do a CreateObject() on it or pass it back to VFP directly because VFP COM objects work through IDispatch. Calvin’s class wrappers it so you can.

    If I remember correctly the ADO classes also have an IStream implementation that you can use from VFP which is meant to be used for binary Image data from a database. That stream object should work for anything that expects an IStream interface.

  5. Cesar Chalom says:

    Since the latest version, GdiPlusX offers many different ways to manipulate images with no disk access….

  6. VFP IMAGING says:

    Since the latest version, GdiPlusX offers many different ways to manipulate images with no disk access….

  7. that is wonderful and actually i see applications of that in gdiplus.

    I ask you if you can download with a stream a web page and convert it silently to an  image (jpg,bmp,tiff,gif) with this method and what a code must apply.

    thank you very much