An Ordeal of OLE
OLE(Object Linking and Embedding) is a critical technology by Microsoft to carry out its enterprise applications, based on COM it's also a quite old one. Despite of its importance, it doesn't seem to be so necessary to me as .NET is far more than enough. However in a recent task I have to touch on it.
The task is mainly about enabling the CAD exporting library to export embedded images in addition to linked images which is supported well by both AutoCAD and the Teigha library underlying our library. After a bit of search online it was discovered that image embedding in a similar manner to image linking were not supported or suggested. There are a few solutions/workarounds including
- using OLE that embodies a paint brush application that includes the image, which is a supported feature by AutoCAD for exchanging embedded images.
- leveraging AutoCAD Raster Design (ARD) that has introduced an IEMBED command to allow images to be embeddes as they can be linked; however ARD is not part of a normal version of AutoCAD and unlikely can be easily supported by the Teigha library that we are using (however the official AutoCAD secondary development may do)
- An uncofirmed solution proposed by a discussion thread that creates an AutoCAD entity that includes the image data and is able to externalise it to a temporary file to be referenced by a normal linked image. (needs a bit investigation into how to create an object like that)
Due to the limit of time and resources available, the first option was chosen for the time being, although the third may be proven the best as OLE is neither stable nor performant when being rendered in the AutoCAD. The development of this task involved a lot of investigation and experimentation and turned out to be purely based on a copy of online queries below,
http://www.dimcax.com/hust/showpost.php?p=18114&postcount=1
The final implemenation mostly matches what's described in the link. It mainly involves in the following steps
- obtain an COleDataSource object from the image file using certain APIs
- retrieve COleDataObject from COleDataSource
- create COleClientItem from COleDataObject
- put the COleClientItem on clipboard (to mimic the process of manual copy of image object to AutoCAD)
- enumerate the binary data items from the clipboard and pass them as a compound document to Teigha by calling setCompoundDocument on its OdDbOle2Frame object
The link above suggested creating a static OLE data object from COleDataSource would be fine, which I muddled through and got work with the code below,
1 // The method of creating a compound document for a static OLE object from a picture file 2 void CreateStaticOleCompoundDocument() 3 { 4 HANDLE hDibCopy = CreateDibFromBmp("c:\\temp\\garbage\\sample.bmp"); 5 6 COleDataSource src; 7 src.CacheGlobalData(CF_DIB, hDibCopy); 8 9 LPDATAOBJECT lpDataObject = (LPDATAOBJECT)src.GetInterface(&IID_IDataObject); 10 11 COleDataObject obj; 12 obj.Attach(lpDataObject); 13 14 COleDocument doc; 15 COleClientItem item(&doc); 16 item.CreateStaticFromData(&obj); 17 18 item.CopyToClipboard(); 19 20 COleDataObject objReceiver; 21 if (objReceiver.AttachClipboard()) 22 { 23 objReceiver.EnsureClipboardObject(); 24 25 objReceiver.BeginEnumFormats(); 26 FORMATETC format; 27 FILE *fp; 28 fopen_s(&fp, "c:\\temp\\garbage\\olestatic.bin", "wb"); 29 while (objReceiver.GetNextFormat(&format)) 30 { 31 HGLOBAL hmem = objReceiver.GetGlobalData(format.cfFormat); 32 int size = ::GlobalSize(hmem); 33 byte *pdata = (byte *)::GlobalLock(hmem); 34 35 fwrite(pdata, 1, size, fp); 36 37 ::GlobalUnlock(hmem); 38 } 39 40 fclose(fp); 41 } 42 43 obj.Detach(); 44 objReceiver.Release(); 45 }
The bin file will be fed to the Teigha through invocation on its setCompoundDocument
To make it work the image has to be converted to a DIB (Device Independent Bitmap) which is given by the code below as a simplified approach,
1 // references: 2 // http://www.codeguru.com/cpp/g-m/bitmap/article.php/c1693/Creating-a-DIB-section-from-a-BMP-file.htm 3 HGLOBAL CreateDibFromBmp(char *filename) 4 { 5 FILE *fp; 6 fopen_s(&fp, filename, "rb"); 7 fseek(fp, 0, SEEK_END); 8 int flen = ftell(fp); 9 fseek(fp, 0, SEEK_SET); 10 11 byte *buf = new byte[flen]; 12 13 fread(buf, 1, flen, fp); 14 15 // this simplified version just takes off the header of the BMP to turn it into DIB 16 HGLOBAL hmem = ::GlobalAlloc(GMEM_MOVEABLE, flen - 14); 17 18 byte *pdib = (byte*)::GlobalLock(hmem); 19 memcpy(pdib, buf+14, flen-14); 20 21 ::GlobalUnlock(hmem); 22 23 delete[] buf; 24 25 fclose(fp); 26 27 return hmem; 28 }
Unfortunately this solution however reasonable it looks doesn't work out well at all, the first and foremost issue it has is the recent versions of AutoCAD show it as a white placeholder for image and upon editing the placeholder object an error message is popped up saying it's a static ActiveX object and is unable to be activated. And it is truly a static ActiveX as is clearly indicated by the subroutine that is used to create a COleClientItem. And that subroutine cannot be replaced by CreateFromData() simply because the way the data object is created from the picture file doesn't allow that.
So I had to find another way which apparently as suggested by the error message has to be creating a non-static OLE object. Hours of effort with alternate attempts for other possibilities was committed and the final solution was inspired by a link http://support.microsoft.com/kb/220844 but not in the same flavour as I couldn't figure out a way to get a simple graft of the Win32 approach to the OLE objects manipulation that follows to work. Below is my current approach,
1 // The method of creating a compound document for an OLE from a picture file 2 void CreateNonStaticCompoundDocument() 3 { 4 *(&afxCurrentAppName) = L"testapp"; 5 6 COleDocument doc; 7 COleClientItem item(&doc); 8 item.CreateFromFile(L"C:\\temp\\garbage\\sample.bmp"); 9 10 item.CopyToClipboard(); 11 12 COleDataObject objReceiver; 13 if (objReceiver.AttachClipboard()) 14 { 15 objReceiver.EnsureClipboardObject(); 16 17 objReceiver.BeginEnumFormats(); 18 FORMATETC format; 19 FILE *fp; 20 fopen_s(&fp, "c:\\temp\\garbage\\olenonstatic.bin", "wb"); 21 while (objReceiver.GetNextFormat(&format)) 22 { 23 HGLOBAL hmem = objReceiver.GetGlobalData(format.cfFormat); 24 int size = ::GlobalSize(hmem); 25 byte *pdata = (byte *)::GlobalLock(hmem); 26 27 fwrite(pdata, 1, size, fp); 28 29 ::GlobalUnlock(hmem); 30 } 31 32 fclose(fp); 33 } 34 35 objReceiver.Release(); 36 }
The above approach brings me much closer to the goal. Although it still just creates a white box with no sign of image but AutoCAD doesn't complain when the user is trying to open the OLE and it does show the right image in the paintbrush application.
Note both of the above approaches preserve the data fine as one can copy past image from AutoCAD.
It can be obviously see how awkward OLE/COM technologies are (I suppose I might have missed some COM object finalisation). They may provide some runtime performance benefit and extensibility, however it's far less productive than .NET and the performance is not that much at all if .NET and its interoperability are optimally used. And I see products using this kind of technology as well as MFC/ATL such as AutoCAD etc. for most part of it including the architecture just because they've long been used rather than they are necessary.