Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit b7504f8

Browse files
committedApr 14, 2025·
Update
1 parent 9ec5543 commit b7504f8

File tree

6 files changed

+365
-146
lines changed

6 files changed

+365
-146
lines changed
 

‎src/Files.App.CsWin32/NativeMethods.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ GdipGetImageEncodersSize
184184
GdipGetImageEncoders
185185
ImageFormatPNG
186186
ImageFormatJPEG
187-
EncoderParameters
188187
IStream
189188
CreateStreamOnHGlobal
190189
STATFLAG
@@ -193,3 +192,8 @@ GdipSaveImageToStream
193192
GdipGetImageRawFormat
194193
_TRANSFER_SOURCE_FLAGS
195194
E_NOTIMPL
195+
DeleteObject
196+
GdipDisposeImage
197+
DeleteObject
198+
OleInitialize
199+
OleUninitialize
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using Windows.Win32;
5+
6+
namespace Files.App.Storage
7+
{
8+
/// <summary>
9+
///
10+
/// </summary>
11+
public partial class STATask
12+
{
13+
public static Task Run(Action action)
14+
{
15+
var tcs = new TaskCompletionSource();
16+
17+
Thread thread =
18+
new(() =>
19+
{
20+
PInvoke.OleInitialize();
21+
22+
try
23+
{
24+
action();
25+
tcs.SetResult();
26+
}
27+
catch (Exception ex)
28+
{
29+
tcs.SetException(ex);
30+
}
31+
finally
32+
{
33+
PInvoke.OleUninitialize();
34+
}
35+
})
36+
{
37+
IsBackground = true,
38+
Priority = ThreadPriority.Normal
39+
};
40+
41+
thread.SetApartmentState(ApartmentState.STA);
42+
thread.Start();
43+
44+
return tcs.Task;
45+
}
46+
47+
public static Task<T> Run<T>(Func<T> func)
48+
{
49+
var tcs = new TaskCompletionSource<T>();
50+
51+
Thread thread =
52+
new(() =>
53+
{
54+
PInvoke.OleInitialize();
55+
56+
try
57+
{
58+
tcs.SetResult(func());
59+
}
60+
catch (Exception ex)
61+
{
62+
tcs.SetException(ex);
63+
}
64+
finally
65+
{
66+
PInvoke.OleUninitialize();
67+
}
68+
})
69+
{
70+
IsBackground = true,
71+
Priority = ThreadPriority.Normal
72+
};
73+
74+
thread.SetApartmentState(ApartmentState.STA);
75+
thread.Start();
76+
77+
return tcs.Task;
78+
}
79+
80+
public static Task Run(Func<Task> func)
81+
{
82+
var tcs = new TaskCompletionSource();
83+
84+
Thread thread =
85+
new(async () =>
86+
{
87+
PInvoke.OleInitialize();
88+
89+
try
90+
{
91+
await func();
92+
tcs.SetResult();
93+
}
94+
catch (Exception ex)
95+
{
96+
tcs.SetException(ex);
97+
}
98+
finally
99+
{
100+
PInvoke.OleUninitialize();
101+
}
102+
})
103+
{
104+
IsBackground = true,
105+
Priority = ThreadPriority.Normal
106+
};
107+
108+
thread.SetApartmentState(ApartmentState.STA);
109+
thread.Start();
110+
111+
return tcs.Task;
112+
}
113+
114+
public static Task<T?> Run<T>(Func<Task<T>> func)
115+
{
116+
var tcs = new TaskCompletionSource<T?>();
117+
118+
Thread thread =
119+
new(async () =>
120+
{
121+
PInvoke.OleInitialize();
122+
123+
try
124+
{
125+
tcs.SetResult(await func());
126+
}
127+
catch (Exception ex)
128+
{
129+
tcs.SetException(ex);
130+
}
131+
finally
132+
{
133+
PInvoke.OleUninitialize();
134+
}
135+
})
136+
{
137+
IsBackground = true,
138+
Priority = ThreadPriority.Normal
139+
};
140+
141+
thread.SetApartmentState(ApartmentState.STA);
142+
thread.Start();
143+
144+
return tcs.Task;
145+
}
146+
}
147+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using Windows.Win32;
5+
using Windows.Win32.Foundation;
6+
using Windows.Win32.System.SystemServices;
7+
using Windows.Win32.UI.Shell;
8+
9+
namespace Files.App.Storage.Storables
10+
{
11+
public static partial class WindowsStorableHelpers
12+
{
13+
public unsafe static HRESULT GetPropertyValue<TValue>(this IWindowsStorable storable, string propKey, out TValue value)
14+
{
15+
using ComPtr<IShellItem2> pShellItem2 = default;
16+
var shellItem2Iid = typeof(IShellItem2).GUID;
17+
HRESULT hr = storable.ThisPtr.Get()->QueryInterface(&shellItem2Iid, (void**)pShellItem2.GetAddressOf());
18+
hr = PInvoke.PSGetPropertyKeyFromName(propKey, out var originalPathPropertyKey);
19+
hr = pShellItem2.Get()->GetString(originalPathPropertyKey, out var szOriginalPath);
20+
21+
if (typeof(TValue) == typeof(string))
22+
{
23+
value = (TValue)(object)szOriginalPath.ToString();
24+
return hr;
25+
}
26+
else
27+
{
28+
value = default!;
29+
return HRESULT.E_FAIL;
30+
}
31+
}
32+
33+
public unsafe static bool HasShellAttributes(this IWindowsStorable storable, SFGAO_FLAGS attributes)
34+
{
35+
return storable.ThisPtr.Get()->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var returnedAttributes).Succeeded &&
36+
returnedAttributes == attributes;
37+
}
38+
39+
public unsafe static bool HasShellAttributes(this ComPtr<IShellItem> pShellItem, SFGAO_FLAGS attributes)
40+
{
41+
return pShellItem.Get()->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var returnedAttributes).Succeeded &&
42+
returnedAttributes == attributes;
43+
}
44+
45+
public unsafe static string GetDisplayName(this IWindowsStorable storable, SIGDN options = SIGDN.SIGDN_FILESYSPATH)
46+
{
47+
PWSTR pszName = default;
48+
HRESULT hr = storable.ThisPtr.Get()->GetDisplayName(options, &pszName);
49+
50+
return hr.ThrowIfFailedOnDebug().Succeeded
51+
? pszName.ToString()
52+
: string.Empty;
53+
}
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using System.Runtime.InteropServices;
5+
using Windows.Win32;
6+
using Windows.Win32.Foundation;
7+
using Windows.Win32.Graphics.Gdi;
8+
using Windows.Win32.Graphics.GdiPlus;
9+
using Windows.Win32.System.Com;
10+
using Windows.Win32.UI.Shell;
11+
12+
namespace Files.App.Storage.Storables
13+
{
14+
public static partial class WindowsStorableHelpers
15+
{
16+
private static (Guid Format, Guid Encorder)[]? GdiEncoders;
17+
18+
/// <inheritdoc cref="GetThumbnail"/>
19+
public static async Task<byte[]> GetThumbnailAsync(this IWindowsStorable storable, int size, SIIGBF options)
20+
{
21+
return await STATask.Run(() => storable.GetThumbnail(size, options));
22+
}
23+
24+
/// <summary>
25+
/// Retrieves a thumbnail image data for the specified <paramref name="storable"/> using <see cref="IShellItemImageFactory"/>.
26+
/// </summary>
27+
/// <param name="storable">An object that implements <see cref="IWindowsStorable"/> and represents a shell item on Windows.</param>
28+
/// <param name="size">The desired size (in pixels) of the thumbnail (width and height are equal).</param>
29+
/// <param name="options">A combination of <see cref="SIIGBF"/> flags that specify how the thumbnail should be retrieved.</param>
30+
/// <returns>A byte array containing the thumbnail image in its native format (e.g., PNG, JPEG).</returns>
31+
/// <remarks>If the thumbnail is JPEG, this tries to decoded as a PNG instead because JPEG loses data.</remarks>
32+
public unsafe static byte[] GetThumbnail(this IWindowsStorable storable, int size, SIIGBF options)
33+
{
34+
using ComPtr<IShellItemImageFactory> pShellItemImageFactory = storable.ThisPtr.As<IShellItemImageFactory>();
35+
if (pShellItemImageFactory.IsNull)
36+
return [];
37+
38+
// Get HBITMAP
39+
HBITMAP hBitmap = default;
40+
HRESULT hr = pShellItemImageFactory.Get()->GetImage(new(size, size), options, &hBitmap);
41+
if (hr.ThrowIfFailedOnDebug().Failed)
42+
{
43+
if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
44+
return [];
45+
}
46+
47+
// Convert to GpBitmap of GDI+
48+
GpBitmap* gpBitmap = default;
49+
PInvoke.GdipCreateBitmapFromHBITMAP(hBitmap, HPALETTE.Null, &gpBitmap);
50+
51+
// Get an encoder for PNG
52+
Guid format = Guid.Empty;
53+
if (PInvoke.GdipGetImageRawFormat((GpImage*)gpBitmap, &format) is not Status.Ok)
54+
{
55+
if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
56+
if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
57+
return [];
58+
}
59+
60+
Guid encoder = GetEncoderClsid(format);
61+
if (format == PInvoke.ImageFormatJPEG || encoder == Guid.Empty)
62+
{
63+
format = PInvoke.ImageFormatPNG;
64+
encoder = GetEncoderClsid(format);
65+
}
66+
67+
using ComPtr<IStream> pStream = default;
68+
hr = PInvoke.CreateStreamOnHGlobal(HGLOBAL.Null, true, pStream.GetAddressOf());
69+
if (hr.ThrowIfFailedOnDebug().Failed)
70+
{
71+
if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
72+
if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
73+
return [];
74+
}
75+
76+
if (PInvoke.GdipSaveImageToStream((GpImage*)gpBitmap, pStream.Get(), &encoder, (EncoderParameters*)null) is not Status.Ok)
77+
{
78+
if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
79+
if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
80+
return [];
81+
}
82+
83+
STATSTG stat = default;
84+
hr = pStream.Get()->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME);
85+
if (hr.ThrowIfFailedOnDebug().Failed)
86+
{
87+
if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
88+
if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
89+
return [];
90+
}
91+
92+
ulong statSize = stat.cbSize & 0xFFFFFFFF;
93+
byte* RawThumbnailData = (byte*)NativeMemory.Alloc((nuint)statSize);
94+
95+
pStream.Get()->Seek(0L, (SystemIO.SeekOrigin)STREAM_SEEK.STREAM_SEEK_SET, null);
96+
hr = pStream.Get()->Read(RawThumbnailData, (uint)statSize);
97+
if (hr.ThrowIfFailedOnDebug().Failed)
98+
{
99+
if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
100+
if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
101+
if (RawThumbnailData is not null) NativeMemory.Free(RawThumbnailData);
102+
return [];
103+
}
104+
105+
byte[] thumbnailData = new ReadOnlySpan<byte>(RawThumbnailData, (int)statSize / sizeof(byte)).ToArray();
106+
NativeMemory.Free(RawThumbnailData);
107+
108+
return thumbnailData;
109+
110+
Guid GetEncoderClsid(Guid format)
111+
{
112+
foreach ((Guid Format, Guid Encoder) in GetGdiEncoders())
113+
if (Format == format)
114+
return Encoder;
115+
116+
return Guid.Empty;
117+
}
118+
119+
(Guid Format, Guid Encorder)[] GetGdiEncoders()
120+
{
121+
if (GdiEncoders is not null)
122+
return GdiEncoders;
123+
124+
if (PInvoke.GdipGetImageEncodersSize(out var numEncoders, out var size) is not Status.Ok)
125+
return [];
126+
127+
ImageCodecInfo* pImageCodecInfo = (ImageCodecInfo*)NativeMemory.Alloc(size);
128+
129+
if (PInvoke.GdipGetImageEncoders(numEncoders, size, pImageCodecInfo) is not Status.Ok)
130+
return [];
131+
132+
ReadOnlySpan<ImageCodecInfo> codecs = new(pImageCodecInfo, (int)numEncoders);
133+
GdiEncoders = new (Guid Format, Guid Encoder)[codecs.Length];
134+
for (int index = 0; index < codecs.Length; index++)
135+
GdiEncoders[index] = (codecs[index].FormatID, codecs[index].Clsid);
136+
137+
return GdiEncoders;
138+
}
139+
}
140+
141+
/// <inheritdoc cref="GetThumbnail"/>
142+
public static async Task<byte[]> GetThumbnailOverlayAsync(this IWindowsStorable storable)
143+
{
144+
return await STATask.Run(() => storable.GetThumbnailOverlay());
145+
}
146+
147+
/// <summary>
148+
/// Retrieves a thumbnail overlay image data for the specified <paramref name="storable"/> using <see cref="IShellIconOverlayManager"/>.
149+
/// </summary>
150+
/// <param name="storable">An object that implements <see cref="IWindowsStorable"/> and represents a shell item on Windows.</param>
151+
/// <returns>A byte array containing the thumbnail overlay image in its native format (e.g., PNG, JPEG).</returns>
152+
public unsafe static byte[] GetThumbnailOverlay(this IWindowsStorable storable)
153+
{
154+
return [];
155+
}
156+
}
157+
}

‎src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.cs

-143
This file was deleted.

‎src/Files.App/Services/Storage/StorageTrashBinService.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,7 @@ public async Task<bool> RestoreAllTrashesAsync()
112112
{
113113
item.GetPropertyValue("System.Recycle.DeletedFrom", out string originalLocationFolderPath);
114114

115-
if (WindowsStorable.TryParse(originalLocationFolderPath, out var originalLocationItem) &&
116-
originalLocationItem is WindowsFolder originalLocationFolder)
115+
if (WindowsStorable.TryParse(originalLocationFolderPath) is WindowsFolder originalLocationFolder)
117116
hr = bulkOperations.QueueMoveOperation(item, originalLocationFolder, null).ThrowIfFailedOnDebug();
118117
}
119118

0 commit comments

Comments
 (0)
Please sign in to comment.