xref: /reactos/dll/win32/shimgvw/loader.cpp (revision bc52d5f1)
1 /*
2  * PROJECT:     ReactOS Picture and Fax Viewer
3  * LICENSE:     GPL-2.0 (https://spdx.org/licenses/GPL-2.0)
4  * PURPOSE:     Image file browsing and manipulation
5  * COPYRIGHT:   Copyright 2025 Whindmar Saksit <whindsaks@proton.me>
6  */
7 
8 #include <windows.h>
9 #include <objbase.h>
10 #include <gdiplus.h>
11 using namespace Gdiplus;
12 #include "shimgvw.h"
13 
14 #define HResultFromWin32 SHIMGVW_HResultFromWin32
15 
Read(HANDLE hFile,void * Buffer,DWORD Size)16 static HRESULT Read(HANDLE hFile, void* Buffer, DWORD Size)
17 {
18     DWORD Transferred;
19     if (!ReadFile(hFile, Buffer, Size, &Transferred, NULL))
20         return HResultFromWin32(GetLastError());
21     return Size == Transferred ? S_OK : HResultFromWin32(ERROR_HANDLE_EOF);
22 }
23 
24 struct IMAGESTATS
25 {
26     UINT w, h;
27     BYTE bpp;
28 };
29 
30 class BitmapInfoHeader : public BITMAPINFOHEADER
31 {
32 public:
BitmapInfoHeader()33     BitmapInfoHeader() {}
BitmapInfoHeader(const void * pbmiHeader)34     BitmapInfoHeader(const void* pbmiHeader) { Initialize(pbmiHeader); }
35 
Initialize(const void * pbmiHeader)36     void Initialize(const void* pbmiHeader)
37     {
38         BITMAPINFOHEADER& bih = *(BITMAPINFOHEADER*)pbmiHeader;
39         if (bih.biSize >= sizeof(BITMAPINFOHEADER))
40         {
41             CopyMemory(this, &bih, min(bih.biSize, sizeof(*this)));
42         }
43         else
44         {
45             ZeroMemory(this, sizeof(*this));
46             BITMAPCOREHEADER& bch = *(BITMAPCOREHEADER*)pbmiHeader;
47             if (bih.biSize >= sizeof(BITMAPCOREHEADER))
48             {
49                 biSize = bch.bcSize;
50                 biWidth = bch.bcWidth;
51                 biHeight = bch.bcHeight;
52                 biPlanes = bch.bcPlanes;
53                 biBitCount = bch.bcBitCount;
54                 biCompression = BI_RGB;
55             }
56         }
57     }
58 };
59 
60 #include <pshpack1.h>
61 union PNGSIGNATURE { UINT64 number; BYTE bytes[8]; };
62 struct PNGCHUNKHEADER { UINT length, type; };
63 struct PNGCHUNKFOOTER { UINT crc; };
64 struct PNGIHDR { UINT w, h; BYTE depth, type, compression, filter, interlace; };
65 struct PNGSIGANDIHDR
66 {
67     PNGSIGNATURE sig;
68     PNGCHUNKHEADER chunkheader;
69     PNGIHDR ihdr;
70     PNGCHUNKFOOTER chunkfooter;
71 };
72 struct PNGFOOTER { PNGCHUNKHEADER chunkheader; PNGCHUNKFOOTER footer; };
73 #include <poppack.h>
74 
IsPngSignature(const void * buffer)75 static inline bool IsPngSignature(const void* buffer)
76 {
77     const BYTE* p = (BYTE*)buffer;
78     return p[0] == 0x89 && p[1] == 'P' && p[2] == 'N' && p[3] == 'G' &&
79            p[4] == 0x0D && p[5] == 0x0A && p[6] == 0x1A && p[7] == 0x0A;
80 }
81 
IsPngSignature(const void * buffer,SIZE_T size)82 static inline bool IsPngSignature(const void* buffer, SIZE_T size)
83 {
84     return size >= sizeof(PNGSIGNATURE) && IsPngSignature(buffer);
85 }
86 
GetPngBppFromIHDRData(const void * buffer)87 static BYTE GetPngBppFromIHDRData(const void* buffer)
88 {
89     static const BYTE channels[] = { 1, 0, 3, 1, 2, 0, 4 };
90     const BYTE* p = (BYTE*)buffer, depth = p[8], type = p[8 + 1];
91     return (depth <= 16 && type <= 6) ? channels[type] * depth : 0;
92 }
93 
GetInfoFromPng(const void * file,SIZE_T size,IMAGESTATS & info)94 static bool GetInfoFromPng(const void* file, SIZE_T size, IMAGESTATS& info)
95 {
96     C_ASSERT(sizeof(PNGSIGNATURE) == 8);
97     C_ASSERT(sizeof(PNGSIGANDIHDR) == 8 + (4 + 4 + (4 + 4 + 5) + 4));
98 
99     if (size > sizeof(PNGSIGANDIHDR) + sizeof(PNGFOOTER) && IsPngSignature(file))
100     {
101         const UINT PNGIHDRSIG = 0x52444849; // Note: Big endian
102         const UINT* chunkhdr = (UINT*)((char*)file + sizeof(PNGSIGNATURE));
103         if (BigToHost32(chunkhdr[0]) >= sizeof(PNGIHDR) && chunkhdr[1] == PNGIHDRSIG)
104         {
105             info.w = BigToHost32(chunkhdr[2]);
106             info.h = BigToHost32(chunkhdr[3]);
107             info.bpp = GetPngBppFromIHDRData(&chunkhdr[2]);
108             return info.bpp != 0;
109         }
110     }
111     return false;
112 }
113 
GetInfoFromBmp(const void * pBitmapInfo,IMAGESTATS & info)114 static bool GetInfoFromBmp(const void* pBitmapInfo, IMAGESTATS& info)
115 {
116     BitmapInfoHeader bih(pBitmapInfo);
117     info.w = bih.biWidth;
118     info.h = abs((int)bih.biHeight);
119     UINT bpp = bih.biBitCount * bih.biPlanes;
120     info.bpp = LOBYTE(bpp);
121     return info.w && bpp == info.bpp;
122 }
123 
GetInfoFromIcoBmp(const void * pBitmapInfo,IMAGESTATS & info)124 static bool GetInfoFromIcoBmp(const void* pBitmapInfo, IMAGESTATS& info)
125 {
126     bool ret = GetInfoFromBmp(pBitmapInfo, info);
127     info.h /= 2; // Don't include mask
128     return ret && info.h;
129 }
130 
GetExtraExtensionsGdipList(VOID)131 EXTERN_C PCWSTR GetExtraExtensionsGdipList(VOID)
132 {
133     return L"*.CUR"; // "*.FOO;*.BAR" etc.
134 }
135 
OverrideFileContent(HGLOBAL & hMem,DWORD & Size)136 static void OverrideFileContent(HGLOBAL& hMem, DWORD& Size)
137 {
138     PBYTE buffer = (PBYTE)GlobalLock(hMem);
139     if (!buffer)
140         return;
141 
142     // TODO: We could try to load an ICO/PNG/BMP resource from a PE file here into buffer
143 
144     // ICO/CUR
145     struct ICOHDR { WORD Sig, Type, Count; };
146     ICOHDR* pIcoHdr = (ICOHDR*)buffer;
147     if (Size > sizeof(ICOHDR) && !pIcoHdr->Sig && pIcoHdr->Type > 0 && pIcoHdr->Type < 3 && pIcoHdr->Count)
148     {
149         const UINT minbmp = sizeof(BITMAPCOREHEADER) + 1, minpng = sizeof(PNGSIGANDIHDR);
150         const UINT minfile = min(minbmp, minpng), count = pIcoHdr->Count;
151         struct ICOENTRY { BYTE w, h, pal, null; WORD planes, bpp; UINT size, offset; };
152         ICOENTRY* entries = (ICOENTRY*)&pIcoHdr[1];
153         if (Size - sizeof(ICOHDR) > (sizeof(ICOENTRY) + minfile) * count)
154         {
155             UINT64 best = 0;
156             int bestindex = -1;
157             // Inspect all the images and find the "best" image
158             for (UINT i = 0; i < count; ++i)
159             {
160                 BOOL valid = FALSE;
161                 IMAGESTATS info;
162                 const BYTE* data = buffer + entries[i].offset;
163                 if (IsPngSignature(data, entries[i].size))
164                     valid = GetInfoFromPng(data, entries[i].size, info);
165                 else
166                     valid = GetInfoFromIcoBmp(data, info);
167 
168                 if (valid)
169                 {
170                     // Note: This treats bpp as more important compared to LookupIconIdFromDirectoryEx
171                     UINT64 score = UINT64(info.w) * info.h * info.bpp;
172                     if (score > best)
173                     {
174                         best = score;
175                         bestindex = i;
176                     }
177                 }
178             }
179             if (bestindex >= 0)
180             {
181                 if (pIcoHdr->Type == 2)
182                 {
183                     // GDI+ does not support .cur files, convert to .ico
184                     pIcoHdr->Type = 1;
185 #if 0               // Because we are already overriding the order, we don't need to correct the ICOENTRY lookup info
186                     for (UINT i = 0; i < count; ++i)
187                     {
188                         BitmapInfoHeader bih;
189                         const BYTE* data = buffer + entries[i].offset;
190                         if (IsPngSignature(data, entries[i].size))
191                         {
192                             IMAGESTATS info;
193                             if (!GetInfoFromPng(data, entries[i].size, info))
194                                 continue;
195                             bih.biPlanes = 1;
196                             bih.biBitCount = info.bpp;
197                             entries[i].pal = 0;
198                         }
199                         else
200                         {
201                             bih.Initialize(data);
202                             entries[i].pal = bih.biPlanes * bih.biBitCount <= 8 ? bih.biClrUsed : 0;
203                         }
204                         entries[i].planes = (WORD)bih.biPlanes;
205                         entries[i].bpp = (WORD)bih.biBitCount;
206                     }
207 #endif
208                 }
209 #if 0
210                 // Convert to a .ico with a single image
211                 pIcoHdr->Count = 1;
212                 const BYTE* data = buffer + entries[bestindex].offset;
213                 entries[0] = entries[bestindex];
214                 entries[0].offset = (UINT)UINT_PTR((PBYTE)&entries[1] - buffer);
215                 MoveMemory(buffer + entries[0].offset, data, entries[0].size);
216                 Size = entries[0].offset + entries[0].size;
217 #else
218                 // Place the best image first, GDI+ will return the first image
219                 ICOENTRY temp = entries[0];
220                 entries[0] = entries[bestindex];
221                 entries[bestindex] = temp;
222 #endif
223             }
224         }
225     }
226 
227     GlobalUnlock(hMem);
228 }
229 
LoadImageFromStream(IStream * pStream,GpImage ** ppImage)230 static HRESULT LoadImageFromStream(IStream* pStream, GpImage** ppImage)
231 {
232     Status status = DllExports::GdipLoadImageFromStream(pStream, ppImage);
233     return HResultFromGdiplus(status);
234 }
235 
LoadImageFromFileHandle(HANDLE hFile,GpImage ** ppImage)236 static HRESULT LoadImageFromFileHandle(HANDLE hFile, GpImage** ppImage)
237 {
238     DWORD size = GetFileSize(hFile, NULL);
239     if (!size || size == INVALID_FILE_SIZE)
240         return HResultFromWin32(ERROR_NOT_SUPPORTED);
241 
242     HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, size);
243     if (!hMem)
244         return HResultFromWin32(ERROR_OUTOFMEMORY);
245     HRESULT hr = E_FAIL;
246     void* buffer = GlobalLock(hMem);
247     if (buffer)
248     {
249         hr = Read(hFile, buffer, size);
250         GlobalUnlock(hMem);
251         if (SUCCEEDED(hr))
252         {
253             OverrideFileContent(hMem, size);
254             IStream* pStream;
255             if (SUCCEEDED(hr = CreateStreamOnHGlobal(hMem, TRUE, &pStream)))
256             {
257                 // CreateStreamOnHGlobal does not know the real size, we do
258                 pStream->SetSize(MakeULargeInteger(size));
259                 hr = LoadImageFromStream(pStream, ppImage);
260                 pStream->Release(); // Calls GlobalFree
261                 return hr;
262             }
263         }
264     }
265     GlobalFree(hMem);
266     return hr;
267 }
268 
LoadImageFromPath(LPCWSTR Path,GpImage ** ppImage)269 EXTERN_C HRESULT LoadImageFromPath(LPCWSTR Path, GpImage** ppImage)
270 {
271     // NOTE: GdipLoadImageFromFile locks the file.
272     //       Avoid file locking by using GdipLoadImageFromStream and memory stream.
273 
274     HANDLE hFile = CreateFileW(Path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE,
275                                NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
276     if (hFile != INVALID_HANDLE_VALUE)
277     {
278         HRESULT hr = LoadImageFromFileHandle(hFile, ppImage);
279         CloseHandle(hFile);
280         return hr;
281     }
282     return HResultFromWin32(GetLastError());
283 }
284