I asked about this issue at the appathon and they were able to find a solution. As suspected, it was the structure packing that were the culprit. Here is the working code for x64:
[Flags]
public enum STGM : int
{
DIRECT = 0x00000000,
TRANSACTED = 0x00010000,
SIMPLE = 0x08000000,
READ = 0x00000000,
WRITE = 0x00000001,
READWRITE = 0x00000002,
SHARE_DENY_NONE = 0x00000040,
SHARE_DENY_READ = 0x00000030,
SHARE_DENY_WRITE = 0x00000020,
SHARE_EXCLUSIVE = 0x00000010,
PRIORITY = 0x00040000,
DELETEONRELEASE = 0x04000000,
NOSCRATCH = 0x00100000,
CREATE = 0x00001000,
CONVERT = 0x00020000,
FAILIFTHERE = 0x00000000,
NOSNAPSHOT = 0x00200000,
DIRECT_SWMR = 0x00400000,
}
enum ulKind : uint
{
PRSPEC_LPWSTR = 0,
PRSPEC_PROPID = 1
}
enum SumInfoProperty : uint
{
PIDSI_TITLE = 0x00000002,
PIDSI_SUBJECT = 0x00000003,
PIDSI_AUTHOR = 0x00000004,
PIDSI_KEYWORDS = 0x00000005,
PIDSI_COMMENTS = 0x00000006,
PIDSI_TEMPLATE = 0x00000007,
PIDSI_LASTAUTHOR = 0x00000008,
PIDSI_REVNUMBER = 0x00000009,
PIDSI_EDITTIME = 0x0000000A,
PIDSI_LASTPRINTED = 0x0000000B,
PIDSI_CREATE_DTM = 0x0000000C,
PIDSI_LASTSAVE_DTM = 0x0000000D,
PIDSI_PAGECOUNT = 0x0000000E,
PIDSI_WORDCOUNT = 0x0000000F,
PIDSI_CHARCOUNT = 0x00000010,
PIDSI_THUMBNAIL = 0x00000011,
PIDSI_APPNAME = 0x00000012,
PIDSI_SECURITY = 0x00000013
}
public enum VARTYPE : short
{
VT_BSTR = 8,
VT_FILETIME = 0x40,
VT_LPSTR = 30,
VT_CF = 71
}
[StructLayout(LayoutKind.Explicit)]
public struct PROPVARIANTunion
{
[FieldOffset(0)]
public sbyte cVal;
[FieldOffset(0)]
public byte bVal;
[FieldOffset(0)]
public short iVal;
[FieldOffset(0)]
public ushort uiVal;
[FieldOffset(0)]
public int lVal;
[FieldOffset(0)]
public uint ulVal;
[FieldOffset(0)]
public int intVal;
[FieldOffset(0)]
public uint uintVal;
[FieldOffset(0)]
public long hVal;
[FieldOffset(0)]
public ulong uhVal;
[FieldOffset(0)]
public float fltVal;
[FieldOffset(0)]
public double dblVal;
[FieldOffset(0)]
public short boolVal;
[FieldOffset(0)]
public int scode;
[FieldOffset(0)]
public long cyVal;
[FieldOffset(0)]
public double date;
[FieldOffset(0)]
public long filetime;
[FieldOffset(0)]
public IntPtr bstrVal;
[FieldOffset(0)]
public IntPtr pszVal;
[FieldOffset(0)]
public IntPtr pwszVal;
[FieldOffset(0)]
public IntPtr punkVal;
[FieldOffset(0)]
public IntPtr pdispVal;
}
struct PACKEDMETA
{
public ushort mm, xExt, yExt, reserved;
}
[DllImport("ole32.dll")]
static extern int StgOpenStorage(
[MarshalAs(UnmanagedType.LPWStr)]string pwcsName, IStorage pstgPriority,
int grfMode, IntPtr snbExclude, uint reserved, out IStorage ppstgOpen);
[DllImport("ole32.dll")]
static extern int StgCreatePropSetStg(IStorage pStorage, uint reserved,
out IPropertySetStorage ppPropSetStg);
[DllImport("ole32.dll")]
private extern static int PropVariantClear(ref PROPVARIANT pvar);
[ComImport]
[Guid("00000138-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertyStorage
{
[PreserveSig]
int ReadMultiple(uint cpspec,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] [In] PropertySpec[] rgpspec,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] [Out] PropertyVariant[] rgpropvar);
[PreserveSig]
void WriteMultiple(uint cpspec,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] [In] PropertySpec[] rgpspec,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] [In] PropertyVariant[] rgpropvar,
uint propidNameFirst);
[PreserveSig]
uint DeleteMultiple(uint cpspec,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] [In] PropertySpec[] rgpspec);
[PreserveSig]
uint ReadPropertyNames(uint cpropid,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] [In] uint[] rgpropid,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 0)] [Out] string[] rglpwstrName);
[PreserveSig]
uint NotDeclared1();
[PreserveSig]
uint NotDeclared2();
[PreserveSig]
uint Commit(uint grfCommitFlags);
[PreserveSig]
uint NotDeclared3();
[PreserveSig]
uint Enum(out IEnumSTATPROPSTG ppenum);
}
[ComImport]
[Guid("0000013A-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IPropertySetStorage
{
[PreserveSig]
uint Create(ref Guid rfmtid, ref Guid pclsid, uint grfFlags, STGM grfMode, out IPropertyStorage ppprstg);
[PreserveSig]
uint Open(ref Guid rfmtid, STGM grfMode, out IPropertyStorage ppprstg);
[PreserveSig]
uint NotDeclared3();
[PreserveSig]
uint Enum(out IEnumSTATPROPSETSTG ppenum);
}
public enum PropertySpecKind
{
Lpwstr,
PropId
}
[StructLayout(LayoutKind.Sequential)]
public struct PropertySpec
{
public PropertySpecKind kind;
public PropertySpecData data;
}
[StructLayout(LayoutKind.Explicit)]
public struct PropertySpecData
{
[FieldOffset(0)]
public uint propertyId;
[FieldOffset(0)]
public IntPtr name;
}
public struct PropertyVariant
{
public VARTYPE vt;
public ushort wReserved1;
public ushort wReserved2;
public ushort wReserved3;
public PROPVARIANTunion unionmember;
}
static public Bitmap GetMaxPreviewBitmapFromFile(string path)
{
var FMTID_SummaryInformation = new Guid("{F29F85E0-4FF9-1068-AB91-08002B27B3D9}");
Bitmap bitmap = null;
IStorage Is;
if (StgOpenStorage(path, null, (int)(STGM.SHARE_EXCLUSIVE | STGM.READWRITE), IntPtr.Zero, 0, out Is) == 0 && Is != null)
{
IPropertySetStorage pss;
if (StgCreatePropSetStg(Is, 0, out pss) == 0)
{
IPropertyStorage ps;
pss.Open(ref FMTID_SummaryInformation, (STGM.SHARE_EXCLUSIVE | STGM.READ), out ps);
if (ps != null)
{
var propSpec = new PropertySpec[1];
var propVariant = new PropertyVariant[1];
propSpec[0].kind = PropertySpecKind.PropId;
propSpec[0].data.propertyId = (uint)SumInfoProperty.PIDSI_THUMBNAIL;
System.UInt32 n = 1;
ps.ReadMultiple(n, propSpec, propVariant);
var clipData =
(CLIPDATA)Marshal.PtrToStructure(propVariant[0].unionmember.pszVal, typeof(CLIPDATA));
var pb = clipData.pClipData;
pb += sizeof(uint);
var packedMeta = (PACKEDMETA)Marshal.PtrToStructure(pb, typeof(PACKEDMETA));
pb += Marshal.SizeOf(packedMeta);
var magicNumber = 3 * 29;
pb += magicNumber;
var pformat = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
int bitsPerPixel = ((int)pformat & 0xff00) >> 8;
int bytesPerPixel = (bitsPerPixel + 7) / 8;
int stride = 4 * ((packedMeta.xExt * bytesPerPixel + 3) / 4);
unsafe
{
byte* ptr = (byte*)pb;
for (int y = 0; y < packedMeta.yExt; y++)
for (int x = 0; x < packedMeta.xExt; x++)
{
var i = (x * 3) + y * stride;
var r = ptr[i];
var g = ptr[i + 1];
var b = ptr[i + 2];
ptr[i] = b;
ptr[i + 1] = r;
ptr[i + 2] = g;
}
bitmap = new Bitmap(packedMeta.xExt, packedMeta.yExt, stride, pformat, (IntPtr)ptr);
bitmap.RotateFlip(RotateFlipType.Rotate180FlipX);
//bmap.Save("test1.jpg");
}
//PropVariantClear(ref propVariant[0]);
Marshal.FinalReleaseComObject(ps);
ps = null;
}
else
{
Console.WriteLine("Could not open property storage");
}
Marshal.FinalReleaseComObject(pss);
pss = null;
}
else
{
Console.WriteLine("Could not create property set storage");
}
Marshal.FinalReleaseComObject(Is);
Is = null;
}
else
{
Console.WriteLine("File does not contain a structured storage");
}
GC.Collect();
return bitmap;
}
So a big thanks to the Autodesk guys that followed up on my problem!
haavard,
This is TOTALY awesome. Thanks for following this through.
For anyone wanting to use this but unsure of how to proceed, I’ve compiled the code into a dotnet dll for everyone to use. (I think you need to install the visual studio SDK too) {EDIT} I’ve now included this in the zip file so you dont have to download the SDK, as well as the source code from my c# solution
to get the thumb just do this:
dotnet.loadassembly "<path to dll>/MaxThumbnail.dll"
mt = dotnetobject "structuredStorage.maxthumbnail"
mt.GetMaxPreviewBitmapFromFile <maxfilepath>
I have a small request that could really make my day! now that you’ve got access to the structured storage, would it be possible to write a method that would allow someone to get custom file properties without opening the file? I have a kind of postit system where people can flag notes and changes into maxfiles. I am using calum mclellan’s structured storage dll for this at the moment, (thanks denisT for putting that on the map) but i like the potential of this one MUCH better
Thanks again for your work, its just fantastic.
Sounds awesome, great work guys…
But without the Visual Studio SDK…
– Runtime error: dotNet runtime exception: Could not load file or assembly ‘Microsoft.VisualStudio.OLE.Interop, Version=7.1.40304.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a’ or one of its dependencies. The system cannot find the file specified.
Do I really have to install the Visual Studio SDK on everyone’s machine or is it just one missing DLL we need?
Hello Dave,
The latter. You could install the SDK on your machine and grab the extra dll to deploy. I’ll add it the the original zip file when I get in tomorrow.
This should be possible. I think the smartest way to approach it is to use the OLE.interop.dll as much as possible and substitute the non-working parts on x64 assemblies with custom stuff. Kevin Vandecar, the guy that resolved the thumbnail issue, tipped me of a library that has a bunch of structured storage stuff for both x32 and x64. So if the OLE.interop.dll fails, just look up the structs/enums signatures from this tool to see how it is done, or reference the tool directly.
How do you approach it today? Maybe post some code or an example file and so we can take a look.
Edit:
The formentioned library https://www.nuget.org/packages/CodeFluentRuntimeClient
Hi Haavard,
I began looking at this many years ago here via my blog research, using the dsofile.dll from Microsoft. It is something that allowed developers access to structured storage on word files I believe. I used this along with stdOle.dll and Microsoft.VisualBasic.Compatibility.dll to grab the thumbnail and convert to a dotnet bitmap. Here is the wrapper class, back when I did everything in VB
public class MaxFileInfo
{
// Fields
private FileInfo _imageinfo;
private OleDocumentProperties _oDocument;
// Methods
public MaxFileInfo(string MaxFile)
{
try
{
this._imageinfo = MyProject.Computer.FileSystem.GetFileInfo(MaxFile);
}
catch (Exception exception1)
{
ProjectData.SetProjectError(exception1);
Exception innerException = exception1;
throw new Exception("The File Could not be opened!", innerException);
}
}
private Image MaxOleCOMInterop(ref OleDocumentProperties _oDocument)
{
Image image;
try
{
Image image2 = Support.IPictureToImage(RuntimeHelpers.GetObjectValue(_oDocument.get_SummaryProperties().get_Thumbnail()));
_oDocument.Close(false);
_oDocument = null;
image = image2;
}
catch (Exception exception1)
{
ProjectData.SetProjectError(exception1);
Exception innerException = exception1;
throw new Exception("Error in MaxOleCOMInterop function", innerException);
}
return image;
}
public Image Thumbnail()
{
Image image;
try
{
this._oDocument = new OleDocumentPropertiesClass();
this._oDocument.Open(this.FullFileName, false, 2);
Bitmap bitmap = new Bitmap(this.MaxOleCOMInterop(ref this._oDocument));
image = bitmap;
}
catch (Exception exception1)
{
ProjectData.SetProjectError(exception1);
Exception innerException = exception1;
throw new Exception("Error in Thumbnail Function", innerException);
}
return image;
}
public Image Thumbnail(bool ToClipboard)
{
Image image;
try
{
this._oDocument = new OleDocumentPropertiesClass();
this._oDocument.Open(this.FullFileName, false, 2);
Bitmap bitmap = new Bitmap(this.MaxOleCOMInterop(ref this._oDocument));
if (ToClipboard)
{
MyProject.Computer.Clipboard.SetImage(bitmap);
bitmap.Dispose();
bitmap = null;
return null;
}
image = bitmap;
}
catch (Exception exception1)
{
ProjectData.SetProjectError(exception1);
Exception innerException = exception1;
throw new Exception("Error in Thumbnail Function", innerException);
}
return image;
}
// Properties
public DateTime Created
{
get
{
return this._imageinfo.CreationTimeUtc;
}
}
public string FileSize
{
get
{
long num2 = this._imageinfo.Length / 0x400L;
if (num2 >= 0x3e8L)
{
return (Math.Round((double) (((double) num2) / 1024.0), 2).ToString() + " Mb");
}
return (num2.ToString() + " Kb");
}
}
public string FullFileName
{
get
{
return Path.Combine(this.Location, this.Name);
}
}
public DateTime LastSave
{
get
{
return this._imageinfo.LastWriteTimeUtc;
}
}
public string Location
{
get
{
return this._imageinfo.DirectoryName;
}
}
public string Name
{
get
{
return this._imageinfo.Name;
}
}
}
For using the file properties, I’m sure this method could have been developed further, however thanks to a DenisT tipoff, I use the structured storage dll from here:: http://calummclellan.com/code.aspx as it is much simpler than my solution.
dotnet.loadassembly (pathconfig.appendpath (getdir #userscripts) "assemblies\StructuredStorage.dll")
maxstr = maxfilepath+maxfilename
--to get custom data without opening the file
filePropData = dotnetobject "CalumMcLellan.StructuredStorage.PropertySets" maxstr true
if filePropData.contains "custom" then
if filePropData.item["custom"].Contains "Data" then
(
try(filePropData.item["custom"].item["Data"].value)catch("")
)
else ""
else ""
--to add custom data
d = dotnetobject "CalumMcLellan.StructuredStorage.PropertySets" maxstr true
d.item["custom"].item["Data"].value
d.add "Data" "Woop"
Anyway, The stuff you are doing is kick ass. I thought I’d share some of my history in this quest as I stumbled through code that was way over my head
I would like to be able to do the following without opening the max file…
Get a List of:
Object Properties
Object Transforms
All Layers
All Materials
All render Settings
All environment Settings
All Effect Settings
Would make me very happy
Pete, where does the OLE dll go?
I’ve tried loading it but to no avail…
dotnet.loadassembly “c://Temp//Microsoft.VisualStudio.OLE.Interop.dll”
dotnet.loadassembly “c://Temp//MaxThumbnail.dll”
mt = dotnetobject “structuredStorage.maxthumbnail”
mt.GetMaxPreviewBitmapFromFile “C:\Temp\AutoBackup01.max”
This is awesome! Thanks Pete and Haavard!
Will try as soon as I can, I have just the project for this!
update: Works great, I’m very happy!
Thanks,
-Johan
Pete,
I took your idea and tried to implement a simple example. A user can add messages to a max file by writing to a specified IStream located in the IStorage of the file. Knowing the name of the custom stream and the layout of the contents stored in it, it is trivial to read out the content of it. So say you want to store a message like this:
struct MessageData
{
string Author;
string Message;
DateTime Time;
}
in order to consistently be able to read out messages from a stream, each stored message should contain some data about the message itself, in this case the length of the message and the length of the author’s name. So we need to convert the MessageData struct to a “streamable” struct like this:
struct StreamData
{
int MessageSize;
int AuthorSize;
long Time;
byte[] Author;
byte[] Message;
}
Then we can open a stream and write it directly into the stream using a custom method like this:
var message = new MessageData("Håvard", "My first message");
WriteToFile(path, message);
Inspecting the file using a structured storage viewer we can find our custom stream, named “CustomMessageData”, and our messages. Each message is seen by some unreadable characters which is the sizes of the message and author as well as the time represented as ticks. Then comes the authors name and is immediately followed by the message. In this example three messages are stored:
Since we know the structure of the stored messages we can open the stream and start extracting the data:
var messageSizeArr = new byte[sizeof(int)];
dataStream.Read(messageSizeArr, (uint)messageSizeArr.Length, out dataRead);
var authorSizeArr = new byte[sizeof(int)];
dataStream.Read(authorSizeArr, (uint)authorSizeArr.Length, out dataRead);
var timeArr = new byte[sizeof(long)];
dataStream.Read(timeArr, (uint)timeArr.Length, out dataRead);
var authorArr = new byte[BitConverter.ToInt32(authorSizeArr, 0)];
dataStream.Read(authorArr, (uint)authorArr.Length, out dataRead);
var messageArr = new byte[BitConverter.ToInt32(messageSizeArr, 0)];
dataStream.Read(messageArr, (uint)messageArr.Length, out dataRead);
var message = new MessageData();
message.Author = Encoding.Default.GetString(authorArr);
message.Message = Encoding.Default.GetString(messageArr);
message.Time = DateTime.FromBinary(BitConverter.ToInt64(timeArr, 0));
I don’t know if this is the right way to do it, but it’s a start! The info about structured storage in c# is very scattered and sparse, so it can be a bit frustrating at times.
Anyway, here is the sample class. It got two public methods and a public struct that represents the messages to be stored in the file.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using Microsoft.VisualStudio.OLE.Interop;
using STATSTG = Microsoft.VisualStudio.OLE.Interop.STATSTG;
using IStream = Microsoft.VisualStudio.OLE.Interop.IStream;
namespace blamespace
{
public static class CustomDataToFile
{
[DllImport("ole32.dll")]
static extern int StgOpenStorage(
[MarshalAs(UnmanagedType.LPWStr)]string pwcsName,
IStorage pstgPriority,
int grfMode,
IntPtr snbExclude,
uint reserved,
out IStorage ppstgOpen);
[Flags]
enum STGM : int
{
DIRECT = 0x00000000,
TRANSACTED = 0x00010000,
SIMPLE = 0x08000000,
READ = 0x00000000,
WRITE = 0x00000001,
READWRITE = 0x00000002,
SHARE_DENY_NONE = 0x00000040,
SHARE_DENY_READ = 0x00000030,
SHARE_DENY_WRITE = 0x00000020,
SHARE_EXCLUSIVE = 0x00000010,
PRIORITY = 0x00040000,
DELETEONRELEASE = 0x04000000,
NOSCRATCH = 0x00100000,
CREATE = 0x00001000,
CONVERT = 0x00020000,
FAILIFTHERE = 0x00000000,
NOSNAPSHOT = 0x00200000,
DIRECT_SWMR = 0x00400000,
}
enum StreamSeek: uint
{
Set,
Cur,
End
}
const string StreamName = "CustomMessageData";
public struct MessageData
{
public string Author;
public string Message;
public DateTime Time;
public MessageData(string author, string message)
{
Author = author;
Message = message;
Time = DateTime.Now;
}
public override string ToString()
{
return string.Format("{0} {1}: {2}", Time, Author, Message);
}
}
struct StreamData
{
public int MessageSize;
public int AuthorSize;
public long Time;
public byte[] Author;
public byte[] Message;
public StreamData(MessageData message)
{
Time = message.Time.Ticks;
Message = Encoding.Default.GetBytes(message.Message);
MessageSize = Encoding.Default.GetByteCount(message.Message);
Author = Encoding.Default.GetBytes(message.Author);
AuthorSize = Encoding.Default.GetByteCount(message.Author);
}
public byte[] WriteToByteArray()
{
var stream = new MemoryStream();
var writer = new BinaryWriter(stream);
writer.Write(MessageSize);
writer.Write(AuthorSize);
writer.Write(Time);
writer.Write(Author);
writer.Write(Message);
writer.Flush();
return stream.ToArray();
}
}
public static void WriteToFile(string filepath, MessageData message)
{
var streamData = new StreamData(message);
IStorage Is;
var dataStream = OpenStream(filepath, out Is);
var byteData = streamData.WriteToByteArray();
uint bytesWritten;
dataStream.Seek(new LARGE_INTEGER(), (uint)StreamSeek.End, new ULARGE_INTEGER[0]);
dataStream.Write(byteData, (uint)byteData.Length, out bytesWritten);
Is.Commit(0);
Marshal.FinalReleaseComObject(Is);
Marshal.FinalReleaseComObject(dataStream);
dataStream = null;
Is = null;
}
public static List<MessageData> GetMessages(string filepath)
{
var messages = new List<MessageData>();
IStorage Is;
var dataStream = OpenStream(filepath, out Is);
var stat = new STATSTG[1];
dataStream.Stat(stat, 0);
uint dataRead = 0, total = 0;
do
{
var messageSizeArr = new byte[sizeof(int)];
dataStream.Read(messageSizeArr, (uint)messageSizeArr.Length, out dataRead);
total += dataRead;
var authorSizeArr = new byte[sizeof(int)];
dataStream.Read(authorSizeArr, (uint)authorSizeArr.Length, out dataRead);
total += dataRead;
var timeArr = new byte[sizeof(long)];
dataStream.Read(timeArr, (uint)timeArr.Length, out dataRead);
total += dataRead;
var authorArr = new byte[BitConverter.ToInt32(authorSizeArr, 0)];
dataStream.Read(authorArr, (uint)authorArr.Length, out dataRead);
total += dataRead;
var messageArr = new byte[BitConverter.ToInt32(messageSizeArr, 0)];
dataStream.Read(messageArr, (uint)messageArr.Length, out dataRead);
total += dataRead;
var message = new MessageData();
message.Author = Encoding.Default.GetString(authorArr);
message.Message = Encoding.Default.GetString(messageArr);
message.Time = DateTime.FromBinary(BitConverter.ToInt64(timeArr, 0));
messages.Add(message);
} while (stat[0].cbSize.QuadPart > total);
Marshal.FinalReleaseComObject(Is);
Marshal.FinalReleaseComObject(dataStream);
dataStream = null;
Is = null;
return messages;
}
static IStream OpenStream(string filepath, out IStorage Is)
{
if (StgOpenStorage(filepath, null, (int)(STGM.SHARE_EXCLUSIVE | STGM.READWRITE), IntPtr.Zero, 0, out Is) == 0 && Is != null)
{
IEnumSTATSTG SSenum;
Is.EnumElements(0, IntPtr.Zero, 0, out SSenum);
var SSstruct = new STATSTG[1];
IStream dataStream;
uint numReturned;
bool exists = false;
do
{
SSenum.Next(1, SSstruct, out numReturned);
if (numReturned != 0)
{
//Console.WriteLine(SSstruct[0].pwcsName + " " + SSstruct[0].type + " " + SSstruct[0].cbSize.QuadPart);
if ((SSstruct[0].pwcsName != StreamName))
continue;
exists = true;
break;
}
} while (numReturned > 0);
if (!exists)
{
Is.CreateStream(
StreamName,
(uint)(STGM.CREATE | STGM.WRITE | STGM.DIRECT | STGM.SHARE_EXCLUSIVE),
0, 0,
out dataStream);
}
else
{
Is.OpenStream(
SSstruct[0].pwcsName,
IntPtr.Zero,
(uint)(STGM.SHARE_EXCLUSIVE | STGM.READWRITE),
0, out dataStream);
}
return dataStream;
}
return null;
}
}
}
Howdy Dave,
For that level, I wouldn’t look at doing it within the file properties. I’d be writing an xml with everything in on a filesave callback. ‘Id then write a c# class that implements XML LINQ to provide me a database querying system for the data.
Pete, where does the OLE dll go?
I would have thought it just needs to be in the same directory as the thumbnail class. Can any one else without the SDK installed clarify that?