UWPApplication.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System;
  6  using System.Collections.Generic;
  7  using System.Diagnostics;
  8  using System.Globalization;
  9  using System.IO;
 10  using System.IO.Abstractions;
 11  using System.Linq;
 12  using System.Reflection;
 13  using System.Runtime.InteropServices;
 14  using System.Text;
 15  using System.Threading.Tasks;
 16  using System.Windows;
 17  using System.Windows.Input;
 18  using System.Windows.Media;
 19  using System.Windows.Media.Imaging;
 20  using System.Xml;
 21  
 22  using ManagedCommon;
 23  using Microsoft.Plugin.Program.Logger;
 24  using Wox.Infrastructure;
 25  using Wox.Infrastructure.Image;
 26  using Wox.Plugin;
 27  using Wox.Plugin.Common;
 28  using Wox.Plugin.Common.Win32;
 29  using Wox.Plugin.Logger;
 30  
 31  using PackageVersion = Microsoft.Plugin.Program.Programs.UWP.PackageVersion;
 32  
 33  namespace Microsoft.Plugin.Program.Programs
 34  {
 35      [Serializable]
 36      public class UWPApplication : IProgram
 37      {
 38          private static readonly IFileSystem FileSystem = new FileSystem();
 39          private static readonly IPath Path = FileSystem.Path;
 40          private static readonly IFile File = FileSystem.File;
 41  
 42          public string AppListEntry { get; set; }
 43  
 44          public string UniqueIdentifier { get; set; }
 45  
 46          public string DisplayName { get; set; }
 47  
 48          public string Description { get; set; }
 49  
 50          public string UserModelId { get; set; }
 51  
 52          public string BackgroundColor { get; set; }
 53  
 54          public string EntryPoint { get; set; }
 55  
 56          public string Name => DisplayName;
 57  
 58          public string Location => Package.Location;
 59  
 60          // Localized path based on windows display language
 61          public string LocationLocalized => Package.LocationLocalized;
 62  
 63          public bool Enabled { get; set; }
 64  
 65          public bool CanRunElevated { get; set; }
 66  
 67          public string LogoPath { get; set; }
 68  
 69          public LogoType LogoType { get; set; }
 70  
 71          public UWP Package { get; set; }
 72  
 73          private string logoUri;
 74  
 75          private const string ContrastWhite = "contrast-white";
 76  
 77          private const string ContrastBlack = "contrast-black";
 78  
 79          // Function to calculate the score of a result
 80          private int Score(string query)
 81          {
 82              var displayNameMatch = StringMatcher.FuzzySearch(query, DisplayName);
 83              var descriptionMatch = StringMatcher.FuzzySearch(query, Description);
 84              var score = new[] { displayNameMatch.Score, descriptionMatch.Score / 2 }.Max();
 85              return score;
 86          }
 87  
 88          // Function to set the subtitle based on the Type of application
 89          private static string SetSubtitle()
 90          {
 91              return Properties.Resources.powertoys_run_plugin_program_packaged_application;
 92          }
 93  
 94          public Result Result(string query, string queryArguments, IPublicAPI api)
 95          {
 96              ArgumentNullException.ThrowIfNull(api);
 97  
 98              var score = Score(query);
 99              if (score <= 0)
100              { // no need to create result if score is 0
101                  return null;
102              }
103  
104              var result = new Result
105              {
106                  SubTitle = SetSubtitle(),
107                  Icon = Logo,
108                  Score = score,
109                  ContextData = this,
110                  ProgramArguments = queryArguments,
111                  Action = e =>
112                  {
113                      Launch(api, queryArguments);
114                      return true;
115                  },
116              };
117  
118              // To set the title to always be the display name of the packaged application
119              result.Title = DisplayName;
120              result.TitleHighlightData = StringMatcher.FuzzySearch(query, Name).MatchData;
121  
122              // Using CurrentCulture since this is user facing
123              var toolTipTitle = result.Title;
124              var toolTipText = LocationLocalized;
125              result.ToolTipData = new ToolTipData(toolTipTitle, toolTipText);
126  
127              return result;
128          }
129  
130          public List<ContextMenuResult> ContextMenus(string queryArguments, IPublicAPI api)
131          {
132              ArgumentNullException.ThrowIfNull(api);
133  
134              var contextMenus = new List<ContextMenuResult>();
135  
136              if (CanRunElevated)
137              {
138                  contextMenus.Add(
139                          new ContextMenuResult
140                          {
141                              PluginName = Assembly.GetExecutingAssembly().GetName().Name,
142                              Title = Properties.Resources.wox_plugin_program_run_as_administrator,
143                              Glyph = "\xE7EF",
144                              FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
145                              AcceleratorKey = Key.Enter,
146                              AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
147                              Action = _ =>
148                              {
149                                  string command = "shell:AppsFolder\\" + UniqueIdentifier;
150                                  command = Environment.ExpandEnvironmentVariables(command.Trim());
151  
152                                  var info = ShellCommand.SetProcessStartInfo(command, verb: "runas");
153                                  info.UseShellExecute = true;
154                                  info.Arguments = queryArguments;
155                                  Process.Start(info);
156                                  return true;
157                              },
158                          });
159  
160                  // We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
161              }
162  
163              contextMenus.Add(
164                  new ContextMenuResult
165                  {
166                      PluginName = Assembly.GetExecutingAssembly().GetName().Name,
167                      Title = Properties.Resources.wox_plugin_program_open_containing_folder,
168                      Glyph = "\xE838",
169                      FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
170                      AcceleratorKey = Key.E,
171                      AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
172                      Action = _ =>
173                      {
174                          Helper.OpenInShell(Package.Location);
175  
176                          return true;
177                      },
178                  });
179  
180              contextMenus.Add(new ContextMenuResult
181              {
182                  PluginName = Assembly.GetExecutingAssembly().GetName().Name,
183                  Title = Properties.Resources.wox_plugin_program_open_in_console,
184                  Glyph = "\xE756",
185                  FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
186                  AcceleratorKey = Key.C,
187                  AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
188                  Action = (context) =>
189                  {
190                      try
191                      {
192                          Helper.OpenInConsole(Package.Location);
193                          return true;
194                      }
195                      catch (Exception e)
196                      {
197                          Log.Exception($"Failed to open {Name} in console, {e.Message}", e, GetType());
198                          return false;
199                      }
200                  },
201              });
202  
203              return contextMenus;
204          }
205  
206          private async void Launch(IPublicAPI api, string queryArguments)
207          {
208              var appManager = new ApplicationActivationHelper.ApplicationActivationManager();
209              const ApplicationActivationHelper.ActivateOptions noFlags = ApplicationActivationHelper.ActivateOptions.None;
210              await Task.Run(() =>
211              {
212                  try
213                  {
214                      appManager.ActivateApplication(UserModelId, queryArguments, noFlags, out var unusedPid);
215                  }
216                  catch (Exception ex)
217                  {
218                      ProgramLogger.Exception($"Unable to launch UWP {DisplayName}", ex, MethodBase.GetCurrentMethod().DeclaringType, queryArguments);
219                      var name = "Plugin: " + Properties.Resources.wox_plugin_program_plugin_name;
220                      var message = $"{Properties.Resources.powertoys_run_plugin_program_uwp_failed}: {DisplayName}";
221                      api.ShowMsg(name, message, string.Empty);
222                  }
223              }).ConfigureAwait(false);
224          }
225  
226          public UWPApplication(IAppxManifestApplication manifestApp, UWP package)
227          {
228              ArgumentNullException.ThrowIfNull(manifestApp);
229  
230              var hr = manifestApp.GetAppUserModelId(out var tmpUserModelId);
231              UserModelId = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUserModelId);
232  
233              hr = manifestApp.GetAppUserModelId(out var tmpUniqueIdentifier);
234              UniqueIdentifier = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUniqueIdentifier);
235  
236              hr = manifestApp.GetStringValue("DisplayName", out var tmpDisplayName);
237              DisplayName = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDisplayName);
238  
239              hr = manifestApp.GetStringValue("Description", out var tmpDescription);
240              Description = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDescription);
241  
242              hr = manifestApp.GetStringValue("BackgroundColor", out var tmpBackgroundColor);
243              BackgroundColor = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpBackgroundColor);
244  
245              hr = manifestApp.GetStringValue("EntryPoint", out var tmpEntryPoint);
246              EntryPoint = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpEntryPoint);
247  
248              Package = package ?? throw new ArgumentNullException(nameof(package));
249  
250              DisplayName = ResourceFromPri(package.FullName, DisplayName);
251              Description = ResourceFromPri(package.FullName, Description);
252              logoUri = LogoUriFromManifest(manifestApp);
253  
254              Enabled = true;
255              CanRunElevated = IfApplicationCanRunElevated();
256          }
257  
258          private bool IfApplicationCanRunElevated()
259          {
260              if (EntryPoint == "Windows.FullTrustApplication")
261              {
262                  return true;
263              }
264              else
265              {
266                  var manifest = Package.Location + "\\AppxManifest.xml";
267                  if (File.Exists(manifest))
268                  {
269                      try
270                      {
271                          // Check the manifest to verify if the Trust Level for the application is "mediumIL"
272                          var file = File.ReadAllText(manifest);
273                          var xmlDoc = new XmlDocument();
274                          xmlDoc.LoadXml(file);
275                          var xmlRoot = xmlDoc.DocumentElement;
276                          var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
277                          namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10");
278                          var trustLevelNode = xmlRoot.SelectSingleNode("//*[local-name()='Application' and @uap10:TrustLevel]", namespaceManager); // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
279  
280                          if (trustLevelNode?.Attributes["uap10:TrustLevel"]?.Value == "mediumIL")
281                          {
282                              return true;
283                          }
284                      }
285                      catch (Exception e)
286                      {
287                          ProgramLogger.Exception($"Unable to parse manifest file for {DisplayName}", e, MethodBase.GetCurrentMethod().DeclaringType, manifest);
288                      }
289                  }
290              }
291  
292              return false;
293          }
294  
295          internal string ResourceFromPri(string packageFullName, string resourceReference)
296          {
297              const string prefix = "ms-resource:";
298  
299              // Using OrdinalIgnoreCase since this is used internally
300              if (!string.IsNullOrWhiteSpace(resourceReference) && resourceReference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
301              {
302                  // magic comes from @talynone
303                  // https://github.com/talynone/Wox.Plugin.WindowsUniversalAppLauncher/blob/master/StoreAppLauncher/Helpers/NativeApiHelper.cs#L139-L153
304                  string key = resourceReference.Substring(prefix.Length);
305                  string parsed;
306                  string parsedFallback = string.Empty;
307  
308                  // Using Ordinal/OrdinalIgnoreCase since these are used internally
309                  if (key.StartsWith("//", StringComparison.Ordinal))
310                  {
311                      parsed = prefix + key;
312                  }
313                  else if (key.StartsWith('/'))
314                  {
315                      parsed = prefix + "//" + key;
316                  }
317                  else if (key.Contains("resources", StringComparison.OrdinalIgnoreCase))
318                  {
319                      parsed = prefix + key;
320                  }
321                  else
322                  {
323                      parsed = prefix + "///resources/" + key;
324  
325                      // e.g. for Windows Terminal version >= 1.12 DisplayName and Description resources are not in the 'resources' subtree
326                      parsedFallback = prefix + "///" + key;
327                  }
328  
329                  var outBuffer = new StringBuilder(128);
330                  string source = $"@{{{packageFullName}? {parsed}}}";
331                  var capacity = (uint)outBuffer.Capacity;
332                  var hResult = NativeMethods.SHLoadIndirectString(source, outBuffer, capacity, IntPtr.Zero);
333                  if (hResult != HRESULT.S_OK)
334                  {
335                      if (!string.IsNullOrEmpty(parsedFallback))
336                      {
337                          string sourceFallback = $"@{{{packageFullName}? {parsedFallback}}}";
338                          hResult = NativeMethods.SHLoadIndirectString(sourceFallback, outBuffer, capacity, IntPtr.Zero);
339                          if (hResult == HRESULT.S_OK)
340                          {
341                              var loaded = outBuffer.ToString();
342                              if (!string.IsNullOrEmpty(loaded))
343                              {
344                                  return loaded;
345                              }
346                              else
347                              {
348                                  ProgramLogger.Exception($"Can't load null or empty result pri {sourceFallback} in uwp location {Package.Location}", new ArgumentNullException(null), GetType(), Package.Location);
349  
350                                  return string.Empty;
351                              }
352                          }
353                      }
354  
355                      // https://github.com/Wox-launcher/Wox/issues/964
356                      // known hresult 2147942522:
357                      // 'Microsoft Corporation' violates pattern constraint of '\bms-resource:.{1,256}'.
358                      // for
359                      // Microsoft.MicrosoftOfficeHub_17.7608.23501.0_x64__8wekyb3d8bbwe: ms-resource://Microsoft.MicrosoftOfficeHub/officehubintl/AppManifest_GetOffice_Description
360                      // Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription
361                      var e = Marshal.GetExceptionForHR((int)hResult);
362                      ProgramLogger.Exception($"Load pri failed {source} with HResult {hResult} and location {Package.Location}", e, GetType(), Package.Location);
363  
364                      return string.Empty;
365                  }
366                  else
367                  {
368                      var loaded = outBuffer.ToString();
369                      if (!string.IsNullOrEmpty(loaded))
370                      {
371                          return loaded;
372                      }
373                      else
374                      {
375                          ProgramLogger.Exception($"Can't load null or empty result pri {source} in uwp location {Package.Location}", new ArgumentNullException(null), GetType(), Package.Location);
376  
377                          return string.Empty;
378                      }
379                  }
380              }
381              else
382              {
383                  return resourceReference;
384              }
385          }
386  
387          private static readonly Dictionary<PackageVersion, string> _logoKeyFromVersion = new Dictionary<PackageVersion, string>
388          {
389              { PackageVersion.Windows10, "Square44x44Logo" },
390              { PackageVersion.Windows81, "Square30x30Logo" },
391              { PackageVersion.Windows8, "SmallLogo" },
392          };
393  
394          internal string LogoUriFromManifest(IAppxManifestApplication app)
395          {
396              if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key))
397              {
398                  var hr = app.GetStringValue(key, out var logoUriFromApp);
399                  _ = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, logoUriFromApp);
400                  return logoUriFromApp;
401              }
402              else
403              {
404                  return string.Empty;
405              }
406          }
407  
408          public void UpdateLogoPath(Theme theme)
409          {
410              LogoPathFromUri(logoUri, theme);
411          }
412  
413          // scale factors on win10: https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables,
414          private static readonly Dictionary<PackageVersion, List<int>> _scaleFactors = new Dictionary<PackageVersion, List<int>>
415          {
416              { PackageVersion.Windows10, new List<int> { 100, 125, 150, 200, 400 } },
417              { PackageVersion.Windows81, new List<int> { 100, 120, 140, 160, 180 } },
418              { PackageVersion.Windows8, new List<int> { 100 } },
419          };
420  
421          private bool SetScaleIcons(string path, string colorscheme, bool highContrast = false)
422          {
423              var extension = Path.GetExtension(path);
424              if (extension != null)
425              {
426                  var end = path.Length - extension.Length;
427                  var prefix = path.Substring(0, end);
428                  var paths = new List<string> { };
429  
430                  if (!highContrast)
431                  {
432                      paths.Add(path);
433                  }
434  
435                  if (_scaleFactors.TryGetValue(Package.Version, out List<int> factors))
436                  {
437                      foreach (var factor in factors)
438                      {
439                          if (highContrast)
440                          {
441                              paths.Add($"{prefix}.scale-{factor}_{colorscheme}{extension}");
442                              paths.Add($"{prefix}.{colorscheme}_scale-{factor}{extension}");
443                          }
444                          else
445                          {
446                              paths.Add($"{prefix}.scale-{factor}{extension}");
447                          }
448                      }
449                  }
450  
451                  var selectedIconPath = paths.FirstOrDefault(File.Exists);
452                  if (!string.IsNullOrEmpty(selectedIconPath))
453                  {
454                      LogoPath = selectedIconPath;
455                      if (highContrast)
456                      {
457                          LogoType = LogoType.HighContrast;
458                      }
459                      else
460                      {
461                          LogoType = LogoType.Colored;
462                      }
463  
464                      return true;
465                  }
466              }
467  
468              return false;
469          }
470  
471          private bool SetTargetSizeIcon(string path, string colorscheme, bool highContrast = false)
472          {
473              var extension = Path.GetExtension(path);
474              if (extension != null)
475              {
476                  var end = path.Length - extension.Length;
477                  var prefix = path.Substring(0, end);
478                  var paths = new List<string> { };
479                  const int appIconSize = 36;
480                  var targetSizes = new List<int> { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 }.AsParallel();
481                  var pathFactorPairs = new Dictionary<string, int>();
482  
483                  foreach (var factor in targetSizes)
484                  {
485                      if (highContrast)
486                      {
487                          string suffixThemePath = $"{prefix}.targetsize-{factor}_{colorscheme}{extension}";
488                          string prefixThemePath = $"{prefix}.{colorscheme}_targetsize-{factor}{extension}";
489                          paths.Add(suffixThemePath);
490                          paths.Add(prefixThemePath);
491                          pathFactorPairs.Add(suffixThemePath, factor);
492                          pathFactorPairs.Add(prefixThemePath, factor);
493                      }
494                      else
495                      {
496                          string simplePath = $"{prefix}.targetsize-{factor}{extension}";
497                          string altformUnPlatedPath = $"{prefix}.targetsize-{factor}_altform-unplated{extension}";
498                          paths.Add(simplePath);
499                          paths.Add(altformUnPlatedPath);
500                          pathFactorPairs.Add(simplePath, factor);
501                          pathFactorPairs.Add(altformUnPlatedPath, factor);
502                      }
503                  }
504  
505                  var selectedIconPath = paths.OrderBy(x => Math.Abs(pathFactorPairs.GetValueOrDefault(x) - appIconSize)).FirstOrDefault(File.Exists);
506                  if (!string.IsNullOrEmpty(selectedIconPath))
507                  {
508                      LogoPath = selectedIconPath;
509                      if (highContrast)
510                      {
511                          LogoType = LogoType.HighContrast;
512                      }
513                      else
514                      {
515                          LogoType = LogoType.Colored;
516                      }
517  
518                      return true;
519                  }
520              }
521  
522              return false;
523          }
524  
525          private bool SetColoredIcon(string path, string colorscheme)
526          {
527              var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
528              if (isSetColoredScaleIcon)
529              {
530                  return true;
531              }
532  
533              var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
534              if (isSetColoredTargetIcon)
535              {
536                  return true;
537              }
538  
539              var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
540              if (isSetHighContrastScaleIcon)
541              {
542                  return true;
543              }
544  
545              var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
546              if (isSetHighContrastTargetIcon)
547              {
548                  return true;
549              }
550  
551              return false;
552          }
553  
554          private bool SetHighContrastIcon(string path, string colorscheme)
555          {
556              var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
557              if (isSetHighContrastScaleIcon)
558              {
559                  return true;
560              }
561  
562              var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
563              if (isSetHighContrastTargetIcon)
564              {
565                  return true;
566              }
567  
568              var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
569              if (isSetColoredScaleIcon)
570              {
571                  return true;
572              }
573  
574              var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
575              if (isSetColoredTargetIcon)
576              {
577                  return true;
578              }
579  
580              return false;
581          }
582  
583          internal void LogoPathFromUri(string uri, Theme theme)
584          {
585              // all https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
586              // windows 10 https://msdn.microsoft.com/library/windows/apps/dn934817.aspx
587              // windows 8.1 https://msdn.microsoft.com/library/windows/apps/hh965372.aspx#target_size
588              // windows 8 https://msdn.microsoft.com/library/windows/apps/br211475.aspx
589              string path;
590              bool isLogoUriSet;
591  
592              // Using Ordinal since this is used internally with uri
593              if (uri.Contains('\\', StringComparison.Ordinal))
594              {
595                  path = Path.Combine(Package.Location, uri);
596              }
597              else
598              {
599                  // for C:\Windows\MiracastView, etc.
600                  path = Path.Combine(Package.Location, "Assets", uri);
601              }
602  
603              switch (theme)
604              {
605                  case Theme.HighContrastBlack:
606                  case Theme.HighContrastOne:
607                  case Theme.HighContrastTwo:
608                      isLogoUriSet = SetHighContrastIcon(path, ContrastBlack);
609                      break;
610                  case Theme.HighContrastWhite:
611                      isLogoUriSet = SetHighContrastIcon(path, ContrastWhite);
612                      break;
613                  case Theme.Light:
614                      isLogoUriSet = SetColoredIcon(path, ContrastWhite);
615                      break;
616                  default:
617                      isLogoUriSet = SetColoredIcon(path, ContrastBlack);
618                      break;
619              }
620  
621              if (!isLogoUriSet)
622              {
623                  LogoPath = string.Empty;
624                  LogoType = LogoType.Error;
625                  ProgramLogger.Exception($"|{UserModelId} can't find logo uri for {uri} in package location: {Package.Location}", new FileNotFoundException(), GetType(), Package.Location);
626              }
627          }
628  
629          public ImageSource Logo()
630          {
631              if (LogoType == LogoType.Colored)
632              {
633                  var logo = ImageFromPath(LogoPath);
634                  var platedImage = PlatedImage(logo);
635                  return platedImage;
636              }
637              else
638              {
639                  return ImageFromPath(LogoPath);
640              }
641          }
642  
643          private const int _dpiScale100 = 96;
644  
645          private ImageSource PlatedImage(BitmapImage image)
646          {
647              if (!string.IsNullOrEmpty(BackgroundColor))
648              {
649                  string currentBackgroundColor;
650                  if (BackgroundColor == "transparent")
651                  {
652                      // Using InvariantCulture since this is internal
653                      currentBackgroundColor = SystemParameters.WindowGlassBrush.ToString(CultureInfo.InvariantCulture);
654                  }
655                  else
656                  {
657                      currentBackgroundColor = BackgroundColor;
658                  }
659  
660                  var padding = 8;
661                  var width = image.Width + (2 * padding);
662                  var height = image.Height + (2 * padding);
663                  var x = 0;
664                  var y = 0;
665  
666                  var group = new DrawingGroup();
667                  var converted = ColorConverter.ConvertFromString(currentBackgroundColor);
668                  if (converted != null)
669                  {
670                      var color = (Color)converted;
671                      var brush = new SolidColorBrush(color);
672                      var pen = new Pen(brush, 1);
673                      var backgroundArea = new Rect(0, 0, width, height);
674                      var rectangleGeometry = new RectangleGeometry(backgroundArea, 8, 8);
675                      var rectDrawing = new GeometryDrawing(brush, pen, rectangleGeometry);
676                      group.Children.Add(rectDrawing);
677  
678                      var imageArea = new Rect(x + padding, y + padding, image.Width, image.Height);
679                      var imageDrawing = new ImageDrawing(image, imageArea);
680                      group.Children.Add(imageDrawing);
681  
682                      // http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
683                      var visual = new DrawingVisual();
684                      var context = visual.RenderOpen();
685                      context.DrawDrawing(group);
686                      context.Close();
687  
688                      var bitmap = new RenderTargetBitmap(
689                          Convert.ToInt32(width),
690                          Convert.ToInt32(height),
691                          _dpiScale100,
692                          _dpiScale100,
693                          PixelFormats.Pbgra32);
694  
695                      bitmap.Render(visual);
696  
697                      return bitmap;
698                  }
699                  else
700                  {
701                      ProgramLogger.Exception($"Unable to convert background string {BackgroundColor} to color for {Package.Location}", new InvalidOperationException(), GetType(), Package.Location);
702  
703                      return new BitmapImage(new Uri(Constant.ErrorIcon));
704                  }
705              }
706              else
707              {
708                  // todo use windows theme as background
709                  return image;
710              }
711          }
712  
713          private BitmapImage ImageFromPath(string path)
714          {
715              if (File.Exists(path))
716              {
717                  var memoryStream = new MemoryStream();
718                  using (var fileStream = File.OpenRead(path))
719                  {
720                      fileStream.CopyTo(memoryStream);
721                      memoryStream.Position = 0;
722  
723                      var image = new BitmapImage();
724                      image.BeginInit();
725                      image.StreamSource = memoryStream;
726                      image.EndInit();
727                      return image;
728                  }
729              }
730              else
731              {
732                  ProgramLogger.Exception($"Unable to get logo for {UserModelId} from {path} and located in {Package.Location}", new FileNotFoundException(), GetType(), path);
733                  return new BitmapImage(new Uri(ImageLoader.ErrorIconPath));
734              }
735          }
736  
737          public override string ToString()
738          {
739              return $"{DisplayName}: {Description}";
740          }
741      }
742  
743      public enum LogoType
744      {
745          Error,
746          Colored,
747          HighContrast,
748      }
749  }