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