|
2 | 2 | // Licensed under the MIT License. |
3 | 3 |
|
4 | 4 | using System.Collections.Concurrent; |
| 5 | +using System.Runtime.CompilerServices; |
5 | 6 | using System.Runtime.InteropServices; |
6 | 7 | using Windows.Win32; |
7 | 8 | using Windows.Win32.Foundation; |
@@ -44,168 +45,145 @@ public unsafe static HRESULT TryGetThumbnail(this IWindowsStorable storable, int |
44 | 45 | { |
45 | 46 | thumbnailData = null; |
46 | 47 |
|
47 | | - using ComPtr<IShellItemImageFactory> pShellItemImageFactory = default; |
48 | | - storable.ThisPtr->QueryInterface(IID.IID_IShellItemImageFactory, (void**)pShellItemImageFactory.GetAddressOf()); |
49 | | - if (pShellItemImageFactory.IsNull) |
50 | | - return HRESULT.E_NOINTERFACE; |
51 | | - |
52 | | - // Get HBITMAP |
53 | 48 | HBITMAP hBitmap = default; |
54 | | - HRESULT hr = pShellItemImageFactory.Get()->GetImage(new(size, size), options, &hBitmap); |
55 | | - if (hr.ThrowIfFailedOnDebug().Failed) |
56 | | - { |
57 | | - if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap); |
58 | | - return hr; |
59 | | - } |
| 49 | + GpBitmap* gpBitmap = null; |
60 | 50 |
|
61 | | - // Retrieve BITMAP data |
62 | | - BITMAP bmp = default; |
63 | | - if (PInvoke.GetObject(hBitmap, sizeof(BITMAP), &bmp) is 0) |
| 51 | + try |
64 | 52 | { |
65 | | - if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap); |
66 | | - return HRESULT.E_FAIL; |
67 | | - } |
| 53 | + using ComPtr<IShellItemImageFactory> pShellItemImageFactory = default; |
| 54 | + storable.ThisPtr->QueryInterface(IID.IID_IShellItemImageFactory, (void**)pShellItemImageFactory.GetAddressOf()); |
| 55 | + if (pShellItemImageFactory.IsNull) |
| 56 | + return HRESULT.E_NOINTERFACE; |
68 | 57 |
|
69 | | - // Allocate buffer for flipped pixel data |
70 | | - byte* flippedBits = (byte*)NativeMemory.AllocZeroed((nuint)(bmp.bmWidthBytes * bmp.bmHeight)); |
| 58 | + HRESULT hr = pShellItemImageFactory.Get()->GetImage(new(size, size), options, &hBitmap); |
| 59 | + if (hr.ThrowIfFailedOnDebug().Failed) return hr; |
71 | 60 |
|
72 | | - // Flip the image manually row by row |
73 | | - for (int y = 0; y < bmp.bmHeight; y++) |
74 | | - { |
75 | | - Buffer.MemoryCopy( |
76 | | - (byte*)bmp.bmBits + y * bmp.bmWidthBytes, |
77 | | - flippedBits + (bmp.bmHeight - y - 1) * bmp.bmWidthBytes, |
78 | | - bmp.bmWidthBytes, |
79 | | - bmp.bmWidthBytes |
80 | | - ); |
81 | | - } |
| 61 | + gpBitmap = ConvertHBITMAPToGpBitmap(hBitmap); |
| 62 | + if (gpBitmap is null) return HRESULT.E_FAIL; |
82 | 63 |
|
83 | | - // Create GpBitmap from the flipped pixel data |
84 | | - GpBitmap* gpBitmap = default; |
85 | | - if (PInvoke.GdipCreateBitmapFromScan0(bmp.bmWidth, bmp.bmHeight, bmp.bmWidthBytes, PInvoke.PixelFormat32bppARGB, flippedBits, &gpBitmap) != Status.Ok) |
86 | | - { |
87 | | - if (flippedBits is not null) NativeMemory.Free(flippedBits); |
88 | | - if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap); |
89 | | - return HRESULT.E_FAIL; |
| 64 | + return (thumbnailData = ConvertGpBitmapToByteArray(gpBitmap)) is null ? HRESULT.E_FAIL : HRESULT.S_OK; |
90 | 65 | } |
91 | | - |
92 | | - if (!TryConvertGpBitmapToByteArray(gpBitmap, out thumbnailData)) |
| 66 | + finally |
93 | 67 | { |
| 68 | + if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); |
94 | 69 | if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap); |
95 | | - return HRESULT.E_FAIL; |
96 | 70 | } |
97 | | - |
98 | | - if (flippedBits is not null) NativeMemory.Free(flippedBits); |
99 | | - if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap); |
100 | | - |
101 | | - return HRESULT.S_OK; |
102 | 71 | } |
103 | 72 |
|
104 | 73 | public unsafe static HRESULT TryExtractImageFromDll(this IWindowsStorable storable, int size, int index, out byte[]? imageData) |
105 | 74 | { |
106 | 75 | DllIconCache ??= []; |
107 | 76 | imageData = null; |
| 77 | + HICON hIcon = default; |
| 78 | + GpBitmap* gpBitmap = default; |
108 | 79 |
|
109 | 80 | if (storable.ToString() is not { } path) |
110 | 81 | return HRESULT.E_INVALIDARG; |
111 | 82 |
|
112 | | - if (DllIconCache.TryGetValue((path, index, size), out var cachedImageData)) |
113 | | - { |
114 | | - imageData = cachedImageData; |
115 | | - return HRESULT.S_OK; |
116 | | - } |
117 | | - else |
| 83 | + try |
118 | 84 | { |
119 | | - HICON hIcon = default; |
120 | | - HRESULT hr = default; |
121 | | - |
122 | | - fixed (char* pszPath = path) |
123 | | - hr = PInvoke.SHDefExtractIcon(pszPath, -1 * index, 0, &hIcon, null, (uint)size); |
124 | | - |
125 | | - if (hr.ThrowIfFailedOnDebug().Failed) |
| 85 | + if (DllIconCache.TryGetValue((path, index, size), out var cachedImageData)) |
126 | 86 | { |
127 | | - if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); |
128 | | - return hr; |
| 87 | + imageData = cachedImageData; |
| 88 | + return HRESULT.S_OK; |
129 | 89 | } |
130 | | - |
131 | | - // Convert to GpBitmap of GDI+ |
132 | | - GpBitmap* gpBitmap = default; |
133 | | - if (PInvoke.GdipCreateBitmapFromHICON(hIcon, &gpBitmap) is not Status.Ok) |
| 90 | + else |
134 | 91 | { |
135 | | - if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); |
136 | | - return HRESULT.E_FAIL; |
137 | | - } |
| 92 | + HRESULT hr = default; |
138 | 93 |
|
139 | | - if (!TryConvertGpBitmapToByteArray(gpBitmap, out imageData) || imageData is null) |
140 | | - { |
141 | | - if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); |
142 | | - return HRESULT.E_FAIL; |
143 | | - } |
| 94 | + fixed (char* pszPath = path) |
| 95 | + hr = PInvoke.SHDefExtractIcon(pszPath, -1 * index, 0, &hIcon, null, (uint)size); |
| 96 | + if (hr.ThrowIfFailedOnDebug().Failed) return hr; |
144 | 97 |
|
145 | | - DllIconCache[(path, index, size)] = imageData; |
146 | | - if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); |
| 98 | + // Convert to GpBitmap of GDI+ |
| 99 | + if (PInvoke.GdipCreateBitmapFromHICON(hIcon, &gpBitmap) is not Status.Ok) return HRESULT.E_FAIL; |
| 100 | + |
| 101 | + imageData = ConvertGpBitmapToByteArray(gpBitmap); |
| 102 | + if (imageData is null) return HRESULT.E_FAIL; |
147 | 103 |
|
148 | | - return HRESULT.S_OK; |
| 104 | + DllIconCache[(path, index, size)] = imageData; |
| 105 | + |
| 106 | + return HRESULT.S_OK; |
| 107 | + } |
149 | 108 | } |
| 109 | + finally |
| 110 | + { |
| 111 | + if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); |
| 112 | + } |
| 113 | + |
150 | 114 | } |
151 | 115 |
|
152 | | - public unsafe static bool TryConvertGpBitmapToByteArray(GpBitmap* gpBitmap, out byte[]? imageData) |
| 116 | + public unsafe static GpBitmap* ConvertHBITMAPToGpBitmap(HBITMAP hBitmap) |
153 | 117 | { |
154 | | - imageData = null; |
| 118 | + BITMAP bmp = default; |
| 119 | + byte* flippedBits = null; |
| 120 | + GpBitmap* gpBitmap = null; |
155 | 121 |
|
156 | | - // Get an encoder for PNG |
157 | | - Guid format = Guid.Empty; |
158 | | - if (PInvoke.GdipGetImageRawFormat((GpImage*)gpBitmap, &format) is not Status.Ok) |
| 122 | + try |
159 | 123 | { |
160 | | - if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); |
161 | | - return false; |
162 | | - } |
| 124 | + // Retrieve BITMAP data |
| 125 | + if (PInvoke.GetObject(hBitmap, sizeof(BITMAP), &bmp) is 0) return null; |
163 | 126 |
|
164 | | - Guid encoder = GetEncoderClsid(format); |
165 | | - if (format == PInvoke.ImageFormatJPEG || encoder == Guid.Empty) |
166 | | - { |
167 | | - format = PInvoke.ImageFormatPNG; |
168 | | - encoder = GetEncoderClsid(format); |
169 | | - } |
| 127 | + // Flip the image manually row by row |
| 128 | + flippedBits = (byte*)NativeMemory.AllocZeroed((nuint)(bmp.bmWidthBytes * bmp.bmHeight)); |
| 129 | + for (int y = 0; y < bmp.bmHeight; y++) |
| 130 | + Buffer.MemoryCopy((byte*)bmp.bmBits + y * bmp.bmWidthBytes, flippedBits + (bmp.bmHeight - y - 1) * bmp.bmWidthBytes, bmp.bmWidthBytes, bmp.bmWidthBytes); |
170 | 131 |
|
171 | | - using ComPtr<IStream> pStream = default; |
172 | | - HRESULT hr = PInvoke.CreateStreamOnHGlobal(HGLOBAL.Null, true, pStream.GetAddressOf()); |
173 | | - if (hr.ThrowIfFailedOnDebug().Failed) |
174 | | - { |
175 | | - if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); |
176 | | - return false; |
177 | | - } |
| 132 | + // Create GpBitmap from the flipped pixel data |
| 133 | + Status status = PInvoke.GdipCreateBitmapFromScan0(bmp.bmWidth, bmp.bmHeight, bmp.bmWidthBytes, PInvoke.PixelFormat32bppARGB, flippedBits, &gpBitmap); |
| 134 | + if (status is not Status.Ok) return null; |
178 | 135 |
|
179 | | - if (PInvoke.GdipSaveImageToStream((GpImage*)gpBitmap, pStream.Get(), &encoder, (EncoderParameters*)null) is not Status.Ok) |
180 | | - { |
181 | | - if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); |
182 | | - return false; |
| 136 | + return gpBitmap; |
183 | 137 | } |
184 | | - |
185 | | - STATSTG stat = default; |
186 | | - hr = pStream.Get()->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME); |
187 | | - if (hr.ThrowIfFailedOnDebug().Failed) |
| 138 | + finally |
188 | 139 | { |
189 | | - if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); |
190 | | - return false; |
| 140 | + if (flippedBits is not null) NativeMemory.Free(flippedBits); |
191 | 141 | } |
| 142 | + } |
192 | 143 |
|
193 | | - ulong statSize = stat.cbSize & 0xFFFFFFFF; |
194 | | - byte* RawThumbnailData = (byte*)NativeMemory.Alloc((nuint)statSize); |
| 144 | + public unsafe static byte[]? ConvertGpBitmapToByteArray(GpBitmap* gpBitmap) |
| 145 | + { |
| 146 | + byte* pRawThumbnailData = null; |
195 | 147 |
|
196 | | - pStream.Get()->Seek(0L, (SystemIO.SeekOrigin)STREAM_SEEK.STREAM_SEEK_SET, null); |
197 | | - hr = pStream.Get()->Read(RawThumbnailData, (uint)statSize); |
198 | | - if (hr.ThrowIfFailedOnDebug().Failed) |
| 148 | + try |
199 | 149 | { |
200 | | - if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); |
201 | | - if (RawThumbnailData is not null) NativeMemory.Free(RawThumbnailData); |
202 | | - return false; |
203 | | - } |
| 150 | + // Get an encoder for PNG |
| 151 | + Guid format = Guid.Empty; |
| 152 | + Status status = PInvoke.GdipGetImageRawFormat((GpImage*)gpBitmap, &format); |
| 153 | + if (status is not Status.Ok) return null; |
204 | 154 |
|
205 | | - imageData = new ReadOnlySpan<byte>(RawThumbnailData, (int)statSize / sizeof(byte)).ToArray(); |
206 | | - NativeMemory.Free(RawThumbnailData); |
| 155 | + Guid encoder = GetEncoderClsid(format); |
| 156 | + if (format == PInvoke.ImageFormatJPEG || encoder == Guid.Empty) |
| 157 | + { |
| 158 | + // Default to PNG |
| 159 | + format = PInvoke.ImageFormatPNG; |
| 160 | + encoder = GetEncoderClsid(format); |
| 161 | + } |
207 | 162 |
|
208 | | - return true; |
| 163 | + using ComPtr<IStream> pStream = default; |
| 164 | + HRESULT hr = PInvoke.CreateStreamOnHGlobal(HGLOBAL.Null, true, pStream.GetAddressOf()); |
| 165 | + if (hr.ThrowIfFailedOnDebug().Failed) return null; |
| 166 | + |
| 167 | + status = PInvoke.GdipSaveImageToStream((GpImage*)gpBitmap, pStream.Get(), &encoder, (EncoderParameters*)null); |
| 168 | + if (status is not Status.Ok) return null; |
| 169 | + |
| 170 | + STATSTG stat = default; |
| 171 | + hr = pStream.Get()->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME); |
| 172 | + if (hr.ThrowIfFailedOnDebug().Failed) return null; |
| 173 | + |
| 174 | + ulong statSize = stat.cbSize & 0xFFFFFFFF; |
| 175 | + pRawThumbnailData = (byte*)NativeMemory.Alloc((nuint)statSize); |
| 176 | + |
| 177 | + pStream.Get()->Seek(0L, (SystemIO.SeekOrigin)STREAM_SEEK.STREAM_SEEK_SET, null); |
| 178 | + hr = pStream.Get()->Read(pRawThumbnailData, (uint)statSize); |
| 179 | + if (hr.ThrowIfFailedOnDebug().Failed) return null; |
| 180 | + |
| 181 | + return new ReadOnlySpan<byte>(pRawThumbnailData, (int)statSize / sizeof(byte)).ToArray(); |
| 182 | + } |
| 183 | + finally |
| 184 | + { |
| 185 | + if (pRawThumbnailData is not null) NativeMemory.Free(pRawThumbnailData); |
| 186 | + } |
209 | 187 |
|
210 | 188 | Guid GetEncoderClsid(Guid format) |
211 | 189 | { |
|
0 commit comments