A property lost in tIME

The built-in PNG property handler does not support the tIME chunk for some reason. Why? I don’t know and it is only 7 bytes of data that is easy to parse!

We can fix this with a little shell extension. Sadly PSCreateMultiplexPropertyStore is documented to be read-only and I did not check if the system supplied version of IPropertyStore supports aggregation. We could hook the interface methods or do other tricks but just creating a simple wrapper seems to be enough to get a read-only version to work.

class PngPropShim 
: public IPropertyStore, public IPropertyStoreCapabilities, public INamedPropertyStore
, public IInitializeWithStream {
	IPropertyStore*m_pInnerPS;
	IPropertyStoreCapabilities*m_pInnerPSC;
	INamedPropertyStore*m_pInnerNPS;
	IStream*m_pStrm;
	ULONG m_RefC;
	UINT m_PropStartIdx;
	UINT8 m_PropPngTime[7], m_PropCount;
public:
	PngPropShim(IPropertyStore*pPS) :  m_pInnerPS(pPS), m_pInnerPSC(0), m_pInnerNPS(0), m_pStrm(0), m_RefC(1)
	{
		m_PropCount = 0;
		pPS->QueryInterface(IID_PPV_ARG(IPropertyStoreCapabilities, &m_pInnerPSC));
		pPS->QueryInterface(IID_PPV_ARG(INamedPropertyStore, &m_pInnerNPS));
	}
	~PngPropShim()
	{
		m_pInnerPS->Release();
		SafeRelease(m_pInnerPSC);
		SafeRelease(m_pInnerNPS);
		SafeRelease(m_pStrm);
	}
protected:
	HRESULT LoadPngProps(IStream&s)
	{
		struct CHUNKHDR { UINT32 Len; union {UINT32 U32;UINT8 U8[4];}Type; } chunkhdr;
		UINT8 bufTmp[8];
		HRESULT hr;
		if (hr = ReadAllAt(s, 0, bufTmp, 8)) return hr;
		if (0x474e5089 != *(UINT32*)bufTmp) return E_UNEXPECTED; // BUG: Only checking first 4 bytes of PNG header
		for (;;)
		{
			if (hr = ReadAll(s, &chunkhdr, sizeof(chunkhdr))) return hr;
			UINT32 cbSkip = U32BE2HE(chunkhdr.Len) + 4; // BUG: Could overflow here
			if (U32HE2BE('IEND') == chunkhdr.Type.U32) break;
			if (U32HE2BE('tIME') == chunkhdr.Type.U32)
			{
				if (hr = ReadAll(s, m_PropPngTime, 7)) return hr; else cbSkip -= 7;
				m_PropCount++; // BUG: Should verify chunk CRC before accepting the data
			}
			if (hr = Seek(s, cbSkip)) return hr;
		}
		return S_OK;
	}
	HRESULT LoadProps()
	{
		if (m_PropCount || -1 == m_PropCount) return S_OK;
		if (!m_pStrm) return E_UNEXPECTED; else m_PropCount = 0;
		UINT64 orgPos;
		HRESULT hr;
		if (hr = GetCurrPos(*m_pStrm, orgPos)) return hr;
		if (hr = LoadPngProps(*m_pStrm)) m_PropCount = -1;
		SetAbsPos(*m_pStrm, orgPos);
		return hr;
	}
	bool IsMyProperty(REFPROPERTYKEY key)
	{
		return m_PropCount && IsEqualPropertyKey(key, PKEY_Media_DateEncoded);
	}
	HRESULT GetAt_(DWORD iProp, PROPERTYKEY*pkey)
	{
		return (*pkey = PKEY_Media_DateEncoded, S_OK);
	}
	HRESULT GetValue_(REFPROPERTYKEY key, PROPVARIANT*pPV)
	{
		UINT8*t = m_PropPngTime;
		SYSTEMTIME st = {0};
		st.wYear = MAKEWORD(t[1], t[0]), st.wMonth = t[2], st.wDay = t[3];
		st.wHour = t[4], st.wMinute = t[5], st.wSecond = t[6];
		if (SystemTimeToFileTime(&st, &pPV->filetime))
			return (pPV->vt = VT_FILETIME, S_OK);
		return E_UNEXPECTED;
	}
public:
	STDMETHODIMP_(ULONG) AddRef()  { return InterlockedIncrement(&m_RefC); }
	STDMETHODIMP_(ULONG) Release() { ULONG r = InterlockedDecrement(&m_RefC); if (!r) delete this; return r; }
	STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
	{
		static const QITAB rgqit[] = {
			QITABENT(PngPropShim, IPropertyStore),
			QITABENT(PngPropShim, IInitializeWithStream),
			QITABENT(PngPropShim, IPropertyStoreCapabilities),
			QITABENT(PngPropShim, INamedPropertyStore),
			{0}
		};
		HRESULT hr = QISearch(this, rgqit, riid, ppv);
		if (hr) hr = m_pInnerPS->QueryInterface(riid, ppv); // Let's break some COM rules
		return hr;
	}
	// IInitializeWithStream
	STDMETHODIMP Initialize(IStream*pStrm, DWORD grfMode)
	{
		IInitializeWithStream*pIWS;
		HRESULT hr = m_pInnerPS->QueryInterface(IID_PPV_ARG(IInitializeWithStream, &pIWS));
		if (hr) return hr;
		hr = pIWS->Initialize(pStrm, grfMode), pIWS->Release();
		if (!hr) m_pStrm = pStrm, m_pStrm->AddRef();
		return hr;
	}
	// IPropertyStore
	STDMETHODIMP GetCount(DWORD*cProps)
	{
		HRESULT hr = m_pInnerPS->GetCount(cProps);
		m_PropStartIdx = hr ? 0 : *cProps;
		if (!LoadProps()) *cProps += m_PropCount;
		return hr;
	}
	STDMETHODIMP GetAt(DWORD iProp, PROPERTYKEY*pkey)
	{
		HRESULT hr = m_pInnerPS->GetAt(iProp, pkey);
		if (hr && !LoadProps() && iProp >= m_PropStartIdx && iProp < m_PropStartIdx + m_PropCount)
			hr = GetAt_(iProp - m_PropStartIdx, pkey);
		return hr;
	}
	STDMETHODIMP GetValue(REFPROPERTYKEY key, PROPVARIANT*pPV)
	{
		HRESULT hr = m_pInnerPS->GetValue(key, pPV);
		if ((FAILED(hr) || VT_EMPTY==pPV->vt) && !LoadProps() && IsMyProperty(key))
			hr = GetValue_(key, pPV);
		return hr;
	}
	STDMETHODIMP SetValue(REFPROPERTYKEY key, REFPROPVARIANT propvar)
	{
		HRESULT hr = m_pInnerPS->SetValue(key, propvar);
		if (hr && IsMyProperty(key)) hr = STG_E_ACCESSDENIED;
		return hr;
	}
	STDMETHODIMP Commit()
	{
		return  m_pInnerPS->Commit();
	}
	// IPropertyStoreCapabilities
	STDMETHODIMP IsPropertyWritable(REFPROPERTYKEY key)
	{
		HRESULT hr = m_pInnerPSC ? m_pInnerPSC->IsPropertyWritable(key) : E_FAIL;
		if (hr && IsMyProperty(key)) hr = S_FALSE;
		return hr;
	}
	// INamedPropertyStore
	STDMETHODIMP GetNamedValue(LPCWSTR pszName, PROPVARIANT *ppropvar)
	{
		return m_pInnerNPS ? m_pInnerNPS->GetNamedValue(pszName, ppropvar) : E_NOTIMPL;
	}
	STDMETHODIMP SetNamedValue(LPCWSTR pszName, REFPROPVARIANT propvar)
	{
		return m_pInnerNPS ? m_pInnerNPS->SetNamedValue(pszName, propvar) : E_NOTIMPL;
	}
	STDMETHODIMP GetNameCount(DWORD *pdwCount)
	{
		return m_pInnerNPS ? m_pInnerNPS->GetNameCount(pdwCount) : E_NOTIMPL;
	}
	STDMETHODIMP GetNameAt(DWORD iProp, BSTR *pbstrName)
	{
		return m_pInnerNPS ? m_pInnerNPS->GetNameAt(iProp, pbstrName) : E_NOTIMPL;
	}
public:
	static HRESULT CreateClassInstance(REFIID riid, void **ppv)
	{
		CLSID cls_inner; // We are going to create a shim around the built-in Windows handler
		CLSIDFromString(L"{a38b883c-1682-497e-97b0-0a3a9e801682}", &cls_inner);
		IPropertyStore*pPS;
		HRESULT hr = CoCreateInstance(cls_inner, 0, CLSCTX_INPROC, IID_PPV_ARG(IPropertyStore, &pPS));
		if (!hr)
		{
			PngPropShim*pThis = new (std::nothrow) PngPropShim(pPS);
			if (pThis)
				hr = pThis->QueryInterface(riid, ppv), pThis->Release();
			else
				pPS->Release(), hr = E_OUTOFMEMORY;
		}
		return hr;
	}
};

That is pretty much it and dealing with the PNG parsing and tIME chunk handling is less than 30 lines. It should be noted that ‘abcd’ style multi-character constants are not portable and real code should probably use some PNG helper functions to deal with those. I decided to map tIME to PKEY_Media_DateEncoded but you could map it to other things like PKEY_Document_DateSaved etc.

We now just need some COM stuff and helper functions:

#include <Windows.h>
#include <Propsys.h>
#include <TChar.h>
#include <new>
#include <InitGuid.h>
DEFINE_GUID(CLSID_ThisShellExt,0x...Make your own...);

#ifdef _WIN32
#define U32BE2HE(x) ( (((x)&0xFF000000) >> 24) | (((x)&0x00FF0000) >>  8) | (((x)&0x0000FF00) <<  8) | (((x)&0x000000FF) << 24) )
#define U32HE2BE U32BE2HE
#endif

ULONG g_DllRefC = 0;
#define ThisModule_AddRef() InterlockedIncrement(&g_DllRefC)
#define ThisModule_Release() InterlockedDecrement(&g_DllRefC)

inline ULONG InterlockedIncrement(ULONG*p) { return (ULONG)InterlockedIncrement((volatile LONG*)p); }
inline ULONG InterlockedDecrement(ULONG*p) { return (ULONG)InterlockedDecrement((volatile LONG*)p); }
template<class T> void SafeRelease(T*p) { if (p) p->Release(); }

HRESULT GetCurrPos(IStream&s, UINT64&pos)
{
	ULARGE_INTEGER np;
	np.QuadPart = 0;
	HRESULT hr = s.Seek(*(LARGE_INTEGER*)&np, STREAM_SEEK_CUR, &np);
	return (pos = np.QuadPart, hr);
}
HRESULT Seek(IStream&s, UINT64 pos, DWORD mode = STREAM_SEEK_CUR)
{
	ULARGE_INTEGER np;
	np.QuadPart = pos;
	return s.Seek(*(LARGE_INTEGER*)&np, mode, &np);
}
HRESULT SetAbsPos(IStream&s, UINT64 pos)
{
	ULARGE_INTEGER np;
	np.QuadPart = pos;
	return s.Seek(*(LARGE_INTEGER*)&np, STREAM_SEEK_SET, 0);
}
HRESULT Read(IStream&s, void*Buf, ULONG cb)
{
	HRESULT hr = s.Read(Buf, cb, &cb);
	return FAILED(hr) ? hr : cb;
}
HRESULT ReadAll(IStream&s, void*Buf, ULONG cb)
{
	HRESULT hr = Read(s, Buf, cb);
	return cb == hr ? S_OK : hr;
}
HRESULT ReadAt(IStream&s, UINT64 pos, void*Buf, ULONG cb)
{
	HRESULT hr = SetAbsPos(s, pos);
	return hr ? hr : Read(s, Buf, cb);
}
HRESULT ReadAllAt(IStream&s, UINT64 pos, void*Buf, ULONG cb)
{
	HRESULT hr = ReadAt(s, pos, Buf, cb);
	return cb == hr ? S_OK : hr;
}

...

class ClassFactory
: public IClassFactory {
public:
	STDMETHODIMP_(ULONG) AddRef()  { return 1; }
	STDMETHODIMP_(ULONG) Release() { return 1; }
	STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
	{
		static const QITAB rgqit[] = {
			QITABENT(ClassFactory, IClassFactory),
			{0}
		};
		return QISearch(this, rgqit, riid, ppv);
	}
	STDMETHODIMP CreateInstance(IUnknown*punkOuter, REFIID riid, void **ppv)
	{
		if (!ppv) return E_POINTER;
		*ppv = NULL;
		return punkOuter ? CLASS_E_NOAGGREGATION : PngPropShim::CreateClassInstance(riid, ppv);
	}
	STDMETHODIMP LockServer(BOOL fLock)
	{
		if (fLock) ThisModule_AddRef(); else ThisModule_Release();
		return S_OK;
	}
} g_CF;

EXTERN_C __declspec(dllexport) HRESULT STDMETHODCALLTYPE DllGetClassObject(REFCLSID rclsid,REFIID riid,LPVOID*ppv)
{
	if (!IsEqualCLSID(rclsid, CLSID_ThisShellExt)) return CLASS_E_CLASSNOTAVAILABLE;
	return g_CF.QueryInterface(riid, ppv);
}
EXTERN_C __declspec(dllexport) HRESULT STDMETHODCALLTYPE DllCanUnloadNow()
{
	return g_DllRefC ? S_FALSE : S_OK;
}

EXTERN_C BOOL APIENTRY DllMain(HINSTANCE hInst, DWORD Reason, PVOID Reserved)
{
	DisableThreadLibraryCalls(hInst);
	return true;
}

To get the system to use this new handler we need to take over the registration under ..\PropertySystem\PropertyHandlers\.png and add the normal COM registration under CLSID.

Code that asks for the PKEY_Media_DateEncoded property will now get it if it exists in the file but to get the shell to display it we need to change its System­.PropList­ entries by adding System.Media.DateEncoded to the *Details strings under SystemFileAssociations\.png. Explorer should now show the property:

PNG tIME chunk prop handler

Advertisements

Tags: , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s