From 329a41458416f633fb6ec016dc434b063cd92b9b Mon Sep 17 00:00:00 2001 From: Whindmar Saksit Date: Wed, 12 Feb 2025 20:57:51 +0100 Subject: [PATCH] [SHIMGVW] Choose a better icon image and also support .cur files (#7706) - Choose an icon image based on size and color and place it first so GDI+ will pick it - Change .cur files to .ico so GDI+ can open it CORE-19945 --- dll/win32/shimgvw/CMakeLists.txt | 1 + dll/win32/shimgvw/loader.cpp | 283 +++++++++++++++++++++++++++++++ dll/win32/shimgvw/shimgvw.c | 81 +++------ dll/win32/shimgvw/shimgvw.h | 43 +++++ 4 files changed, 350 insertions(+), 58 deletions(-) create mode 100644 dll/win32/shimgvw/loader.cpp diff --git a/dll/win32/shimgvw/CMakeLists.txt b/dll/win32/shimgvw/CMakeLists.txt index 1a4589abd43..15983aff150 100644 --- a/dll/win32/shimgvw/CMakeLists.txt +++ b/dll/win32/shimgvw/CMakeLists.txt @@ -3,6 +3,7 @@ spec2def(shimgvw.dll shimgvw.spec) list(APPEND SOURCE anime.c + loader.cpp shimgvw.c comsup.c shimgvw.rc diff --git a/dll/win32/shimgvw/loader.cpp b/dll/win32/shimgvw/loader.cpp new file mode 100644 index 00000000000..50112f6c486 --- /dev/null +++ b/dll/win32/shimgvw/loader.cpp @@ -0,0 +1,283 @@ +/* + * PROJECT: ReactOS Picture and Fax Viewer + * LICENSE: GPL-2.0 (https://spdx.org/licenses/GPL-2.0) + * PURPOSE: Image file browsing and manipulation + * COPYRIGHT: Copyright 2025 Whindmar Saksit + */ + +#include +#include +#include +using namespace Gdiplus; +#include "shimgvw.h" + +#define HResultFromWin32 SHIMGVW_HResultFromWin32 + +static HRESULT Read(HANDLE hFile, void* Buffer, DWORD Size) +{ + DWORD Transferred; + if (!ReadFile(hFile, Buffer, Size, &Transferred, NULL)) + return HResultFromWin32(GetLastError()); + return Size == Transferred ? S_OK : HResultFromWin32(ERROR_HANDLE_EOF); +} + +struct IMAGEINFO +{ + UINT w, h; + BYTE bpp; +}; + +class BitmapInfoHeader : public BITMAPINFOHEADER +{ +public: + BitmapInfoHeader() {} + BitmapInfoHeader(const void* pbmiHeader) { Initialize(pbmiHeader); } + + void Initialize(const void* pbmiHeader) + { + BITMAPINFOHEADER& bih = *(BITMAPINFOHEADER*)pbmiHeader; + if (bih.biSize >= sizeof(BITMAPINFOHEADER)) + { + CopyMemory(this, &bih, min(bih.biSize, sizeof(*this))); + } + else + { + ZeroMemory(this, sizeof(*this)); + BITMAPCOREHEADER& bch = *(BITMAPCOREHEADER*)pbmiHeader; + if (bih.biSize >= sizeof(BITMAPCOREHEADER)) + { + biSize = bch.bcSize; + biWidth = bch.bcWidth; + biHeight = bch.bcHeight; + biPlanes = bch.bcPlanes; + biBitCount = bch.bcBitCount; + biCompression = BI_RGB; + } + } + } +}; + +#include +union PNGSIGNATURE { UINT64 number; BYTE bytes[8]; }; +struct PNGCHUNKHEADER { UINT length, type; }; +struct PNGCHUNKFOOTER { UINT crc; }; +struct PNGIHDR { UINT w, h; BYTE depth, type, compression, filter, interlace; }; +struct PNGSIGANDIHDR +{ + PNGSIGNATURE sig; + PNGCHUNKHEADER chunkheader; + PNGIHDR ihdr; + PNGCHUNKFOOTER chunkfooter; +}; +struct PNGFOOTER { PNGCHUNKHEADER chunkheader; PNGCHUNKFOOTER footer; }; +#include + +static inline bool IsPngSignature(const void* buffer) +{ + const BYTE* p = (BYTE*)buffer; + return p[0] == 0x89 && p[1] == 'P' && p[2] == 'N' && p[3] == 'G' && + p[4] == 0x0D && p[5] == 0x0A && p[6] == 0x1A && p[7] == 0x0A; +} + +static inline bool IsPngSignature(const void* buffer, SIZE_T size) +{ + return size >= sizeof(PNGSIGNATURE) && IsPngSignature(buffer); +} + +static BYTE GetPngBppFromIHDRData(const void* buffer) +{ + static const BYTE channels[] = { 1, 0, 3, 1, 2, 0, 4 }; + const BYTE* p = (BYTE*)buffer, depth = p[8], type = p[8 + 1]; + return (depth <= 16 && type <= 6) ? channels[type] * depth : 0; +} + +static bool GetInfoFromPng(const void* file, SIZE_T size, IMAGEINFO& info) +{ + C_ASSERT(sizeof(PNGSIGNATURE) == 8); + C_ASSERT(sizeof(PNGSIGANDIHDR) == 8 + (4 + 4 + (4 + 4 + 5) + 4)); + + if (size > sizeof(PNGSIGANDIHDR) + sizeof(PNGFOOTER) && IsPngSignature(file)) + { + const UINT PNGIHDRSIG = 0x52444849; // Note: Big endian + const UINT* chunkhdr = (UINT*)((char*)file + sizeof(PNGSIGNATURE)); + if (BigToHost32(chunkhdr[0]) >= sizeof(PNGIHDR) && chunkhdr[1] == PNGIHDRSIG) + { + info.w = BigToHost32(chunkhdr[2]); + info.h = BigToHost32(chunkhdr[3]); + info.bpp = GetPngBppFromIHDRData(&chunkhdr[2]); + return info.bpp != 0; + } + } + return false; +} + +static bool GetInfoFromBmp(const void* pBitmapInfo, IMAGEINFO& info) +{ + BitmapInfoHeader bih(pBitmapInfo); + info.w = bih.biWidth; + info.h = abs((int)bih.biHeight); + UINT bpp = bih.biBitCount * bih.biPlanes; + info.bpp = LOBYTE(bpp); + return info.w && bpp == info.bpp; +} + +static bool GetInfoFromIcoBmp(const void* pBitmapInfo, IMAGEINFO& stat) +{ + bool ret = GetInfoFromBmp(pBitmapInfo, stat); + stat.h /= 2; // Don't include mask + return ret && stat.h; +} + +EXTERN_C PCWSTR GetExtraExtensionsGdipList(VOID) +{ + return L"*.CUR"; // "*.FOO;*.BAR" etc. +} + +static void OverrideFileContent(HGLOBAL& hMem, DWORD& Size) +{ + PBYTE buffer = (PBYTE)GlobalLock(hMem); + if (!buffer) + return; + + // TODO: We could try to load an ICO/PNG/BMP resource from a PE file here into buffer + + // ICO/CUR + struct ICOHDR { WORD Sig, Type, Count; }; + ICOHDR* pIcoHdr = (ICOHDR*)buffer; + if (Size > sizeof(ICOHDR) && !pIcoHdr->Sig && pIcoHdr->Type > 0 && pIcoHdr->Type < 3 && pIcoHdr->Count) + { + const UINT minbmp = sizeof(BITMAPCOREHEADER) + 1, minpng = sizeof(PNGSIGANDIHDR); + const UINT minfile = min(minbmp, minpng), count = pIcoHdr->Count; + struct ICOENTRY { BYTE w, h, pal, null; WORD planes, bpp; UINT size, offset; }; + ICOENTRY* entries = (ICOENTRY*)&pIcoHdr[1]; + if (Size - sizeof(ICOHDR) > (sizeof(ICOENTRY) + minfile) * count) + { + UINT64 best = 0; + int bestindex = -1; + // Inspect all the images and find the "best" image + for (UINT i = 0; i < count; ++i) + { + BOOL valid = FALSE; + IMAGEINFO info; + const BYTE* data = buffer + entries[i].offset; + if (IsPngSignature(data, entries[i].size)) + valid = GetInfoFromPng(data, entries[i].size, info); + else + valid = GetInfoFromIcoBmp(data, info); + + if (valid) + { + // Note: This treats bpp as more important compared to LookupIconIdFromDirectoryEx + UINT64 score = UINT64(info.w) * info.h * info.bpp; + if (score > best) + { + best = score; + bestindex = i; + } + } + } + if (bestindex >= 0) + { + if (pIcoHdr->Type == 2) + { + // GDI+ does not support .cur files, convert to .ico + pIcoHdr->Type = 1; +#if 0 // Because we are already overriding the order, we don't need to correct the ICOENTRY lookup info + for (UINT i = 0; i < count; ++i) + { + BitmapInfoHeader bih; + const BYTE* data = buffer + entries[i].offset; + if (IsPngSignature(data, entries[i].size)) + { + IMAGEINFO info; + if (!GetInfoFromPng(data, entries[i].size, info)) + continue; + bih.biPlanes = 1; + bih.biBitCount = info.bpp; + entries[i].pal = 0; + } + else + { + bih.Initialize(data); + entries[i].pal = bih.biPlanes * bih.biBitCount <= 8 ? bih.biClrUsed : 0; + } + entries[i].planes = (WORD)bih.biPlanes; + entries[i].bpp = (WORD)bih.biBitCount; + } +#endif + } +#if 0 + // Convert to a .ico with a single image + pIcoHdr->Count = 1; + const BYTE* data = buffer + entries[bestindex].offset; + entries[0] = entries[bestindex]; + entries[0].offset = (UINT)UINT_PTR((PBYTE)&entries[1] - buffer); + MoveMemory(buffer + entries[0].offset, data, entries[0].size); + Size = entries[0].offset + entries[0].size; +#else + // Place the best image first, GDI+ will return the first image + ICOENTRY temp = entries[0]; + entries[0] = entries[bestindex]; + entries[bestindex] = temp; +#endif + } + } + } + + GlobalUnlock(hMem); +} + +static HRESULT LoadImageFromStream(IStream* pStream, GpImage** ppImage) +{ + Status status = DllExports::GdipLoadImageFromStream(pStream, ppImage); + return HResultFromGdiplus(status); +} + +static HRESULT LoadImageFromFileHandle(HANDLE hFile, GpImage** ppImage) +{ + DWORD size = GetFileSize(hFile, NULL); + if (!size || size == INVALID_FILE_SIZE) + return HResultFromWin32(ERROR_NOT_SUPPORTED); + + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size); + if (!hMem) + return HResultFromWin32(ERROR_OUTOFMEMORY); + HRESULT hr = E_FAIL; + void* buffer = GlobalLock(hMem); + if (buffer) + { + hr = Read(hFile, buffer, size); + GlobalUnlock(hMem); + if (SUCCEEDED(hr)) + { + OverrideFileContent(hMem, size); + IStream* pStream; + if (SUCCEEDED(hr = CreateStreamOnHGlobal(hMem, TRUE, &pStream))) + { + // CreateStreamOnHGlobal does not know the real size, we do + pStream->SetSize(MakeULargeInteger(size)); + hr = LoadImageFromStream(pStream, ppImage); + pStream->Release(); // Calls GlobalFree + return hr; + } + } + } + GlobalFree(hMem); + return hr; +} + +EXTERN_C HRESULT LoadImageFromPath(LPCWSTR Path, GpImage** ppImage) +{ + // NOTE: GdipLoadImageFromFile locks the file. + // Avoid file locking by using GdipLoadImageFromStream and memory stream. + + HANDLE hFile = CreateFileW(Path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); + if (hFile != INVALID_HANDLE_VALUE) + { + HRESULT hr = LoadImageFromFileHandle(hFile, ppImage); + CloseHandle(hFile); + return hr; + } + return HResultFromWin32(GetLastError()); +} diff --git a/dll/win32/shimgvw/shimgvw.c b/dll/win32/shimgvw/shimgvw.c index c0ce4de2d39..7284266b94a 100644 --- a/dll/win32/shimgvw/shimgvw.c +++ b/dll/win32/shimgvw/shimgvw.c @@ -4,6 +4,7 @@ * PURPOSE: Image file browsing and manipulation * COPYRIGHT: Copyright Dmitry Chapyshev (dmitry@reactos.org) * Copyright 2018-2023 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com) + * Copyright 2025 Whindmar Saksit */ #include "shimgvw.h" @@ -13,6 +14,9 @@ #include #include +EXTERN_C PCWSTR GetExtraExtensionsGdipList(VOID); +EXTERN_C HRESULT LoadImageFromPath(LPCWSTR Path, GpImage** ppImage); + /* Toolbar image size */ #define TB_IMAGE_WIDTH 16 #define TB_IMAGE_HEIGHT 16 @@ -114,7 +118,6 @@ typedef struct tagPREVIEW_DATA UINT m_nTimerInterval; BOOL m_bHideCursor; POINT m_ptOrigin; - IStream *m_pMemStream; WCHAR m_szFile[MAX_PATH]; } PREVIEW_DATA, *PPREVIEW_DATA; @@ -371,67 +374,20 @@ Preview_pFreeImage(PPREVIEW_DATA pData) g_pImage = NULL; } - if (pData->m_pMemStream) - { - pData->m_pMemStream->lpVtbl->Release(pData->m_pMemStream); - pData->m_pMemStream = NULL; - } - pData->m_szFile[0] = UNICODE_NULL; } -IStream* MemStreamFromFile(LPCWSTR pszFileName) -{ - HANDLE hFile; - DWORD dwFileSize, dwRead; - LPBYTE pbMemFile = NULL; - IStream *pStream; - - hFile = CreateFileW(pszFileName, GENERIC_READ, FILE_SHARE_READ, NULL, - OPEN_EXISTING, 0, NULL); - if (hFile == INVALID_HANDLE_VALUE) - return NULL; - - dwFileSize = GetFileSize(hFile, NULL); - pbMemFile = QuickAlloc(dwFileSize, FALSE); - if (!dwFileSize || (dwFileSize == INVALID_FILE_SIZE) || !pbMemFile) - { - CloseHandle(hFile); - return NULL; - } - - if (!ReadFile(hFile, pbMemFile, dwFileSize, &dwRead, NULL) || (dwRead != dwFileSize)) - { - QuickFree(pbMemFile); - CloseHandle(hFile); - return NULL; - } - - CloseHandle(hFile); - pStream = SHCreateMemStream(pbMemFile, dwFileSize); - QuickFree(pbMemFile); - return pStream; -} - static VOID Preview_pLoadImage(PPREVIEW_DATA pData, LPCWSTR szOpenFileName) { + HRESULT hr; Preview_pFreeImage(pData); + InvalidateRect(pData->m_hwnd, NULL, FALSE); /* Schedule redraw in case we change to "No preview" */ - pData->m_pMemStream = MemStreamFromFile(szOpenFileName); - if (!pData->m_pMemStream) + hr = LoadImageFromPath(szOpenFileName, &g_pImage); + if (FAILED(hr)) { - DPRINT1("MemStreamFromFile() failed\n"); - Preview_UpdateTitle(pData, NULL); - return; - } - - /* NOTE: GdipLoadImageFromFile locks the file. - Avoid file locking by using GdipLoadImageFromStream and memory stream. */ - GdipLoadImageFromStream(pData->m_pMemStream, &g_pImage); - if (!g_pImage) - { - DPRINT1("GdipLoadImageFromStream() failed\n"); + DPRINT1("GdipLoadImageFromStream() failed, %d\n", hr); Preview_pFreeImage(pData); Preview_UpdateTitle(pData, NULL); return; @@ -439,8 +395,8 @@ Preview_pLoadImage(PPREVIEW_DATA pData, LPCWSTR szOpenFileName) Anime_LoadInfo(&pData->m_Anime); - SHAddToRecentDocs(SHARD_PATHW, szOpenFileName); GetFullPathNameW(szOpenFileName, _countof(pData->m_szFile), pData->m_szFile, NULL); + SHAddToRecentDocs(SHARD_PATHW, pData->m_szFile); /* Reset zoom and redraw display */ Preview_ResetZoom(pData); @@ -625,7 +581,7 @@ static SHIMGVW_FILENODE* pBuildFileList(LPCWSTR szFirstFile) { HANDLE hFindHandle; - WCHAR *extension; + WCHAR *extension, *buffer; WCHAR szSearchPath[MAX_PATH]; WCHAR szSearchMask[MAX_PATH]; WCHAR szFileTypes[MAX_PATH]; @@ -634,15 +590,19 @@ pBuildFileList(LPCWSTR szFirstFile) SHIMGVW_FILENODE *root = NULL; SHIMGVW_FILENODE *conductor = NULL; ImageCodecInfo *codecInfo; - UINT num; - UINT size; + UINT num = 0, size = 0, ExtraSize = 0; UINT j; + const PCWSTR ExtraExtensions = GetExtraExtensionsGdipList(); + const UINT ExtraCount = ExtraExtensions[0] ? 1 : 0; + if (ExtraCount) + ExtraSize += sizeof(*codecInfo) + (wcslen(ExtraExtensions) + 1) * sizeof(WCHAR); + StringCbCopyW(szSearchPath, sizeof(szSearchPath), szFirstFile); PathRemoveFileSpecW(szSearchPath); GdipGetImageDecodersSize(&num, &size); - codecInfo = QuickAlloc(size, FALSE); + codecInfo = QuickAlloc(size + ExtraSize, FALSE); if (!codecInfo) { DPRINT1("QuickAlloc() failed in pLoadFileList()\n"); @@ -650,6 +610,10 @@ pBuildFileList(LPCWSTR szFirstFile) } GdipGetImageDecoders(num, size, codecInfo); + buffer = (PWSTR)((UINT_PTR)codecInfo + size + (sizeof(*codecInfo) * ExtraCount)); + if (ExtraCount) + codecInfo[num].FilenameExtension = wcscpy(buffer, ExtraExtensions); + num += ExtraCount; root = QuickAlloc(sizeof(SHIMGVW_FILENODE), FALSE); if (!root) @@ -663,6 +627,7 @@ pBuildFileList(LPCWSTR szFirstFile) for (j = 0; j < num; ++j) { + // FIXME: Parse each FilenameExtension list to bypass szFileTypes limit StringCbCopyW(szFileTypes, sizeof(szFileTypes), codecInfo[j].FilenameExtension); extension = wcstok(szFileTypes, L";"); diff --git a/dll/win32/shimgvw/shimgvw.h b/dll/win32/shimgvw/shimgvw.h index 776da7f849c..c17a15bb8f2 100644 --- a/dll/win32/shimgvw/shimgvw.h +++ b/dll/win32/shimgvw/shimgvw.h @@ -78,3 +78,46 @@ static inline VOID QuickFree(LPVOID ptr) { HeapFree(GetProcessHeap(), 0, ptr); } + +static inline WORD Swap16(WORD v) +{ + return MAKEWORD(HIBYTE(v), LOBYTE(v)); +} + +static inline UINT Swap32(UINT v) +{ + return MAKELONG(Swap16(HIWORD(v)), Swap16(LOWORD(v))); +} + +#ifdef _WIN32 +#define BigToHost32 Swap32 +#endif + +static inline ULARGE_INTEGER MakeULargeInteger(UINT64 value) +{ + ULARGE_INTEGER ret; + ret.QuadPart = value; + return ret; +} + +static inline HRESULT SHIMGVW_HResultFromWin32(DWORD hr) +{ + // HRESULT_FROM_WIN32 will evaluate its parameter twice, this function will not. + return HRESULT_FROM_WIN32(hr); +} + +static inline HRESULT HResultFromGdiplus(Status status) +{ + switch ((UINT)status) + { + case Ok: return S_OK; + case InvalidParameter: return E_INVALIDARG; + case OutOfMemory: return E_OUTOFMEMORY; + case NotImplemented: return HRESULT_FROM_WIN32(ERROR_CALL_NOT_IMPLEMENTED); + case Win32Error: return SHIMGVW_HResultFromWin32(GetLastError()); + case FileNotFound: return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + case AccessDenied: return HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED); + case UnknownImageFormat: return HRESULT_FROM_WIN32(ERROR_BAD_FORMAT); + } + return E_FAIL; +}