IconPathConverter.cpp
1 #include "pch.h" 2 #include "IconPathConverter.h" 3 #include "IconPathConverter.g.cpp" 4 5 #include "FontIconGlyphClassifier.h" 6 7 #include <Shlobj.h> 8 #include <Shlobj_core.h> 9 #include <wincodec.h> 10 11 namespace winrt 12 { 13 namespace MUX = Microsoft::UI::Xaml; 14 } 15 16 using namespace winrt::Windows; 17 using namespace winrt::Windows::UI::Xaml; 18 19 using namespace winrt::Windows::Graphics::Imaging; 20 using namespace winrt::Windows::Storage::Streams; 21 22 namespace winrt::Microsoft::Terminal::UI::implementation 23 { 24 // These are templates that help us figure out which BitmapIconSource/FontIconSource to use for a given IconSource. 25 // We have to do this because some of our code still wants to use WUX/MUX IconSources. 26 #pragma region BitmapIconSource 27 template<typename TIconSource> 28 struct BitmapIconSource 29 { 30 }; 31 32 template<> 33 struct BitmapIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource> 34 { 35 using type = winrt::Microsoft::UI::Xaml::Controls::BitmapIconSource; 36 }; 37 38 /*template<> 39 struct BitmapIconSource<winrt::Windows::UI::Xaml::Controls::IconSource> 40 { 41 using type = winrt::Windows::UI::Xaml::Controls::BitmapIconSource; 42 };*/ 43 #pragma endregion 44 45 #pragma region FontIconSource 46 template<typename TIconSource> 47 struct FontIconSource 48 { 49 }; 50 51 template<> 52 struct FontIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource> 53 { 54 using type = winrt::Microsoft::UI::Xaml::Controls::FontIconSource; 55 }; 56 57 /*template<> 58 struct FontIconSource<winrt::Windows::UI::Xaml::Controls::IconSource> 59 { 60 using type = winrt::Windows::UI::Xaml::Controls::FontIconSource; 61 };*/ 62 #pragma endregion 63 64 #pragma region PathIconSource 65 template<typename TIconSource> 66 struct PathIconSource 67 { 68 }; 69 70 template<> 71 struct PathIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource> 72 { 73 using type = winrt::Microsoft::UI::Xaml::Controls::PathIconSource; 74 }; 75 #pragma endregion 76 #pragma region ImageIconSource 77 template<typename TIconSource> 78 struct ImageIconSource 79 { 80 }; 81 82 template<> 83 struct ImageIconSource<winrt::Microsoft::UI::Xaml::Controls::IconSource> 84 { 85 using type = winrt::Microsoft::UI::Xaml::Controls::ImageIconSource; 86 }; 87 #pragma endregion 88 89 // Method Description: 90 // - Creates an IconSource for the given path. The icon returned is a colored 91 // icon. If we couldn't create the icon for any reason, we return an empty 92 // IconElement. 93 // Template Types: 94 // - <TIconSource>: The type of IconSource (MUX, WUX) to generate. 95 // Arguments: 96 // - path: the full, expanded path to the icon. 97 // Return Value: 98 // - An IconElement with its IconSource set, if possible. 99 template<typename TIconSource> 100 TIconSource _getColoredBitmapIcon(const winrt::hstring& path, bool monochrome) 101 { 102 // FontIcon uses glyphs in the private use area, whereas valid URIs only contain ASCII characters. 103 // To skip throwing on Uri construction, we can quickly check if the first character is ASCII. 104 if (!path.empty() && path.front() < 128) 105 { 106 try 107 { 108 winrt::Windows::Foundation::Uri iconUri{ path }; 109 110 if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg")) 111 { 112 typename ImageIconSource<TIconSource>::type iconSource; 113 winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri }; 114 iconSource.ImageSource(source); 115 return iconSource; 116 } 117 else 118 { 119 typename BitmapIconSource<TIconSource>::type iconSource; 120 // Make sure to set this to false, so we keep the RGB data of the 121 // image. Otherwise, the icon will be white for all the 122 // non-transparent pixels in the image. 123 iconSource.ShowAsMonochrome(monochrome); 124 iconSource.UriSource(iconUri); 125 return iconSource; 126 } 127 } 128 CATCH_LOG(); 129 } 130 131 return nullptr; 132 } 133 134 static winrt::hstring _expandIconPath(const hstring& iconPath) 135 { 136 if (iconPath.empty()) 137 { 138 return iconPath; 139 } 140 // winrt::hstring envExpandedPath{ wil::ExpandEnvironmentStringsW<std::wstring>(iconPath.c_str()) }; 141 winrt::hstring envExpandedPath{ iconPath }; 142 return envExpandedPath; 143 } 144 145 // Method Description: 146 // - Creates an IconSource for the given path. 147 // * If the icon is a path to an image, we'll use that. 148 // * If it isn't, then we'll try and use the text as a FontIcon. If the 149 // character is in the range of symbols reserved for the Segoe MDL2 150 // Asserts, well treat it as such. Otherwise, we'll default to a Sego 151 // UI icon, so things like emoji will work. 152 // * If we couldn't create the icon for any reason, we return an empty 153 // IconElement. 154 // Template Types: 155 // - <TIconSource>: The type of IconSource (MUX, WUX) to generate. 156 // Arguments: 157 // - path: the unprocessed path to the icon. 158 // Return Value: 159 // - An IconElement with its IconSource set, if possible. 160 template<typename TIconSource> 161 TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const winrt::hstring& fontFamily, const int targetSize) 162 { 163 TIconSource iconSource{ nullptr }; 164 165 if (iconPath.size() != 0) 166 { 167 const auto expandedIconPath{ _expandIconPath(iconPath) }; 168 iconSource = _getColoredBitmapIcon<TIconSource>(expandedIconPath, monochrome); 169 170 // If we fail to set the icon source using the "icon" as a path, 171 // let's try it as a symbol/emoji. 172 if (!iconSource) 173 { 174 try 175 { 176 const auto glyph_kind = FontIconGlyphClassifier::Classify(iconPath); 177 178 winrt::hstring family; 179 if (glyph_kind == FontIconGlyphKind::Invalid) 180 { 181 family = L"Segoe UI"; 182 } 183 else if (!fontFamily.empty()) 184 { 185 family = fontFamily; 186 } 187 else if (glyph_kind == FontIconGlyphKind::FluentSymbol) 188 { 189 family = L"Segoe Fluent Icons, Segoe MDL2 Assets"; 190 } 191 else if (glyph_kind == FontIconGlyphKind::Emoji) 192 { 193 // Emoji and other symbols go in the Segoe UI Emoji font. 194 // Some emojis (e.g. 2️⃣) would be rendered as emoji glyphs otherwise. 195 family = L"Segoe UI Emoji, Segoe UI"; 196 } 197 else 198 { 199 family = L"Segoe UI"; 200 } 201 202 typename FontIconSource<TIconSource>::type icon; 203 icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family }); 204 icon.FontSize(targetSize); 205 icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath); 206 iconSource = icon; 207 } 208 CATCH_LOG(); 209 } 210 } 211 212 if (!iconSource) 213 { 214 // Set the default IconSource to a BitmapIconSource with a null source 215 // (instead of just nullptr) because there's a really weird crash when swapping 216 // data bound IconSourceElements in a ListViewTemplate (i.e. CommandPalette). 217 // Swapping between nullptr IconSources and non-null IconSources causes a crash 218 // to occur, but swapping between IconSources with a null source and non-null IconSources 219 // work perfectly fine :shrug:. 220 typename BitmapIconSource<TIconSource>::type icon; 221 icon.UriSource(nullptr); 222 iconSource = icon; 223 } 224 225 return iconSource; 226 } 227 228 // Windows::UI::Xaml::Controls::IconSource IconPathConverter::IconSourceWUX(const hstring& path) 229 // { 230 // // * If the icon is a path to an image, we'll use that. 231 // // * If it isn't, then we'll try and use the text as a FontIcon. If the 232 // // character is in the range of symbols reserved for the Segoe MDL2 233 // // Asserts, well treat it as such. Otherwise, we'll default to a Segoe 234 // // UI icon, so things like emoji will work. 235 // return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false); 236 // } 237 238 static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const winrt::hstring& fontFamily, const int targetSize) 239 { 240 return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, fontFamily, targetSize); 241 } 242 243 static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon, 244 BitmapPixelFormat pixelFormat, 245 BitmapAlphaMode alphaMode, 246 IWICImagingFactory* imagingFactory) 247 { 248 // Load the icon into an IWICBitmap 249 wil::com_ptr<IWICBitmap> iconBitmap; 250 THROW_IF_FAILED(imagingFactory->CreateBitmapFromHICON(hicon, iconBitmap.put())); 251 252 // Put the IWICBitmap into a SoftwareBitmap. This may fail if WICBitmap's format is not supported by 253 // SoftwareBitmap. CreateBitmapFromHICON always creates RGBA8 so we're ok. 254 auto softwareBitmap = winrt::capture<SoftwareBitmap>( 255 winrt::create_instance<ISoftwareBitmapNativeFactory>(CLSID_SoftwareBitmapNativeFactory), 256 &ISoftwareBitmapNativeFactory::CreateFromWICBitmap, 257 iconBitmap.get(), 258 false); 259 260 // Convert the pixel format and alpha mode if necessary 261 if (softwareBitmap.BitmapPixelFormat() != pixelFormat || softwareBitmap.BitmapAlphaMode() != alphaMode) 262 { 263 softwareBitmap = SoftwareBitmap::Convert(softwareBitmap, pixelFormat, alphaMode); 264 } 265 266 return softwareBitmap; 267 } 268 269 static SoftwareBitmap _getBitmapFromIconFileAsync(const winrt::hstring& iconPath, 270 int32_t iconIndex, 271 uint32_t iconSize) 272 { 273 wil::unique_hicon hicon; 274 LOG_IF_FAILED(SHDefExtractIcon(iconPath.c_str(), iconIndex, 0, &hicon, nullptr, iconSize)); 275 276 if (!hicon) 277 { 278 return nullptr; 279 } 280 281 wil::com_ptr<IWICImagingFactory> wicImagingFactory; 282 THROW_IF_FAILED(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&wicImagingFactory))); 283 284 return _convertToSoftwareBitmap(hicon.get(), 285 BitmapPixelFormat::Bgra8, 286 BitmapAlphaMode::Premultiplied, 287 wicImagingFactory.get()); 288 } 289 290 // Method Description: 291 // - Attempt to get the icon index from the icon path provided 292 // Arguments: 293 // - iconPath: the full icon path, including the index if present 294 // - iconPathWithoutIndex: the place to store the icon path, sans the index if present 295 // Return Value: 296 // - nullopt if the iconPath is not an exe/dll/lnk file in the first place 297 // - 0 if the iconPath is an exe/dll/lnk file but does not contain an index (i.e. we default 298 // to the first icon in the file) 299 // - the icon index if the iconPath is an exe/dll/lnk file and contains an index 300 static std::optional<int> _getIconIndex(const winrt::hstring& iconPath, std::wstring_view& iconPathWithoutIndex) 301 { 302 const auto pathView = std::wstring_view{ iconPath }; 303 // Does iconPath have a comma in it? If so, split the string on the 304 // comma and look for the index and extension. 305 const auto commaIndex = pathView.find(L','); 306 307 // split the path on the comma 308 iconPathWithoutIndex = pathView.substr(0, commaIndex); 309 310 // It's an exe, dll, or lnk, so we need to extract the icon from the file. 311 if (!til::ends_with(iconPathWithoutIndex, L".exe") && 312 !til::ends_with(iconPathWithoutIndex, L".dll") && 313 !til::ends_with(iconPathWithoutIndex, L".lnk")) 314 { 315 return std::nullopt; 316 } 317 318 if (commaIndex != std::wstring::npos) 319 { 320 // Convert the string iconIndex to a signed int to support negative numbers which represent an Icon's ID. 321 const auto index{ til::to_int(pathView.substr(commaIndex + 1)) }; 322 if (index == til::to_int_error) 323 { 324 return std::nullopt; 325 } 326 return static_cast<int>(index); 327 } 328 329 // We had a binary path, but no index. Default to 0. 330 return 0; 331 } 332 333 static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex, 334 int index, 335 int targetSize) 336 { 337 // Try: 338 // * c:\Windows\System32\SHELL32.dll, 210 339 // * c:\Windows\System32\notepad.exe, 0 340 // * C:\Program Files\PowerShell\6-preview\pwsh.exe, 0 (this doesn't exist for me) 341 // * C:\Program Files\PowerShell\7\pwsh.exe, 0 342 343 const auto swBitmap{ _getBitmapFromIconFileAsync(winrt::hstring{ iconPathWithoutIndex }, index, targetSize) }; 344 if (swBitmap == nullptr) 345 { 346 return nullptr; 347 } 348 349 winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource bitmapSource{}; 350 bitmapSource.SetBitmapAsync(swBitmap); 351 return bitmapSource; 352 } 353 354 MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath, 355 const bool monochrome, 356 const winrt::hstring& fontFamily, 357 const int targetSize) 358 { 359 std::wstring_view iconPathWithoutIndex; 360 const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex); 361 if (!indexOpt.has_value()) 362 { 363 return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize); 364 } 365 366 const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize); 367 368 MUX::Controls::ImageIconSource imageIconSource{}; 369 imageIconSource.ImageSource(bitmapSource); 370 371 return imageIconSource; 372 } 373 374 Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath) { 375 return IconMUX(iconPath, 24); 376 } 377 Microsoft::UI::Xaml::Controls::IconElement IconPathConverter::IconMUX(const winrt::hstring& iconPath, const int targetSize) 378 { 379 std::wstring_view iconPathWithoutIndex; 380 const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex); 381 if (!indexOpt.has_value()) 382 { 383 auto source = IconSourceMUX(iconPath, false, L"", targetSize); 384 Microsoft::UI::Xaml::Controls::IconSourceElement icon; 385 icon.IconSource(source); 386 return icon; 387 } 388 389 const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize); 390 391 winrt::Microsoft::UI::Xaml::Controls::ImageIcon icon{}; 392 icon.Source(bitmapSource); 393 icon.Width(targetSize); 394 icon.Height(targetSize); 395 return icon; 396 } 397 398 }