/ src / modules / cmdpal / Microsoft.Terminal.UI / IconPathConverter.cpp
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  }