一般在XP文件夹里面,特别是图片和视频文件夹里有一个文件—Thumbs.db文件。这个文件是XP用来缓存图片和影音文件的缩略图的,有了这个文件,XP在打开保存大量图片文件的文件夹的时候,显示速度会明显比没有Thumbs.db文件的文件夹快—因为后者需要实时生成缩略图。
最近在做一个自己的图片管理程序,需要快速生成缩略图,就想到复用这个文件,这样我的程序可以无缝地继承视窗系统的资源管理器功能。因为Thumbs.db文件的文件结构和访问API没有被公开,所以在Google查了一些资料,发现Thumbs.db文件采用的是结构化存储文件(Structured Storage File)结构,这个文件在COM时代非常的流行,不知道为什么在.Net里面,微软把这个文件结构扔掉了。
结构化存储概述
结构化存储文件结构说白了就是一个保存在文件里面的文件系统,就是说在一个结构化存储文件里面,保存有“文件夹”信息,也保存有“文件”信息和其内容。例如,我们熟悉的Winrar的打包多个文件的过程,就可以使用结构化存储文件结构来保存(当然啦,我没有Winrar的源代码,不是说Winrar就是这样实现打包的啊)。
使用结构化存储文件的一个好处是,使得更新文件内容非常方便。 举个例子,比如我们日常使用的Word吧,当我们编辑一个文件的时候,如果Word采用的顺序存储结构—文件内容是按照内容的逻辑结构顺序存储在磁盘里的,即在硬盘里,第一页保存在第二页的前面。顺序存储方式的问题在于,它使得修改Word文档的时候,会变得非常麻烦。假设你的文档有几千页,当你增删第一页的内容的时候,顺序存储的方式就要求你必须移动后面几千页内容—可以想象到这个过程有多慢了。 如果我们将Word文档看作一个小的文件系统的话,那么对于文档中的每一页我们可以看成是一个“文件夹”,然后所有的文字段落可以看成是“文件夹”里面的文件。如果文档里面插入了图片的话,可以另外在“文件夹”里创建一个小的文件夹—“图片”文件夹,而在使用到这个图片的位置上加入一个快捷方式链接到每一页的内容里就可以了。下图演示了前一段描述的概念(注意-我没有看到Office的源代码,上述内容只不过是我的一个小猜想而已):
结构化存储文件的COM接口
刚才讲完了概念,在COM中,IStorage接口就相当于结构化存储文件中的 “文件夹”,而IStream接口就是“文件”啦。下面就是IStorage的接口:
MIDL_INTERFACE("0000000b-0000-0000-C000-000000000046") IStorage : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE CreateStream( /* [string][in] */ __RPC__in const OLECHAR *pwcsName, /* [in] */ DWORD grfMode, /* [in] */ DWORD reserved1, /* [in] */ DWORD reserved2, /* [out] */ __RPC__deref_out_opt IStream **ppstm) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE OpenStream( /* [string][in] */ const OLECHAR *pwcsName, /* [unique][in] */ void *reserved1, /* [in] */ DWORD grfMode, /* [in] */ DWORD reserved2, /* [out] */ IStream **ppstm) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateStorage( /* [string][in] */ __RPC__in const OLECHAR *pwcsName, /* [in] */ DWORD grfMode, /* [in] */ DWORD reserved1, /* [in] */ DWORD reserved2, /* [out] */ __RPC__deref_out_opt IStorage **ppstg) = 0;
virtual HRESULT STDMETHODCALLTYPE OpenStorage( /* [string][unique][in] */ __RPC__in_opt const OLECHAR *pwcsName, /* [unique][in] */ __RPC__in_opt IStorage *pstgPriority, /* [in] */ DWORD grfMode, /* [unique][in] */ __RPC__deref_opt_in_opt SNB snbExclude, /* [in] */ DWORD reserved, /* [out] */ __RPC__deref_out_opt IStorage **ppstg) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE CopyTo( /* [in] */ DWORD ciidExclude, /* [size_is][unique][in] */ const IID *rgiidExclude, /* [unique][in] */ SNB snbExclude, /* [unique][in] */ IStorage *pstgDest) = 0;
virtual HRESULT STDMETHODCALLTYPE MoveElementTo( /* [string][in] */ __RPC__in const OLECHAR *pwcsName, /* [unique][in] */ __RPC__in_opt IStorage *pstgDest, /* [string][in] */ __RPC__in const OLECHAR *pwcsNewName, /* [in] */ DWORD grfFlags) = 0;
virtual HRESULT STDMETHODCALLTYPE Commit( /* [in] */ DWORD grfCommitFlags) = 0;
virtual HRESULT STDMETHODCALLTYPE Revert( void) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE EnumElements( /* [in] */ DWORD reserved1, /* [size_is][unique][in] */ void *reserved2, /* [in] */ DWORD reserved3, /* [out] */ IEnumSTATSTG **ppenum) = 0;
virtual HRESULT STDMETHODCALLTYPE DestroyElement( /* [string][in] */ __RPC__in const OLECHAR *pwcsName) = 0;
virtual HRESULT STDMETHODCALLTYPE RenameElement( /* [string][in] */ __RPC__in const OLECHAR *pwcsOldName, /* [string][in] */ __RPC__in const OLECHAR *pwcsNewName) = 0;
virtual HRESULT STDMETHODCALLTYPE SetElementTimes( /* [string][unique][in] */ __RPC__in_opt const OLECHAR *pwcsName, /* [unique][in] */ __RPC__in_opt const FILETIME *pctime, /* [unique][in] */ __RPC__in_opt const FILETIME *patime, /* [unique][in] */ __RPC__in_opt const FILETIME *pmtime) = 0;
virtual HRESULT STDMETHODCALLTYPE SetClass( /* [in] */ __RPC__in REFCLSID clsid) = 0;
virtual HRESULT STDMETHODCALLTYPE SetStateBits( /* [in] */ DWORD grfStateBits, /* [in] */ DWORD grfMask) = 0;
virtual HRESULT STDMETHODCALLTYPE Stat( /* [out] */ __RPC__out STATSTG *pstatstg, /* [in] */ DWORD grfStatFlag) = 0; }; |
注意上面的定义里面,[Create/Open]Stream就是创建和打开“文件”的方式,而 [Create/Open]Storage就是创建和打开“文件夹”的方式—“文件夹”里面不是可以包含其他的文件夹吗?下面是IStream接口的定义:
MIDL_INTERFACE("0000000c-0000-0000-C000-000000000046") IStream : public ISequentialStream { public: virtual /* [local] */ HRESULT STDMETHODCALLTYPE Seek( /* [in] */ LARGE_INTEGER dlibMove, /* [in] */ DWORD dwOrigin, /* [out] */ ULARGE_INTEGER *plibNewPosition) = 0;
virtual HRESULT STDMETHODCALLTYPE SetSize( /* [in] */ ULARGE_INTEGER libNewSize) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE CopyTo( /* [unique][in] */ IStream *pstm, /* [in] */ ULARGE_INTEGER cb, /* [out] */ ULARGE_INTEGER *pcbRead, /* [out] */ ULARGE_INTEGER *pcbWritten) = 0;
virtual HRESULT STDMETHODCALLTYPE Commit( /* [in] */ DWORD grfCommitFlags) = 0;
virtual HRESULT STDMETHODCALLTYPE Revert( void) = 0;
virtual HRESULT STDMETHODCALLTYPE LockRegion( /* [in] */ ULARGE_INTEGER libOffset, /* [in] */ ULARGE_INTEGER cb, /* [in] */ DWORD dwLockType) = 0;
virtual HRESULT STDMETHODCALLTYPE UnlockRegion( /* [in] */ ULARGE_INTEGER libOffset, /* [in] */ ULARGE_INTEGER cb, /* [in] */ DWORD dwLockType) = 0;
virtual HRESULT STDMETHODCALLTYPE Stat( /* [out] */ __RPC__out STATSTG *pstatstg, /* [in] */ DWORD grfStatFlag) = 0;
virtual HRESULT STDMETHODCALLTYPE Clone( /* [out] */ __RPC__deref_out_opt IStream **ppstm) = 0; }; |
IStream的用法跟.Net里面的System.IO.Stream的用法类似,其中IStream::Commit函数的作用就是将内存中的修改保存到硬盘中。
一般来说,结构化存储文件的“文件夹”IStorage里面都会有一个IStream保存该“文件夹”的目录—即说明“文件夹”里面有哪些文件。
Thumbs.db文件的文件描述
既然我们已经知道IStorage和IStream的概念和用法了,回过头来看看Thumbs.db文件,Thumbs.db文件中有一个名称为“Catalog”的 IStream保存了整个Thumbs.db文件里面缓存的缩略图的文件名列表。
它包含两段内容,第一段内容的结构叫做CatalogHeader(当然这也是我们随便取的—因为微软并没有公开Thumbs.db的API),保存了所有缩略图的大小,是32x32的,还是64x64之类的,另外还有一个重要的变量保存了缩略图文件的个数。下面是这个数据结构的声明,因为没有对应的COM API,所以我们直接在C#中声明了。
[Interop.StructLayout(Interop.LayoutKind.Sequential)] public struct CatalogHeader { public short Reserved1;
public short Reserved2;
public int ThumbCount;
public int ThumbWidth;
public int ThumbHeight; } |
注意声明上面的StructLayout属性,由于.Net是即时编译的系统,在编译的过程当中,通常情况下,JIT会根据当前系统内存和CPU的架构,为结构生成最优的内存布局以便在访问结构体的时候能够达到最快的速度—因此JIT可能会调整结构的一些成员在内存布局的顺序。 由于我们是在读取COM生成的数据,C++编译器可没有做到这一点,所以LayoutKind.Sequential告诉JIT编译器,不要随意更改结构成员在内存中的布局。而ReveredX属性的存在是因为这个结构是我们猜的结构,前两个属性没猜出来。
第二段内容就是缩略图的“文件名”信息了,除了名字以外,还保存了缩略图生成的时间—以便同名文件更新的时候可以生成新的缩略图,还有一个莫名其妙的 ItemId—估计是用来提高检索缩略图速度的,当然还有两个没猜出来的属性。下面是这个成员的结构定义:
[Interop.StructLayout(Interop.LayoutKind.Sequential)] public struct CatalogItem { public int Reserved1;
private int m_ItemId; public int ItemId { get { return m_ItemId; } set { m_ItemId = value; BuildItemIdString(m_ItemId); } }
public DateTime Modified;
public string FileName;
public short Reserved2;
// 自己添加的新域 public string ItemIdString { get; private set; }
private void BuildItemIdString(int itemId) { var temp = itemId.ToString(); var buffer = new char[temp.Length]; for (int i = 0; i < temp.Length; ++i) buffer[i] = temp[temp.Length - i - 1];
ItemIdString = new string(buffer); } } |
不知道是什么原因,在Thumbs.db文件当中,数据都是以倒序保存的,比如字符串就是倒序的, 而整形的四个字节也是倒序排列的—难道微软真的不想让第三方程序员访问Thumbs.db文件?
Thumbs.db文件的读取
既然已经知道文件结构,访问的方式就不多讲了,无非就是先用StgOpenStorage函数打开结构化存储文件,获取IStorage接口的引用,读取“Catalog”获得Thumbs.db文件的目录,接着获得每一个缩略图“文件名”对应的CatalogItem,使用CatalogItem的倒序ItemId拿到具体缩略图的IStream指针,然后通过IStream::Read的方法来读取缩略图的内容,最后显示在窗体上。唯一要注意的是,每一个缩略图IStream的前12个字节(3个整形)不是缩略图的内容,不能用的,因此在读取的时候跳过那三个字节好了。
因为.Net只提供了IStream的定义,而IStorage的定义需要我们自己生成。这个接口手工编写.Net对应的接口有点麻烦,因此建议去http://www.pinvoke.net/ 去搜索别人已经写好的定义。
代码下载: