#include "precomp.h" #include #include "imagprop.h" #include "shutil.h" #pragma hdrstop #define TF_SUSPENDRESUME 0 // turn on to debug CDecodeStream::Suspend/Resume #define PF_NOSUSPEND 0 // disable suspend and resume (for debugging purposes) class CDecodeStream; //////////////////////////////////////////////////////////////////////////// class CEncoderInfo { public: CEncoderInfo(); virtual ~CEncoderInfo(); protected: HRESULT _GetDataFormatFromPath(LPCWSTR pszPath, GUID *pguidFmt); HRESULT _GetEncoderList(); HRESULT _GetEncoderFromFormat(const GUID *pfmt, CLSID *pclsidEncoder); UINT _cEncoders; // number of encoders discovered ImageCodecInfo *_pici; // array of image encoder classes }; class CImageFactory : public IShellImageDataFactory, private CEncoderInfo, public NonATLObject { public: // IUnknown STDMETHODIMP QueryInterface(REFIID riid, void **ppv); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // IShellImageDataFactory STDMETHODIMP CreateIShellImageData(IShellImageData **ppshimg); STDMETHODIMP CreateImageFromFile(LPCWSTR pszPath, IShellImageData **ppshimg); STDMETHODIMP CreateImageFromStream(IStream *pStream, IShellImageData **ppshimg); STDMETHODIMP GetDataFormatFromPath(LPCWSTR pszPath, GUID *pDataFormat); CImageFactory(); private: ~CImageFactory(); LONG _cRef; CGraphicsInit _cgi; }; class CImageData : public IShellImageData, IPersistFile, IPersistStream, IPropertySetStorage, private CEncoderInfo, public NonATLObject { public: CImageData(BOOL fPropertyOnly = FALSE); static BOOL CALLBACK QueryAbort(void *pvRef); // IUnknown STDMETHOD(QueryInterface)(REFIID riid, void **ppv); STDMETHOD_(ULONG, AddRef)(); STDMETHOD_(ULONG, Release)(); // IPersist STDMETHOD(GetClassID)(CLSID *pclsid) { *pclsid = CLSID_ShellImageDataFactory; return S_OK; } // IPersistFile STDMETHODIMP IsDirty(); STDMETHODIMP Load(LPCOLESTR pszFileName, DWORD dwMode); STDMETHODIMP Save(LPCOLESTR pszFileName, BOOL fRemember); STDMETHODIMP SaveCompleted(LPCOLESTR pszFileName); STDMETHODIMP GetCurFile(LPOLESTR *ppszFileName); // IPersistStream STDMETHOD(Load)(IStream *pstm); STDMETHOD(Save)(IStream *pstm, BOOL fClearDirty); STDMETHOD(GetSizeMax)(ULARGE_INTEGER *pcbSize) { return E_NOTIMPL; } // IPropertySetStorage methods STDMETHODIMP Create(REFFMTID fmtid, const CLSID * pclsid, DWORD grfFlags, DWORD grfMode, IPropertyStorage** ppPropStg); STDMETHODIMP Open(REFFMTID fmtid, DWORD grfMode, IPropertyStorage** ppPropStg); STDMETHODIMP Delete(REFFMTID fmtid); STDMETHODIMP Enum(IEnumSTATPROPSETSTG** ppenum); // IShellImageData STDMETHODIMP Decode(DWORD dwFlags, ULONG pcx, ULONG pcy); STDMETHODIMP Draw(HDC hdc, LPRECT prcDest, LPRECT prcSrc); STDMETHODIMP NextFrame(); STDMETHODIMP NextPage(); STDMETHODIMP PrevPage(); STDMETHODIMP IsTransparent(); STDMETHODIMP IsVector(); STDMETHODIMP IsAnimated() { return _fAnimated ? S_OK : S_FALSE; } STDMETHODIMP IsMultipage() { return (!_fAnimated && _cImages > 1) ? S_OK : S_FALSE; } STDMETHODIMP IsDecoded(); STDMETHODIMP IsPrintable() { return S_OK; } // all images are printable STDMETHODIMP IsEditable() { return _fEditable ? S_OK : S_FALSE; } STDMETHODIMP GetCurrentPage(ULONG *pnPage) { *pnPage = _iCurrent; return S_OK; } STDMETHODIMP GetPageCount(ULONG *pcPages) { HRESULT hr = _EnsureImage(); *pcPages = _cImages; return hr; } STDMETHODIMP SelectPage(ULONG iPage); STDMETHODIMP GetResolution(ULONG *puResolutionX, ULONG *puResolutionY); STDMETHODIMP GetRawDataFormat(GUID *pfmt); STDMETHODIMP GetPixelFormat(PixelFormat *pfmt); STDMETHODIMP GetSize(SIZE *pSize); STDMETHODIMP GetDelay(DWORD *pdwDelay); STDMETHODIMP DisplayName(LPWSTR wszName, UINT cch); STDMETHODIMP GetProperties(DWORD dwMode, IPropertySetStorage **ppPropSet); STDMETHODIMP Rotate(DWORD dwAngle); STDMETHODIMP Scale(ULONG cx, ULONG cy, InterpolationMode hints); STDMETHODIMP DiscardEdit(); STDMETHODIMP SetEncoderParams(IPropertyBag *ppbEnc); STDMETHODIMP GetEncoderParams(GUID *pguidFmt, EncoderParameters **ppencParams); STDMETHODIMP RegisterAbort(IShellImageDataAbort *pAbort, IShellImageDataAbort **ppAbortPrev); STDMETHODIMP CloneFrame(Image **ppimg); STDMETHODIMP ReplaceFrame(Image *pimg); private: CGraphicsInit _cgi; LONG _cRef; DWORD _dwMode; // open mode from IPersistFile::Load() CDecodeStream *_pstrm; // stream that will produce our data BOOL _fLoaded; // true once PersistFile or PersistStream have been called BOOL _fDecoded; // true once Decode ahs been called DWORD _dwFlags; // flags and size passed to Decode method int _cxDesired; int _cyDesired; Image *_pImage; // source of the images (created from the filename) // REVIEW: do we need to make these be per-frame/page? // YES! Image *_pimgEdited; // edited image HDPA _hdpaProps; // properties for each frame DWORD _dwRotation; BOOL _fDestructive; // not a lossless edit operation BOOL _fAnimated; // this is an animated stream (eg. not multi page picture) BOOL _fLoopForever; // loop the animated gif forever int _cLoop; // loop count (0 forever, n = repeat count) BOOL _fEditable; // can be edited GUID _guidFmt; // format GUID (original stream is this) DWORD _cImages; // number of frames/pages in the image DWORD _iCurrent; // current frame/page we want to display PropertyItem *_piAnimDelay; // array of the delay assocated with each frame BOOL _fPropertyOnly; BOOL _fPropertyChanged; // image encoder information (created on demand) IPropertyBag *_ppbEncoderParams; // property bag with encoder parameters IShellImageDataAbort *_pAbort; // optional abort callback CDSA _dsaChangedProps; // which properties have changed private: ~CImageData(); HRESULT _EnsureImage(); HRESULT _SuspendStream(); HRESULT _SetDecodeStream(CDecodeStream *pds); HRESULT _CreateMemPropSetStorage(IPropertySetStorage **ppss); HRESULT _PropImgToVariant(PropertyItem *pi, VARIANT *pvar); HRESULT _GetProperty(PROPID id, VARIANT *pvar, VARTYPE vt); HRESULT _GetDisplayedImage(); void _SetEditImage(Image *pimgEdit); HRESULT _SaveImages(IStream *pstrm, GUID * pguidFmt); HRESULT _ReplaceFile(LPCTSTR pszNewFile); HRESULT _MakeTempFile(LPWSTR pszFile); void _AddEncParameter(EncoderParameters *pep, GUID guidProperty, ULONG type, void *pv); HRESULT _EnsureProperties(IPropertySetStorage **ppss); HRESULT _CreatePropStorage(IPropertyStorage **ppps, REFFMTID fmtid); static int _FreeProps(void *pProp, void *pData); // // since CImagePropSet objects come and go, we need to persist which properties need updating in the CImageData // void _SaveFrameProperties(Image *pimg, LONG iFrame); static void _PropertyChanged(IShellImageData *pThis, SHCOLUMNID *pscid ); }; //////////////////////////////////////////////////////////////////////////// // // CDecodeStream // // Wraps a regular IStream, but is cancellable and can be // suspended/resumed to prevent the underlying file from being held // open unnecessarily. // //////////////////////////////////////////////////////////////////////////// class CDecodeStream : public IStream, public NonATLObject { public: CDecodeStream(CImageData *pid, IStream *pstrm); CDecodeStream(CImageData *pid, LPCTSTR pszFilename, DWORD dwMode); ~CDecodeStream() { ASSERT(!_pidOwner); #ifdef DEBUG // Need #ifdef because we call a function if (IsFileStream()) { TraceMsg(TF_SUSPENDRESUME, "ds.Release %s", PathFindFileName(_szFilename)); } #endif ATOMICRELEASE(_pstrmInner); } HRESULT Suspend(); HRESULT Resume(BOOL fFullLoad = FALSE); void Reload(); // // Before releasing, you must Detach to break the backreference. // Otherwise, the next time somebody calls QueryCancel, we will fault. // void Detach() { _pidOwner = NULL; } BOOL IsFileStream() { return _szFilename[0]; } LPCTSTR GetFilename() { return _szFilename; } HRESULT DisplayName(LPWSTR wszName, UINT cch); // *** IUnknown *** STDMETHODIMP QueryInterface(REFIID riid, LPVOID * ppvObj); STDMETHODIMP_(ULONG) AddRef(void); STDMETHODIMP_(ULONG) Release(void); // *** IStream *** STDMETHODIMP Read(void *pv, ULONG cb, ULONG *pcbRead); STDMETHODIMP Write(void const *pv, ULONG cb, ULONG *pcbWritten); STDMETHODIMP Seek(LARGE_INTEGER dlibMove, DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition); STDMETHODIMP SetSize(ULARGE_INTEGER libNewSize); STDMETHODIMP CopyTo(IStream *pstm, ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, ULARGE_INTEGER *pcbWritten); STDMETHODIMP Commit(DWORD grfCommitFlags); STDMETHODIMP Revert(); STDMETHODIMP LockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType); STDMETHODIMP UnlockRegion(ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType); STDMETHODIMP Stat(STATSTG *pstatstg, DWORD grfStatFlag); STDMETHODIMP Clone(IStream **ppstm); private: void CommonConstruct(CImageData *pid) { _cRef = 1; _pidOwner = pid; _fSuspendable = !(g_dwPrototype & PF_NOSUSPEND);} HRESULT FilterAccess(); private: IStream * _pstrmInner; CImageData *_pidOwner; // NOT REFCOUNTED LONG _cRef; LARGE_INTEGER _liPos; // Where we were in the file when we suspended TCHAR _szFilename[MAX_PATH]; // file we are a stream for BOOL _fSuspendable; }; CDecodeStream::CDecodeStream(CImageData *pid, IStream *pstrm) { CommonConstruct(pid); IUnknown_Set((IUnknown**)&_pstrmInner, pstrm); } CDecodeStream::CDecodeStream(CImageData *pid, LPCTSTR pszFilename, DWORD dwMode) { CommonConstruct(pid); lstrcpyn(_szFilename, pszFilename, ARRAYSIZE(_szFilename)); // ignore the mode } //reload is only used for file streams void CDecodeStream::Reload() { if (IsFileStream()) { ATOMICRELEASE(_pstrmInner); if (_fSuspendable) { ZeroMemory(&_liPos, sizeof(_liPos)); } } } HRESULT CDecodeStream::Suspend() { HRESULT hr; if (IsFileStream() && _pstrmInner && _fSuspendable) { // Remember the file position so we can restore it when we resume const LARGE_INTEGER liZero = { 0, 0 }; hr = _pstrmInner->Seek(liZero, FILE_CURRENT, (ULARGE_INTEGER*)&_liPos); if (SUCCEEDED(hr)) { #ifdef DEBUG // Need #ifdef because we call a function TraceMsg(TF_SUSPENDRESUME, "ds.Suspend %s, pos=0x%08x", PathFindFileName(_szFilename), _liPos.LowPart); #endif ATOMICRELEASE(_pstrmInner); hr = S_OK; } } else { hr = S_FALSE; // Not suspendable or already suspended } return hr; } HRESULT CDecodeStream::Resume(BOOL fLoadFull) { HRESULT hr; if (_pstrmInner) { return S_OK; } if (fLoadFull) { _fSuspendable = FALSE; } if (IsFileStream()) { if (PathIsURL(_szFilename)) { // TODO: use URLMon to load the image, make sure we check for being allowed to go on-line hr = E_NOTIMPL; } else { if (!fLoadFull) { hr = SHCreateStreamOnFileEx(_szFilename, STGM_READ | STGM_SHARE_DENY_NONE, 0, FALSE, NULL, &_pstrmInner); if (SUCCEEDED(hr)) { hr = _pstrmInner->Seek(_liPos, FILE_BEGIN, NULL); if (SUCCEEDED(hr)) { #ifdef DEBUG // Need #ifdef because we call a function TraceMsg(TF_SUSPENDRESUME, "ds.Resumed %s, pos=0x%08x", PathFindFileName(_szFilename), _liPos.LowPart); #endif } else { ATOMICRELEASE(_pstrmInner); } } } else { hr = S_OK; HANDLE hFile = CreateFile(_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (INVALID_HANDLE_VALUE != hFile) { LARGE_INTEGER liSize = {0}; // we can't handle huge files if (GetFileSizeEx(hFile, &liSize) && !liSize.HighPart) { DWORD dwToRead = liSize.LowPart; HGLOBAL hGlobal = GlobalAlloc(GHND, dwToRead); if (hGlobal) { void *pv = GlobalLock(hGlobal); DWORD dwRead; if (pv) { if (ReadFile(hFile, pv, dwToRead, &dwRead, NULL)) { ASSERT(dwRead == dwToRead); GlobalUnlock(hGlobal); hr = CreateStreamOnHGlobal(hGlobal, TRUE, &_pstrmInner); } else { GlobalUnlock(hGlobal); } } if (!_pstrmInner) { GlobalFree(hGlobal); } } } CloseHandle(hFile); } } if (SUCCEEDED(hr) && !_pstrmInner) { DWORD dw = GetLastError(); hr = HRESULT_FROM_WIN32(dw); } } if (FAILED(hr)) { #ifdef DEBUG // Need #ifdef because we call a function TraceMsg(TF_SUSPENDRESUME, "ds.Resume %s failed: %08x", PathFindFileName(_szFilename), hr); #endif } } else { hr = E_FAIL; // Can't resume without a filename } return hr; } // // This function is called at the top of each IStream method to make // sure that the stream has not been cancelled and resumes it if // necessary. // HRESULT CDecodeStream::FilterAccess() { if (_pidOwner && _pidOwner->QueryAbort(_pidOwner)) { return E_ABORT; } return Resume(); } HRESULT CDecodeStream::DisplayName(LPWSTR wszName, UINT cch) { HRESULT hr = E_FAIL; if (IsFileStream()) { // from the filename generate the leaf name which we can // return the name to caller. LPTSTR pszFilename = PathFindFileName(_szFilename); if (pszFilename) { SHTCharToUnicode(pszFilename, wszName, cch); hr = S_OK; } } else if (_pstrmInner) { // this is a stream, so lets get the display name from the that stream // and return that into the buffer that the caller has given us. STATSTG stat; hr = _pstrmInner->Stat(&stat, 0x0); if (SUCCEEDED(hr)) { if (stat.pwcsName) { StrCpyN(wszName, stat.pwcsName, cch); CoTaskMemFree(stat.pwcsName); } else { hr = E_FAIL; } } } else { hr = E_FAIL; } return hr; } // // Now the boring part... // // *** IUnknown *** HRESULT CDecodeStream::QueryInterface(REFIID riid, LPVOID * ppvObj) { static const QITAB qit[] = { QITABENT(CDecodeStream, IStream), { 0 }, }; return QISearch(this, qit, riid, ppvObj); } ULONG CDecodeStream::AddRef() { return InterlockedIncrement(&_cRef); } ULONG CDecodeStream::Release() { ASSERT( 0 != _cRef ); ULONG cRef = InterlockedDecrement(&_cRef); if ( 0 == cRef ) { delete this; } return cRef; } // *** IStream *** #define WRAP_METHOD(fn, args, argl) \ HRESULT CDecodeStream::fn args \ { \ HRESULT hr = FilterAccess(); \ if (SUCCEEDED(hr)) \ { \ hr = _pstrmInner->fn argl; \ } \ return hr; \ } WRAP_METHOD(Read, (void *pv, ULONG cb, ULONG *pcbRead), (pv, cb, pcbRead)) WRAP_METHOD(Write, (void const *pv, ULONG cb, ULONG *pcbWritten), (pv, cb, pcbWritten)) WRAP_METHOD(Seek, (LARGE_INTEGER dlibMove, DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition), (dlibMove, dwOrigin, plibNewPosition)) WRAP_METHOD(SetSize, (ULARGE_INTEGER libNewSize), (libNewSize)) WRAP_METHOD(CopyTo, (IStream *pstm, ULARGE_INTEGER cb, ULARGE_INTEGER *pcbRead, ULARGE_INTEGER *pcbWritten), (pstm, cb, pcbRead, pcbWritten)) WRAP_METHOD(Commit, (DWORD grfCommitFlags), (grfCommitFlags)) WRAP_METHOD(Revert, (), ()) WRAP_METHOD(LockRegion, (ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType), (libOffset, cb, dwLockType)) WRAP_METHOD(UnlockRegion, (ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, DWORD dwLockType), (libOffset, cb, dwLockType)) WRAP_METHOD(Stat, (STATSTG *pstatstg, DWORD grfStatFlag), (pstatstg, grfStatFlag)) WRAP_METHOD(Clone, (IStream **ppstm), (ppstm)) #undef WRAP_METHOD //////////////////////////////////////////////////////////////////////////// class CFmtEnum : public IEnumSTATPROPSETSTG, public NonATLObject { public: STDMETHODIMP Next(ULONG celt, STATPROPSETSTG *rgelt, ULONG *pceltFetched); STDMETHODIMP Skip(ULONG celt); STDMETHODIMP Reset(void); STDMETHODIMP Clone(IEnumSTATPROPSETSTG **ppenum); STDMETHODIMP QueryInterface(REFIID riid, void **ppvObj); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); CFmtEnum(IEnumSTATPROPSETSTG *pEnum); private: ~CFmtEnum(); IEnumSTATPROPSETSTG *_pEnum; ULONG _idx; LONG _cRef; }; #define HR_FROM_STATUS(x) ((x) == Ok) ? S_OK : E_FAIL // IUnknown STDMETHODIMP CImageData::QueryInterface(REFIID riid, void **ppv) { static const QITAB qit[] = { QITABENT(CImageData, IShellImageData), QITABENT(CImageData, IPersistFile), QITABENT(CImageData, IPersistStream), QITABENT(CImageData, IPropertySetStorage), { 0 }, }; return QISearch(this, qit, riid, ppv); } STDMETHODIMP_(ULONG) CImageData::AddRef() { return InterlockedIncrement(&_cRef); } STDMETHODIMP_(ULONG) CImageData::Release() { ASSERT( 0 != _cRef ); ULONG cRef = InterlockedDecrement(&_cRef); if ( 0 == cRef ) { delete this; } return cRef; } CImageData::CImageData(BOOL fPropertyOnly) : _cRef(1), _cImages(1), _fPropertyOnly(fPropertyOnly), _fPropertyChanged(FALSE) { // Catch unexpected STACK allocations which would break us. ASSERT(_dwMode == 0); ASSERT(_pstrm == NULL); ASSERT(_fLoaded == FALSE); ASSERT(_fDecoded == FALSE); ASSERT(_dwFlags == 0); ASSERT(_cxDesired == 0); ASSERT(_cyDesired == 0); ASSERT(_pImage == NULL); ASSERT(_pimgEdited == NULL); ASSERT(_hdpaProps == NULL); ASSERT(_dwRotation == 0); ASSERT(_fDestructive == FALSE); ASSERT(_fAnimated == FALSE); ASSERT(_fLoopForever == FALSE); ASSERT(_cLoop == 0); ASSERT(_fEditable == FALSE); ASSERT(_iCurrent == 0); ASSERT(_piAnimDelay == NULL); ASSERT(_ppbEncoderParams == NULL); ASSERT(_pAbort == NULL); } CImageData::~CImageData() { if (_fPropertyOnly && _fPropertyChanged) { Save((LPCTSTR)NULL, FALSE); } if (_pstrm) { _pstrm->Detach(); _pstrm->Release(); } if (_pImage) { delete _pImage; // discard the pImage object we have been using _pImage = NULL; } if (_pimgEdited) { delete _pimgEdited; _pimgEdited = NULL; } if (_piAnimDelay) LocalFree(_piAnimDelay); // do we have an array of image frame delays to destroy if (_hdpaProps) DPA_DestroyCallback(_hdpaProps, _FreeProps, NULL); if (_fLoaded) { _dsaChangedProps.Destroy(); } ATOMICRELEASE(_pAbort); } // IPersistStream HRESULT CImageData::_SetDecodeStream(CDecodeStream *pds) { ASSERT(_pstrm == NULL); _pstrm = pds; if (_pstrm) { _fLoaded = TRUE; _dsaChangedProps.Create(10); return S_OK; } else { return E_OUTOFMEMORY; } } HRESULT CImageData::Load(IStream *pstrm) { if (_fLoaded) return STG_E_INUSE; return _SetDecodeStream(new CDecodeStream(this, pstrm)); } HRESULT CImageData::Save(IStream *pstrm, BOOL fClearDirty) { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { hr = _SaveImages(pstrm, &_guidFmt); if (SUCCEEDED(hr)) { _fPropertyChanged = FALSE; DiscardEdit(); } } return hr; } // IPersistFile methods HRESULT CImageData::IsDirty() { return (_dwRotation || _pimgEdited) ? S_OK : S_FALSE; } HRESULT CImageData::Load(LPCOLESTR pszFileName, DWORD dwMode) { if (_fLoaded) return STG_E_INUSE; if (!*pszFileName) return E_INVALIDARG; return _SetDecodeStream(new CDecodeStream(this, pszFileName, dwMode)); } #define ATTRIBUTES_TEMPFILE (FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_TEMPORARY) // IPersistFile::Save() see SDK docs HRESULT CImageData::Save(LPCOLESTR pszFile, BOOL fRemember) { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { // If this fires, then somebody managed to get _EnsureImage to // succeed without ever actually loading anything... (?) ASSERT(_pstrm); if (pszFile == NULL && !_pstrm->IsFileStream()) { // Trying to "save with same name you loaded from" // when we weren't loaded from a file to begin with hr = HRESULT_FROM_WIN32(ERROR_INVALID_NAME); } else { // we default to saving in the original format that we were given // if the name is NULL (we will also attempt to replace the original file). TCHAR szTempFile[MAX_PATH]; GUID guidFmt = _guidFmt; // default to original format BOOL fReplaceOriginal = _pstrm->IsFileStream() && ((NULL == pszFile) || (S_OK == IsSameFile(pszFile, _pstrm->GetFilename()))); if (fReplaceOriginal) { // we are being told to save to the current file, but we have the current file locked open. // To get around this we save to a temporary file, close our handles on the current file, // and then replace the current file with the new file hr = _MakeTempFile(szTempFile); pszFile = szTempFile; } else if (!_ppbEncoderParams) { // the caller did not tell us which encoder to use? // determine the encoder based on the target file name hr = _GetDataFormatFromPath(pszFile, &guidFmt); } if (SUCCEEDED(hr)) { // the attributes are important as they need to match those of the // temp file we created else this call fails IStream *pstrm; hr = SHCreateStreamOnFileEx(pszFile, STGM_WRITE | STGM_CREATE, fReplaceOriginal ? ATTRIBUTES_TEMPFILE : 0, TRUE, NULL, &pstrm); if (SUCCEEDED(hr)) { hr = _SaveImages(pstrm, &guidFmt); pstrm->Release(); if (SUCCEEDED(hr) && fReplaceOriginal) { hr = _ReplaceFile(szTempFile); if (SUCCEEDED(hr)) { _fPropertyChanged = FALSE; DiscardEdit(); // note we can't discard any edits until the original was overwritten delete _pImage; _pImage = NULL; _fDecoded = FALSE; _pstrm->Reload(); DWORD iCurrentPage = _iCurrent; hr = Decode(_dwFlags, _cxDesired, _cyDesired); if (iCurrentPage < _cImages) _iCurrent = iCurrentPage; } } } if (FAILED(hr) && fReplaceOriginal) { // make sure temp file is gone DeleteFile(szTempFile); } } } } return hr; } HRESULT CImageData::SaveCompleted(LPCOLESTR pszFileName) { return E_NOTIMPL; } HRESULT CImageData::GetCurFile(LPOLESTR *ppszFileName) { if (_pstrm && _pstrm->IsFileStream()) return SHStrDup(_pstrm->GetFilename(), ppszFileName); return E_FAIL; } // handle decoding the image this includes updating our cache of the images HRESULT CImageData::_EnsureImage() { if (_fDecoded && _pImage) return S_OK; return E_FAIL; } HRESULT CImageData::_SuspendStream() { HRESULT hr = S_OK; if (_pstrm) { hr = _pstrm->Suspend(); } return hr; } HRESULT CImageData::_GetDisplayedImage() { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { const CLSID * pclsidFrameDim = _fAnimated ? &FrameDimensionTime : &FrameDimensionPage; hr = HR_FROM_STATUS(_pImage->SelectActiveFrame(pclsidFrameDim, _iCurrent)); } return hr; } // IShellImageData method HRESULT CImageData::Decode(DWORD dwFlags, ULONG cx, ULONG cy) { if (!_fLoaded) return E_FAIL; if (_fDecoded) return S_FALSE; HRESULT hr = S_OK; _dwFlags = dwFlags; _cxDesired = cx; _cyDesired = cy; // // Resume the stream now so GDI+ won't go nuts trying to detect the // image type of a file that it can't read... // hr = _pstrm->Resume(dwFlags & SHIMGDEC_LOADFULL); // if that succeeded then we can create an image using the stream and decode // the images from that. once we are done we will release the objects. if (SUCCEEDED(hr)) { _pImage = new Image(_pstrm, TRUE); if (_pImage) { if (Ok != _pImage->GetLastStatus()) { delete _pImage; _pImage = NULL; hr = E_FAIL; } else { _fEditable = TRUE; if (_dwFlags & SHIMGDEC_THUMBNAIL) { // for thumbnails, _cxDesired and _cyDesired define a bounding rectangle but we // should maintain the original aspect ratio. int cxT = _pImage->GetWidth(); int cyT = _pImage->GetHeight(); if (cxT > _cxDesired || cyT > _cyDesired) { if (Int32x32To64(_cxDesired, cyT) > Int32x32To64(cxT, _cyDesired)) { // constrained by height cxT = MulDiv(cxT, _cyDesired, cyT); if (cxT < 1) cxT = 1; cyT = _cyDesired; } else { // constrained by width cyT = MulDiv(cyT, _cxDesired, cxT); if (cyT < 1) cyT = 1; cxT = _cxDesired; } } Image * pThumbnail; pThumbnail = _pImage->GetThumbnailImage(cxT, cyT, QueryAbort, this); // // GDI+ sometimes forgets to tell us it gave up due to an abort. // if (pThumbnail && !QueryAbort(this)) { delete _pImage; _pImage = pThumbnail; } else { delete pThumbnail; // "delete" ignores NULL pointers hr = E_FAIL; } } else { _pImage->GetRawFormat(&_guidFmt); // read the raw format of the file if (_guidFmt == ImageFormatTIFF) { VARIANT var; if (SUCCEEDED(_GetProperty(PropertyTagExifIFD, &var, VT_UI4))) { // TIFF images with an EXIF IFD aren't editable by GDI+ _fEditable = FALSE; VariantClear(&var); } } // is this an animated/multi page image? _cImages = _pImage->GetFrameCount(&FrameDimensionPage); if (_cImages <= 1) { _cImages = _pImage->GetFrameCount(&FrameDimensionTime); if (_cImages > 1) { _fAnimated = TRUE; // store the frame delays in PropertyItem *_piAnimDelay; UINT cb = _pImage->GetPropertyItemSize(PropertyTagFrameDelay); if (cb) { _piAnimDelay = (PropertyItem*)LocalAlloc(LPTR, cb); if (_piAnimDelay) { if (Ok != _pImage->GetPropertyItem(PropertyTagFrameDelay, cb, _piAnimDelay)) { LocalFree(_piAnimDelay); _piAnimDelay = NULL; } } } } } _pImage->GetLastStatus(); // 145081: clear the error from the first call to _pImage->GetFrameCount so that // the later call to _GetProperty won't immediately fail. // we wouldn't have to do this always if the gdi interface didn't maintain its own // error code and fail automatically based on it without allowing us to check it // without resetting it. // some decoders will return zero as the frame count when they don't support that dimension if (0 == _cImages) _cImages = 1; // is it a looping image? this will only be if its animated if (_fAnimated) { VARIANT var; if (SUCCEEDED(_GetProperty(PropertyTagLoopCount, &var, VT_UI4))) { _cLoop = var.ulVal; _fLoopForever = (_cLoop == 0); VariantClear(&var); } } PixelFormat pf = _pImage->GetPixelFormat(); // can we edit this image? NOTE: The caller needs to ensure that the file is writeable // all of that jazz, we only check if we have an encoder for this format. Just cause we // can edit a file doesn't mean the file can be written to the original source location. // We can't edit images with > 8 bits per channel either if (_fEditable) { _fEditable = !_fAnimated && SUCCEEDED(_GetEncoderFromFormat(&_guidFmt, NULL)) && !IsExtendedPixelFormat(pf); } } } } else { hr = E_OUTOFMEMORY; // failed to allocate the image decoder } } // Suspend the stream so we don't leave the file open _SuspendStream(); _fDecoded = TRUE; return hr; } HRESULT CImageData::Draw(HDC hdc, LPRECT prcDest, LPRECT prcSrc) { if (!prcDest) return E_INVALIDARG; // not much chance without a destination to paint into HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { Image *pimg = _pimgEdited ? _pimgEdited : _pImage; RECT rcSrc; if (prcSrc) { rcSrc.left = prcSrc->left; rcSrc.top = prcSrc->top; rcSrc.right = RECTWIDTH(*prcSrc); rcSrc.bottom= RECTHEIGHT(*prcSrc); } else { rcSrc.left = 0; rcSrc.top = 0; rcSrc.right = pimg->GetWidth(); rcSrc.bottom= pimg->GetHeight(); } Unit unit; RectF rectf; if (Ok==pimg->GetBounds(&rectf, &unit) && UnitPixel==unit) { rcSrc.left += (int)rectf.X; rcSrc.top += (int)rectf.Y; } // we have a source rectangle so lets apply that when we render this image. Rect rc(prcDest->left, prcDest->top, RECTWIDTH(*prcDest), RECTHEIGHT(*prcDest)); DWORD dwLayout = SetLayout(hdc, LAYOUT_BITMAPORIENTATIONPRESERVED); Graphics g(hdc); g.SetPageUnit(UnitPixel); // WARNING: If you remove this line (as has happened twice since Beta 1) you will break printing. if (_guidFmt == ImageFormatTIFF) { g.SetInterpolationMode(InterpolationModeHighQualityBilinear); } hr = HR_FROM_STATUS(g.DrawImage(pimg, rc, rcSrc.left, rcSrc.top, rcSrc.right, rcSrc.bottom, UnitPixel, NULL, QueryAbort, this)); // // GDI+ sometimes forgets to tell us it gave up due to an abort. // if (SUCCEEDED(hr) && QueryAbort(this)) hr = E_ABORT; if (GDI_ERROR != dwLayout) SetLayout(hdc, dwLayout); } // Suspend the stream so we don't leave the file open _SuspendStream(); return hr; } HRESULT CImageData::SelectPage(ULONG iPage) { if (iPage >= _cImages) return OLE_E_ENUM_NOMORE; if (_iCurrent != iPage) { // Since we are moving to a different page throw away any edits DiscardEdit(); } _iCurrent = iPage; return _GetDisplayedImage(); } HRESULT CImageData::NextFrame() { if (!_fAnimated) return S_FALSE; // not animated, so no next frame // if this is the last image, then lets look at the loop // counter and try and decide if we should cycle this image // around or not. if ((_iCurrent == _cImages-1) && !_fLoopForever) { if (_cLoop) _cLoop --; // if cLoop is zero then we're done looping if (_cLoop == 0) return S_FALSE; } // advance to the next image in the sequence _iCurrent = (_iCurrent+1) % _cImages; return _GetDisplayedImage(); } HRESULT CImageData::NextPage() { if (_iCurrent >= _cImages-1) return OLE_E_ENUM_NOMORE; // Since we are moving to the next page throw away any edits DiscardEdit(); _iCurrent++; return _GetDisplayedImage(); } HRESULT CImageData::PrevPage() { if (_iCurrent == 0) return OLE_E_ENUM_NOMORE; // Since we are moving to the next page throw away any edits DiscardEdit(); _iCurrent--; return _GetDisplayedImage(); } STDMETHODIMP CImageData::IsTransparent() { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) hr = (_pImage->GetFlags() & ImageFlagsHasAlpha) ? S_OK : S_FALSE; return hr; } HRESULT CImageData::IsVector() { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { hr = (_pImage->GetFlags() & ImageFlagsScalable) ? S_OK : S_FALSE; } return hr; } HRESULT CImageData::GetSize(SIZE *pSize) { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { Image *pimg = _pimgEdited ? _pimgEdited : _pImage; pSize->cx = pimg->GetWidth(); pSize->cy = pimg->GetHeight(); } return hr; } HRESULT CImageData::GetRawDataFormat(GUID *pfmt) { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { *pfmt = _guidFmt; } return hr; } HRESULT CImageData::GetPixelFormat(PixelFormat *pfmt) { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { *pfmt = _pImage->GetPixelFormat(); } return hr; } HRESULT CImageData::GetDelay(DWORD *pdwDelay) { HRESULT hr = _EnsureImage(); DWORD dwFrame = _iCurrent; if (SUCCEEDED(hr)) { hr = E_FAIL; if (_piAnimDelay) { if (_piAnimDelay->length != (sizeof(DWORD) * _cImages)) { dwFrame = 0; // if array is not the expected size, be safe and just grab the delay of the first image } CopyMemory(pdwDelay, (void *)((UINT_PTR)_piAnimDelay->value + dwFrame * sizeof(DWORD)), sizeof(DWORD)); *pdwDelay = *pdwDelay * 10; if (*pdwDelay < 100) { *pdwDelay = 100; // hack: do the same thing as mshtml, see inetcore\mshtml\src\site\download\imggif.cxx!CImgTaskGif::ReadGIFMaster } hr = S_OK; } } return hr; } HRESULT CImageData::IsDecoded() { return _pImage ? S_OK : S_FALSE; } HRESULT CImageData::DisplayName(LPWSTR wszName, UINT cch) { HRESULT hr = E_FAIL; // always set the out parameter to something known *wszName = L'\0'; if (_pstrm) { hr = _pstrm->DisplayName(wszName, cch); } // REVIEW: If the user has selected not to view file extentions for known types should we hide the extention? return hr; } // property handling code - decoding, conversion and other packing HRESULT CImageData::_PropImgToVariant(PropertyItem *pi, VARIANT *pvar) { HRESULT hr = S_OK; switch (pi->type) { case PropertyTagTypeByte: pvar->vt = VT_UI1; // check for multi-valued property and convert to safearray if found if (pi->length > sizeof(UCHAR)) { SAFEARRAYBOUND bound; bound.cElements = pi->length/sizeof(UCHAR); bound.lLbound = 0; pvar->vt |= VT_ARRAY; hr = E_OUTOFMEMORY; pvar->parray = SafeArrayCreate(VT_UI1, 1, &bound); if (pvar->parray) { void *pv; hr = SafeArrayAccessData (pvar->parray, &pv); if (SUCCEEDED(hr)) { CopyMemory(pv, pi->value, pi->length); SafeArrayUnaccessData(pvar->parray); } } } else { pvar->bVal = *((UCHAR*)pi->value); } break; case PropertyTagTypeShort: pvar->vt = VT_UI2; pvar->uiVal = *((USHORT*)pi->value); break; case PropertyTagTypeLong: pvar->vt = VT_UI4; pvar->ulVal = *((ULONG*)pi->value); break; case PropertyTagTypeASCII: { WCHAR szValue[MAX_PATH]; SHAnsiToUnicode(((LPSTR)pi->value), szValue, ARRAYSIZE(szValue)); hr = InitVariantFromStr(pvar, szValue); } break; case PropertyTagTypeRational: { LONG *pl = (LONG*)pi->value; LONG num = pl[0]; LONG den = pl[1]; pvar->vt = VT_R8; if (0 == den) pvar->dblVal = 0; // don't divide by zero else pvar->dblVal = ((double)num)/((double)den); } break; case PropertyTagTypeUndefined: case PropertyTagTypeSLONG: case PropertyTagTypeSRational: default: hr = E_UNEXPECTED; break; } return hr; } HRESULT CImageData::_GetProperty(PROPID id, VARIANT *pvar, VARTYPE vt) { UINT cb = _pImage->GetPropertyItemSize(id); HRESULT hr = HR_FROM_STATUS(_pImage->GetLastStatus()); if (cb && SUCCEEDED(hr)) { PropertyItem *pi = (PropertyItem*)LocalAlloc(LPTR, cb); if (pi) { hr = HR_FROM_STATUS(_pImage->GetPropertyItem(id, cb, pi)); if (SUCCEEDED(hr)) { hr = _PropImgToVariant(pi, pvar); } LocalFree(pi); } } if (SUCCEEDED(hr) && (vt != 0) && (pvar->vt != vt)) hr = VariantChangeType(pvar, pvar, 0, vt); return hr; } HRESULT CImageData::GetProperties(DWORD dwMode, IPropertySetStorage **ppss) { HRESULT hr = _EnsureProperties(NULL); if (SUCCEEDED(hr)) { hr = QueryInterface(IID_PPV_ARG(IPropertySetStorage, ppss)); } return hr; } HRESULT CImageData::_CreateMemPropSetStorage(IPropertySetStorage **ppss) { *ppss = NULL; ILockBytes *plb; HRESULT hr = CreateILockBytesOnHGlobal(NULL, TRUE, &plb); if (SUCCEEDED(hr)) { IStorage *pstg; hr = StgCreateDocfileOnILockBytes(plb, STGM_DIRECT|STGM_READWRITE|STGM_CREATE|STGM_SHARE_EXCLUSIVE, 0, &pstg); if (SUCCEEDED(hr)) { hr = pstg->QueryInterface(IID_PPV_ARG(IPropertySetStorage, ppss)); pstg->Release(); } plb->Release(); } return hr; } // _EnsureProperties returns an in-memory IPropertySetStorage for the current active frame // For read-only files we may not be able to modify the property set, so handle access issues // gracefully. HRESULT CImageData::_EnsureProperties(IPropertySetStorage **ppss) { if (ppss) { *ppss = NULL; } Decode(SHIMGDEC_DEFAULT, 0, 0); HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr) && !_hdpaProps) { _hdpaProps = DPA_Create(_cImages); if (!_hdpaProps) { hr = E_OUTOFMEMORY; } } if (SUCCEEDED(hr)) { IPropertySetStorage *pss = (IPropertySetStorage*)DPA_GetPtr(_hdpaProps, _iCurrent); if (!pss) { hr = _CreateMemPropSetStorage(&pss); if (SUCCEEDED(hr)) { // fill in the NTFS or memory-based FMTID_ImageProperties if it doesn't already exist IPropertyStorage *pps; // we use CImagePropset to fill in the propertystorage when it is first created if (SUCCEEDED(pss->Create(FMTID_ImageProperties, &CLSID_NULL, PROPSETFLAG_DEFAULT, STGM_READWRITE|STGM_SHARE_EXCLUSIVE, &pps))) { CImagePropSet *ppsImg = new CImagePropSet(_pImage, NULL, pps, FMTID_ImageProperties); if (ppsImg) { ppsImg->SyncImagePropsToStorage(); ppsImg->Release(); } pps->Release(); } if (_guidFmt == ImageFormatJPEG || _guidFmt == ImageFormatTIFF) { // for now ignore failures here it's not a catastrophic problem if they aren't written if (SUCCEEDED(pss->Create(FMTID_SummaryInformation, &CLSID_NULL, PROPSETFLAG_DEFAULT, STGM_FAILIFTHERE|STGM_READWRITE|STGM_SHARE_EXCLUSIVE, &pps))) { CImagePropSet *ppsSummary = new CImagePropSet(_pImage, NULL, pps, FMTID_SummaryInformation); if (ppsSummary) { ppsSummary->SyncImagePropsToStorage(); ppsSummary->Release(); } pps->Release(); } } DPA_SetPtr(_hdpaProps, _iCurrent, pss); } } if (SUCCEEDED(hr) && ppss) { *ppss = pss; } } return hr; } // NOTE: ppps is an IN-OUT parameter HRESULT CImageData::_CreatePropStorage(IPropertyStorage **ppps, REFFMTID fmtid) { HRESULT hr = E_FAIL; if (_pImage) { CImagePropSet *ppsImg = new CImagePropSet(_pImage, this, *ppps, fmtid, _PropertyChanged); ATOMICRELEASE(*ppps); if (ppsImg) { hr = ppsImg->QueryInterface(IID_PPV_ARG(IPropertyStorage, ppps)); ppsImg->Release(); } else { hr = E_OUTOFMEMORY; } } return hr; } // IPropertySetStorage // // If the caller wants FMTID_ImageProperties use CImagePropSet // STDMETHODIMP CImageData::Create(REFFMTID fmtid, const CLSID *pclsid, DWORD grfFlags, DWORD grfMode, IPropertyStorage **pppropstg) { *pppropstg = NULL; IPropertySetStorage *pss; HRESULT hr= _EnsureProperties(&pss); if (SUCCEEDED(hr)) { if ((S_OK != IsEditable()) && (grfMode & (STGM_READWRITE | STGM_WRITE))) { hr = STG_E_ACCESSDENIED; } } if (SUCCEEDED(hr)) { IPropertyStorage *pps = NULL; hr = pss->Create(fmtid, pclsid, grfFlags, grfMode, &pps); if (SUCCEEDED(hr)) { hr = _CreatePropStorage(&pps, fmtid); } *pppropstg = pps; } return hr; } STDMETHODIMP CImageData::Open(REFFMTID fmtid, DWORD grfMode, IPropertyStorage **pppropstg) { *pppropstg = NULL; IPropertySetStorage *pss; HRESULT hr = _EnsureProperties(&pss); if (SUCCEEDED(hr)) { if ((S_OK != IsEditable()) && (grfMode & (STGM_READWRITE | STGM_WRITE))) { hr = STG_E_ACCESSDENIED; } } if (SUCCEEDED(hr)) { IPropertyStorage *pps = NULL; // special case FMTID_ImageSummaryInformation...it is readonly and not backed up // by a real property stream. if (FMTID_ImageSummaryInformation != fmtid) { hr = pss->Open(fmtid, grfMode, &pps); } if (SUCCEEDED(hr)) { hr = _CreatePropStorage(&pps, fmtid); } *pppropstg = pps; } return hr; } STDMETHODIMP CImageData::Delete(REFFMTID fmtid) { IPropertySetStorage *pss; HRESULT hr = _EnsureProperties(&pss); if (SUCCEEDED(hr)) { hr = pss->Delete(fmtid); } return hr; } STDMETHODIMP CImageData::Enum(IEnumSTATPROPSETSTG **ppenum) { IPropertySetStorage *pss; HRESULT hr = E_INVALIDARG; if (ppenum) { hr = _EnsureProperties(&pss); *ppenum = NULL; } if (SUCCEEDED(hr)) { IEnumSTATPROPSETSTG *pEnum; hr = pss->Enum(&pEnum); if (SUCCEEDED(hr)) { CFmtEnum *pFmtEnum = new CFmtEnum(pEnum); if (pFmtEnum) { hr = pFmtEnum->QueryInterface(IID_PPV_ARG(IEnumSTATPROPSETSTG, ppenum)); pEnum->Release(); pFmtEnum->Release(); } else { *ppenum = pEnum; } } } return hr; } // editing support void CImageData::_SetEditImage(Image *pimgEdit) { if (_pimgEdited) delete _pimgEdited; _pimgEdited = pimgEdit; } // valid input is 0, 90, 180, or 270 HRESULT CImageData::Rotate(DWORD dwAngle) { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { // this has bad effects on animated images so don't do it if (_fAnimated) return E_NOTVALIDFORANIMATEDIMAGE; RotateFlipType rft; switch (dwAngle) { case 0: hr = S_FALSE; break; case 90: rft = Rotate90FlipNone; break; case 180: rft = Rotate180FlipNone; break; case 270: rft = Rotate270FlipNone; break; default: hr = E_INVALIDARG; } if (S_OK == hr) { // get the current image we have displayed, ready to edit it. Image * pimg = _pimgEdited ? _pimgEdited->Clone() : _pImage->Clone(); if (pimg) { // In order to fix Windows bug #325413 GDIPlus needs to throw away any decoded frames // in memory for the cloned image. Therefore, we can no longer rely on // RotateFlip to flip the decoded frame already in memory and must explicitly // select it into the cloned image before calling RotateFlip to fix Windows Bug #368498 const CLSID * pclsidFrameDim = _fAnimated ? &FrameDimensionTime : &FrameDimensionPage; hr = HR_FROM_STATUS(pimg->SelectActiveFrame(pclsidFrameDim, _iCurrent)); if (SUCCEEDED(hr)) { hr = HR_FROM_STATUS(pimg->RotateFlip(rft)); if (SUCCEEDED(hr)) { _dwRotation = (_dwRotation + dwAngle) % 360; _SetEditImage(pimg); } } if (FAILED(hr)) { delete pimg; } } else { hr = E_OUTOFMEMORY; } } } return hr; } HRESULT CImageData::Scale(ULONG cx, ULONG cy, InterpolationMode hints) { HRESULT hr = _EnsureImage(); if (SUCCEEDED(hr)) { // this has bad effects on animated images if (_fAnimated) return E_NOTVALIDFORANIMATEDIMAGE; Image * pimg = _pimgEdited ? _pimgEdited : _pImage; // we have an image, lets determine the new size (preserving aspect ratio // and ensuring that we don't end up with a 0 sized image as a result. if (cy == 0) cy = MulDiv(pimg->GetHeight(), cx, pimg->GetWidth()); else if (cx == 0) cx = MulDiv(pimg->GetWidth(), cy, pimg->GetHeight()); cx = max(cx, 1); cy = max(cy, 1); // construct our new image and draw into it. Bitmap *pimgNew = new Bitmap(cx, cy); if (pimgNew) { Graphics g(pimgNew); g.SetInterpolationMode(hints); hr = HR_FROM_STATUS(g.DrawImage(pimg, Rect(0, 0, cx, cy), 0, 0, pimg->GetWidth(), pimg->GetHeight(), UnitPixel, NULL, QueryAbort, this)); // // GDI+ sometimes forgets to tell us it gave up due to an abort. // if (SUCCEEDED(hr) && QueryAbort(this)) hr = E_ABORT; if (SUCCEEDED(hr)) { pimgNew->SetResolution(pimg->GetHorizontalResolution(), pimg->GetVerticalResolution()); _SetEditImage(pimgNew); _fDestructive = TRUE; // the edit was Destructive } else { delete pimgNew; } } else { hr = E_OUTOFMEMORY; } } // Suspend the stream so we don't leave the file open _SuspendStream(); return hr; } HRESULT CImageData::DiscardEdit() { // NB: The following code is not valid in all cases. For example, if you rotated, then scaled, then rotated again // this code wouldn't work. We currently don't allow that scenario, so we shouldn't hit this problem, but it // could be an issue for others using this object so we should figure out what to do about it. This code works // if: 1.) your first edit is a scale, or 2.) you only do rotates. Also note, this code will clear the "dirty" bit // so it would prevent the image from being saved, thus the failure of this code won't effect the data on disk. if (_pimgEdited) { delete _pimgEdited; _pimgEdited = NULL; } _dwRotation = 0; _fDestructive = FALSE; return S_OK; } // handle persisting images HRESULT CImageData::SetEncoderParams(IPropertyBag *ppbEnc) { IUnknown_Set((IUnknown**)&_ppbEncoderParams, ppbEnc); return S_OK; } // save images to the given stream that we have, using the format ID we have void CImageData::_AddEncParameter(EncoderParameters *pep, GUID guidProperty, ULONG type, void *pv) { pep->Parameter[pep->Count].Guid = guidProperty; pep->Parameter[pep->Count].Type = type; pep->Parameter[pep->Count].NumberOfValues = 1; pep->Parameter[pep->Count].Value = pv; pep->Count++; } #define MAX_ENC_PARAMS 3 HRESULT CImageData::_SaveImages(IStream *pstrm, GUID * pguidFmt) { HRESULT hr = S_OK; int iQuality = 0; // == 0 is a special case // did the encoder specify a format for us to save in? ASSERTMSG(NULL != pguidFmt, "Invalid pguidFmt passed to internal function CImageData::_SaveImages"); GUID guidFmt = *pguidFmt; if (_ppbEncoderParams) { VARIANT var = {0}; // read the encoder format to be used if (SUCCEEDED(_ppbEncoderParams->Read(SHIMGKEY_RAWFORMAT, &var, NULL))) { VariantToGUID(&var, &guidFmt); VariantClear(&var); } // read the encoder quality to be used, this is set for the JPEG one only if (guidFmt == ImageFormatJPEG) { SHPropertyBag_ReadInt(_ppbEncoderParams, SHIMGKEY_QUALITY, &iQuality); iQuality = max(0, iQuality); iQuality = min(100, iQuality); } } // given the format GUID lets determine the encoder we intend to // use to save the image CLSID clsidEncoder; hr = _GetEncoderFromFormat(&guidFmt, &clsidEncoder); if (SUCCEEDED(hr)) { // the way encoding works with GDI+ is a bit strange, you first need to call an image to // have it save into a particular stream/file. if the image is multi-page then you // must set an encoder parameter which defines that this will be a multi-page save (and // that you will be calling the SaveAdd later). // // having performed the initial save, you must then attempt to add the subsequent pages // to the file by calling SaveAdd, you call that method on the first image you saved // specifying that you are adding another page (and possibly that this is the last image // in the series). BOOL bSaveCurrentOnly = FALSE; Image *pimgFirstSave = NULL; DWORD dwMaxPage = _cImages; DWORD dwMinPage = 0; // If viewing a multipage image and saving to a single page format, only save the current frame if (_cImages > 1 && !FmtSupportsMultiPage(this, &guidFmt)) { bSaveCurrentOnly = TRUE; dwMaxPage = _iCurrent+1; dwMinPage = _iCurrent; } for (DWORD i = dwMinPage; SUCCEEDED(hr) && (i < dwMaxPage); i++) { EncoderParameters ep[MAX_ENC_PARAMS] = { 0 }; ULONG ulCompression = 0; // in same scope as ep // we use _pImage as the source if unedited in order to preserve properties const CLSID * pclsidFrameDim = _fAnimated ? &FrameDimensionTime : &FrameDimensionPage; _pImage->SelectActiveFrame(pclsidFrameDim, i); Image *pimg; if (_pimgEdited && i==_iCurrent) { pimg = _pimgEdited; } else { pimg = _pImage; } _SaveFrameProperties(pimg, i); if (guidFmt == ImageFormatTIFF) { VARIANT var = {0}; if (SUCCEEDED(_GetProperty(PropertyTagCompression, &var, VT_UI2))) { // be sure to preserve TIFF compression // these values are taken from the TIFF spec switch (var.uiVal) { case 1: ulCompression = EncoderValueCompressionNone; break; case 2: ulCompression = EncoderValueCompressionCCITT3; break; case 3: ulCompression = EncoderValueCompressionCCITT4; break; case 5: ulCompression = EncoderValueCompressionLZW; break; case 32773: ulCompression = EncoderValueCompressionRle; break; default: // use the GDI+ default break; } VariantClear(&var); if (ulCompression) { _AddEncParameter(ep, EncoderCompression, EncoderParameterValueTypeLong, &ulCompression); } } } if (i == dwMinPage) { // we are writing the first page of the image, if this is a multi-page // image then we need to set the encoder parameters accordingly (eg. set to // multi-page). ULONG ulValue = 0; // This needs to be in scope when Save is called // We can only to lossless rotation when: // * The original image is a JPEG file // * The destination image is a JPEG file // * We are only rotating and not scaling // * The width and height of the JPEG are multiples of 8 // * Quality is unchanged by the caller if (!_fDestructive && IsEqualIID(_guidFmt, ImageFormatJPEG) && IsEqualIID(guidFmt, ImageFormatJPEG) && (iQuality == 0)) { // this code assumes JPEG files are single page since it's inside the i==0 case ASSERT(_cImages == 1); // for JPEG when doing only a rotate we use a special encoder parameter on the original // image rather than using the edit image. This allows lossless rotation. pimg = _pImage; switch (_dwRotation) { case 90: ulValue = EncoderValueTransformRotate90; break; case 180: ulValue = EncoderValueTransformRotate180; break; case 270: ulValue = EncoderValueTransformRotate270; break; } _AddEncParameter(ep, EncoderTransformation, EncoderParameterValueTypeLong, &ulValue); } else if (_cImages > 1 && !bSaveCurrentOnly) { ulValue = EncoderValueMultiFrame; _AddEncParameter(ep, EncoderSaveFlag, EncoderParameterValueTypeLong, &ulValue); pimgFirstSave = pimg; // keep this image as we will us it for appending pages } // JPEG quality is only ever set for a single image, therefore don't // bother passing it for the multi page case. if (iQuality > 0) _AddEncParameter(ep, EncoderQuality, EncoderParameterValueTypeLong, &iQuality); hr = HR_FROM_STATUS(pimg->Save(pstrm, &clsidEncoder, (ep->Count > 0) ? ep:NULL)); } else { // writing the next image in the series, set the encoding parameter // to indicate that this is the next page. if we are writing the last // image then set the last frame flag. ULONG flagValueDim = EncoderValueFrameDimensionPage; ULONG flagValueLastFrame = EncoderValueLastFrame; _AddEncParameter(ep, EncoderSaveFlag, EncoderParameterValueTypeLong, &flagValueDim); if (i == (dwMaxPage-1)) _AddEncParameter(ep, EncoderSaveFlag, EncoderParameterValueTypeLong, &flagValueLastFrame); hr = HR_FROM_STATUS(pimgFirstSave->SaveAdd(pimg, (ep->Count > 0) ? ep:NULL)); } } } // Suspend the stream so we don't leave the file open _SuspendStream(); return hr; } // returns the DPI of the image STDMETHODIMP CImageData::GetResolution(ULONG *puResolutionX, ULONG *puResolutionY) { if (!puResolutionX && !puResolutionY) { return E_INVALIDARG; } HRESULT hr = _EnsureImage(); if (puResolutionX) { *puResolutionX = 0; } if (puResolutionY) { *puResolutionY = 0; } if (SUCCEEDED(hr)) { UINT uFlags = _pImage->GetFlags(); // // We only return the DPI information from the image header for TIFFs whose // X and Y DPI differ, those images are likely faxes. // We want our client applications (slideshow, image preview) // to deal with actual pixel sizes for the most part // ULONG resX = (ULONG)_pImage->GetHorizontalResolution(); ULONG resY = (ULONG)_pImage->GetVerticalResolution(); #ifndef USE_EMBEDDED_DPI_ALWAYS if (_guidFmt != ImageFormatTIFF || !(uFlags & ImageFlagsHasRealDPI) || resX == resY ) { // if GetDC fails we have to rely on the numbers back from GDI+ HDC hdc = GetDC(NULL); if (hdc) { resX = GetDeviceCaps(hdc, LOGPIXELSX); resY = GetDeviceCaps(hdc, LOGPIXELSY); ReleaseDC(NULL, hdc); } } #endif if (puResolutionX) { *puResolutionX = resX; } if (puResolutionY) { *puResolutionY = resY; } if ((puResolutionX && !*puResolutionX) || (puResolutionY && !*puResolutionY)) { hr = E_FAIL; } } return hr; } // handle saving and replacing the original file // in the case of replacing an existing file, we want the temp file to be in the same volume // as the target HRESULT CImageData::_MakeTempFile(LPWSTR pszFile) { ASSERT(_pstrm); WCHAR szTempPath[MAX_PATH]; HRESULT hr = S_OK; if (_pstrm->IsFileStream()) { StrCpyN(szTempPath, _pstrm->GetFilename(), ARRAYSIZE(szTempPath)); PathRemoveFileSpec(szTempPath); } else if (!GetTempPath(ARRAYSIZE(szTempPath), szTempPath)) { hr = E_FAIL; } if (SUCCEEDED(hr)) { // SIV == "Shell Image Viewer" if (GetTempFileName(szTempPath, TEXT("SIV"), 0, pszFile)) { SetFileAttributes(pszFile, ATTRIBUTES_TEMPFILE); // we need to suppress the change notfy from the GetTempFileName() // call as that causes defview to display this. // but for some reason it does not work SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, pszFile, NULL); } else { hr = E_FAIL; } } return hr; } HRESULT CImageData::_ReplaceFile(LPCTSTR pszNewFile) { // first we get some info about the file we're replacing: LPCTSTR pszOldFile = _pstrm->GetFilename(); STATSTG ss = {0}; _pstrm->Stat(&ss, STATFLAG_NONAME); // This ensures that the source handle is closed _SuspendStream(); HRESULT hr; // ReplaceFile doesn't save the modified time, so if we rotate an image twice in quick succession // we won't add a full 2 seconds to the modified time. So query the time before replacing the file. WIN32_FIND_DATA wfd ={0}; GetFileAttributesEx(pszOldFile, GetFileExInfoStandard, &wfd); if (ReplaceFile(pszOldFile, pszNewFile, NULL, REPLACEFILE_WRITE_THROUGH, NULL, NULL)) { // The old file has been replaced with the new file, but now we need to ensure that the // filetime actually changed due to the 2 sec accuracy of FAT. // we do this on NTFS too, since XP pidls have 2 sec accuracy since they cast the filetime // down to a dos datetime. HANDLE hFile = CreateFile(pszOldFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (INVALID_HANDLE_VALUE != hFile) { FILETIME *pft = (CompareFileTime(&wfd.ftLastWriteTime, &ss.mtime) < 0) ? &ss.mtime : &wfd.ftLastWriteTime; IncrementFILETIME(pft, 2 * FT_ONESECOND); SetFileTime(hFile, NULL, NULL, pft); CloseHandle(hFile); } // the replacefile call wont always keep the "replaced" file (pszOldFile) attributes, if it's // replacing across a win98 share for example. no biggie, just set the attributes again, using // the attribs we know we got from the stat. SetFileAttributes(pszOldFile, ss.reserved); SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATH | SHCNF_FLUSHNOWAIT | SHCNF_FLUSH, pszOldFile, NULL); hr = S_OK; } else { hr = HRESULT_FROM_WIN32(GetLastError()); } return hr; } void SaveProperties(IPropertySetStorage *pss, Image *pimg, REFFMTID fmtid, CDSA *pdsaChanges) { IPropertyStorage *pps; if (SUCCEEDED(pss->Open(fmtid, STGM_READWRITE | STGM_SHARE_EXCLUSIVE, &pps))) { CImagePropSet *pips = new CImagePropSet(pimg, NULL, pps, fmtid); if (pips) { pips->SaveProps(pimg, pdsaChanges); pips->Release(); } pps->Release(); } } void CImageData::_SaveFrameProperties(Image *pimg, LONG iFrame) { // make sure _dsaChangedProps is non-NULL if (_hdpaProps && (HDSA)_dsaChangedProps) { IPropertySetStorage *pss = (IPropertySetStorage *)DPA_GetPtr(_hdpaProps, iFrame); if (pss) { // Start with FMTID_ImageProperties to make sure other FMTIDs take precedence (last one wins) SaveProperties(pss, pimg, FMTID_ImageProperties, &_dsaChangedProps); // enum all the property storages and create a CImagePropSet for each one // and have it save to the pimg IEnumSTATPROPSETSTG *penum; if (SUCCEEDED(pss->Enum(&penum))) { STATPROPSETSTG spss; while (S_OK == penum->Next(1, &spss, NULL)) { if (!IsEqualGUID(spss.fmtid, FMTID_ImageProperties)) { SaveProperties(pss, pimg, spss.fmtid, &_dsaChangedProps); } } penum->Release(); } } } } void CImageData::_PropertyChanged(IShellImageData* pThis, SHCOLUMNID *pscid) { ((CImageData*)pThis)->_fPropertyChanged = TRUE; if ((HDSA)(((CImageData*)pThis)->_dsaChangedProps)) { ((CImageData*)pThis)->_dsaChangedProps.AppendItem(pscid); } } // // This function determines the list of available encoder parameters given the file format // Hopefully future versions of GDI+ will decouple this call from the Image() object // Don't call this function until ready to save the loaded image STDMETHODIMP CImageData::GetEncoderParams(GUID *pguidFmt, EncoderParameters **ppencParams) { CLSID clsidEncoder; HRESULT hr = E_FAIL; if (_pImage && ppencParams) { hr = _GetEncoderFromFormat(pguidFmt, &clsidEncoder); } if (SUCCEEDED(hr)) { hr = E_FAIL; UINT uSize = _pImage->GetEncoderParameterListSize(&clsidEncoder); if (uSize) { *ppencParams = (EncoderParameters *)CoTaskMemAlloc(uSize); if (*ppencParams) { hr = HR_FROM_STATUS(_pImage->GetEncoderParameterList(&clsidEncoder, uSize, *ppencParams)); if (FAILED(hr)) { CoTaskMemFree(*ppencParams); *ppencParams = NULL; } } } } return hr; } STDMETHODIMP CImageData::RegisterAbort(IShellImageDataAbort *pAbort, IShellImageDataAbort **ppAbortPrev) { if (ppAbortPrev) { *ppAbortPrev = _pAbort; // Transfer ownership to caller } else if (_pAbort) { _pAbort->Release(); // Caller doesn't want it, so throw away } _pAbort = pAbort; // Set the new abort callback if (_pAbort) { _pAbort->AddRef(); } return S_OK; } BOOL CALLBACK CImageData::QueryAbort(void *pvRef) { CImageData* pThis = reinterpret_cast(pvRef); return pThis->_pAbort && pThis->_pAbort->QueryAbort() == S_FALSE; } HRESULT CImageData::CloneFrame(Image **ppimg) { *ppimg = NULL; Image *pimg = _pimgEdited ? _pimgEdited : _pImage; if (pimg) { *ppimg = pimg->Clone(); } return *ppimg ? S_OK : E_FAIL; } HRESULT CImageData::ReplaceFrame(Image *pimg) { _SetEditImage(pimg); return S_OK; } ///////////////////////////////////////////////////////////////////////////////////////////////// // CImageDataFactory ///////////////////////////////////////////////////////////////////////////////////////////////// STDAPI CImageDataFactory_CreateInstance(IUnknown* pUnkOuter, IUnknown** ppunk, LPCOBJECTINFO poi) { CImageFactory *psid = new CImageFactory(); if (!psid) { *ppunk = NULL; // incase of failure return E_OUTOFMEMORY; } HRESULT hr = psid->QueryInterface(IID_PPV_ARG(IUnknown, ppunk)); psid->Release(); return hr; } CImageFactory::CImageFactory() : _cRef(1) { _Module.Lock(); } CImageFactory::~CImageFactory() { _Module.Unlock(); } STDMETHODIMP CImageFactory::QueryInterface(REFIID riid, void **ppv) { static const QITAB qit[] = { QITABENT(CImageFactory, IShellImageDataFactory), { 0 }, }; return QISearch(this, qit, riid, ppv); } STDMETHODIMP_(ULONG) CImageFactory::AddRef() { return InterlockedIncrement(&_cRef); } STDMETHODIMP_(ULONG) CImageFactory::Release() { ASSERT( 0 != _cRef ); ULONG cRef = InterlockedDecrement(&_cRef); if ( 0 == cRef ) { delete this; } return cRef; } HRESULT CImageFactory::CreateIShellImageData(IShellImageData **ppshimg) { CImageData *psid = new CImageData(); if (!psid) return E_OUTOFMEMORY; HRESULT hr = psid->QueryInterface(IID_PPV_ARG(IShellImageData, ppshimg)); psid->Release(); return hr; } HRESULT CImageFactory::CreateImageFromFile(LPCWSTR pszPath, IShellImageData **ppshimg) { HRESULT hr = E_OUTOFMEMORY; CImageData *psid = new CImageData(); if (psid) { IPersistFile *ppf; hr = psid->QueryInterface(IID_PPV_ARG(IPersistFile, &ppf)); if (SUCCEEDED(hr)) { hr = ppf->Load(pszPath, STGM_READ); ppf->Release(); } if (SUCCEEDED(hr)) hr = psid->QueryInterface(IID_PPV_ARG(IShellImageData, ppshimg)); psid->Release(); } return hr; } HRESULT CImageFactory::CreateImageFromStream(IStream *pstrm, IShellImageData **ppshimg) { HRESULT hr = E_OUTOFMEMORY; CImageData *psid = new CImageData(); if (psid) { IPersistStream *ppstrm; hr = psid->QueryInterface(IID_PPV_ARG(IPersistStream, &ppstrm)); if (SUCCEEDED(hr)) { hr = ppstrm->Load(pstrm); ppstrm->Release(); } if (SUCCEEDED(hr)) hr = psid->QueryInterface(IID_PPV_ARG(IShellImageData, ppshimg)); psid->Release(); } return hr; } HRESULT CImageFactory::GetDataFormatFromPath(LPCWSTR pszPath, GUID *pguidFmt) { return _GetDataFormatFromPath(pszPath, pguidFmt); } HRESULT CEncoderInfo::_GetDataFormatFromPath(LPCWSTR pszPath, GUID *pguidFmt) { *pguidFmt = GUID_NULL; HRESULT hr = _GetEncoderList(); if (SUCCEEDED(hr)) { UINT i = FindInDecoderList(_pici, _cEncoders, pszPath); if (-1 != i) { *pguidFmt = _pici[i].FormatID; hr = S_OK; } else { hr = E_FAIL; } } return hr; } HRESULT CEncoderInfo::_GetEncoderList() { HRESULT hr = S_OK; if (!_pici) { // lets pick up the list of encoders, first we get the encoder size which // gives us the CB and the number of encoders that are installed on the // machine. UINT cb; hr = HR_FROM_STATUS(GetImageEncodersSize(&_cEncoders, &cb)); if (SUCCEEDED(hr)) { // allocate the buffer for the encoders and then fill it // with the encoder list. _pici = (ImageCodecInfo*)LocalAlloc(LPTR, cb); if (_pici) { hr = HR_FROM_STATUS(GetImageEncoders(_cEncoders, cb, _pici)); if (FAILED(hr)) { LocalFree(_pici); _pici = NULL; } } else { hr = E_OUTOFMEMORY; } } } return hr; } HRESULT CEncoderInfo::_GetEncoderFromFormat(const GUID *pfmt, CLSID *pclsidEncoder) { HRESULT hr = _GetEncoderList(); if (SUCCEEDED(hr)) { hr = E_FAIL; for (UINT i = 0; i != _cEncoders; i++) { if (_pici[i].FormatID == *pfmt) { if (pclsidEncoder) { *pclsidEncoder = _pici[i].Clsid; // return the CLSID of the encoder so we can create again } hr = S_OK; break; } } } return hr; } CEncoderInfo::CEncoderInfo() { _pici = NULL; _cEncoders = 0; } CEncoderInfo::~CEncoderInfo() { if (_pici) LocalFree(_pici); // do we have an encoder array to be destroyed } STDAPI CImageData_CreateInstance(IUnknown* pUnkOuter, IUnknown** ppunk, LPCOBJECTINFO poi) { CImageData *psid = new CImageData(*(poi->pclsid) == CLSID_ImagePropertyHandler); if (!psid) { *ppunk = NULL; // incase of failure return E_OUTOFMEMORY; } HRESULT hr = psid->QueryInterface(IID_PPV_ARG(IUnknown, ppunk)); psid->Release(); return hr; } int CImageData::_FreeProps(void* pProp, void* pData) { if (pProp) { ((IPropertySetStorage*)pProp)->Release(); } return 1; } // Our CFmtEnum is a minimal enumerator to provide FMTID_SummaryInformation in our // formats. It's optimized for 1-by-1 enumeration STDMETHODIMP CFmtEnum::Next(ULONG celt, STATPROPSETSTG *rgelt, ULONG *pceltFetched) { HRESULT hr = S_OK; if (pceltFetched) { *pceltFetched = 0; } if (!celt || !rgelt) { hr = E_INVALIDARG; } else if (0 == _idx) { ZeroMemory(rgelt, sizeof(*rgelt)); rgelt->fmtid = FMTID_ImageSummaryInformation; rgelt->grfFlags = STGM_READ | STGM_SHARE_DENY_NONE; if (pceltFetched) { *pceltFetched = 1; } _idx++; celt--; rgelt++; } if (SUCCEEDED(hr) && celt) { ULONG ul; hr = _pEnum->Next(celt, rgelt, &ul); if (SUCCEEDED(hr) && pceltFetched) { (*pceltFetched) += ul; } } return hr; } STDMETHODIMP CFmtEnum::Skip(ULONG celt) { HRESULT hr = S_OK; if (_idx == 0) { _idx++; celt--; } if (celt) { hr = _pEnum->Skip(celt); } return hr; } STDMETHODIMP CFmtEnum::Reset(void) { _idx = 0; return _pEnum->Reset(); } STDMETHODIMP CFmtEnum::Clone(IEnumSTATPROPSETSTG **ppenum) { HRESULT hr = E_OUTOFMEMORY; CFmtEnum *pNew = new CFmtEnum(_pEnum); if (pNew) { hr = pNew->QueryInterface(IID_PPV_ARG(IEnumSTATPROPSETSTG, ppenum)); pNew->Release(); } return hr; } STDMETHODIMP CFmtEnum::QueryInterface(REFIID riid, void **ppvObj) { static const QITAB qit[] = { QITABENT(CFmtEnum, IEnumSTATPROPSETSTG), { 0 }, }; return QISearch(this, qit, riid, ppvObj); } STDMETHODIMP_(ULONG) CFmtEnum::AddRef() { return InterlockedIncrement(&_cRef); } STDMETHODIMP_(ULONG) CFmtEnum::Release() { ASSERT( 0 != _cRef ); ULONG cRef = InterlockedDecrement(&_cRef); if ( 0 == cRef ) { delete this; } return cRef; } CFmtEnum::CFmtEnum(IEnumSTATPROPSETSTG *pEnum) : _cRef(1), _idx(0), _pEnum(pEnum) { _pEnum->AddRef(); } CFmtEnum::~CFmtEnum() { _pEnum->Release(); }