mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
CmdPal: Improve loading of application icons (uwp and jumbo icons) - part 2 (#44973)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This PR improves icons for app items: - Refactors icon detection and selection from the AppX manifest out of `UWPApplication` - Prefer *unplated* UWP app logos so icons no longer appear smaller than expected - Adds an icon loader based on `IShellItemImageFactory` to correctly load large icons - Jumbo icons loaded from shortcuts are now crisp - Jumbo icons loaded from shortcuts are no longer scaled down - Refactors detail loading in `AppListItem` to prevent potential deadlocks - Makes PWA icons more crisp - Fixes fallback item (now it gets used not only when the icon is null, but also when it's empty). <table> <thead> <tr> <th></th> <th>Old</th> <th>New</th> </tr> </thead> <tr> <td>1</td> <td> <img width="830" height="495" alt="image" src="https://github.com/user-attachments/assets/bc9875bd-6a8b-4a3d-88e1-07a655a5a5cd" /> </td> <td> <img width="750" height="533" alt="image" src="https://github.com/user-attachments/assets/a82ed464-b925-4d0c-95c4-6c04859e886e" /> </td> </tr> <tr> <td>2</td> <td> <img width="814" height="233" alt="image" src="https://github.com/user-attachments/assets/d560d3c0-ffc5-4178-a610-4e3b3c7107c8" /> </td> <td> <img width="760" height="299" alt="image" src="https://github.com/user-attachments/assets/f29c825e-324f-46f1-b6bb-6edcf286fc9a" /> </td> </tr> <tr> <td>3</td> <td> <img width="813" height="262" alt="image" src="https://github.com/user-attachments/assets/d94f724d-ec26-48c8-bb8a-1b10f6a0f7eb" /> </td> <td> <img width="762" height="260" alt="image" src="https://github.com/user-attachments/assets/76c5debb-baac-417e-8aba-9cec198e742c" /> </td> </tr> <tr> <td>4</td> <td> <img width="819" height="250" alt="image" src="https://github.com/user-attachments/assets/5f16d714-56d8-42f2-ad8b-1c2be6570e5c" /> </td> <td> <img width="747" height="244" alt="image" src="https://github.com/user-attachments/assets/485c72cf-ef39-4c05-afdd-877f0a47f51a" /> </td> </tr> <tr> <td>5</td> <td> <img width="815" height="327" alt="image" src="https://github.com/user-attachments/assets/4108e36a-5950-43c9-bdff-6a9f58dadcf6" /> </td> <td> <img width="762" height="272" alt="image" src="https://github.com/user-attachments/assets/804a3159-a165-4a48-87f6-15849f5f4516" /> </td> </tr> <tr> <td>6</td> <td> <img width="809" height="257" alt="image" src="https://github.com/user-attachments/assets/93ad8241-1d75-415f-b08c-4161c0905e41" /> </td> <td> <img width="756" height="231" alt="image" src="https://github.com/user-attachments/assets/a0c9bb44-7151-438d-a811-82d5e2080f44" /> </td> </tr> <tr> <td></td> <td> </td> <td> </td> </tr> </table> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #44970 - [x] Closes: #43320 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -33,6 +33,8 @@ public sealed class AppItem
|
||||
|
||||
public string? FullExecutablePath { get; set; }
|
||||
|
||||
public string? JumboIconPath { get; set; }
|
||||
|
||||
public AppItem()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||
using Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -17,6 +18,7 @@ public sealed partial class AppListItem : ListItem
|
||||
{
|
||||
private readonly AppCommand _appCommand;
|
||||
private readonly AppItem _app;
|
||||
|
||||
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
|
||||
private readonly Lazy<Task<Details>> _detailsLoadTask;
|
||||
|
||||
@@ -66,7 +68,7 @@ public sealed partial class AppListItem : ListItem
|
||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||
|
||||
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
private async Task LoadDetailsAsync()
|
||||
@@ -85,7 +87,7 @@ public sealed partial class AppListItem : ListItem
|
||||
{
|
||||
try
|
||||
{
|
||||
Icon = _appCommand.Icon = await _iconLoadTask.Value ?? Icons.GenericAppIcon;
|
||||
Icon = _appCommand.Icon = CoalesceIcon(await _iconLoadTask.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -93,6 +95,21 @@ public sealed partial class AppListItem : ListItem
|
||||
}
|
||||
}
|
||||
|
||||
private static IconInfo CoalesceIcon(IconInfo? value)
|
||||
{
|
||||
return CoalesceIcon(value, Icons.GenericAppIcon)!;
|
||||
}
|
||||
|
||||
private static IconInfo? CoalesceIcon(IconInfo? value, IconInfo? replacement)
|
||||
{
|
||||
return IconIsNullOrEmpty(value) ? replacement : value;
|
||||
}
|
||||
|
||||
private static bool IconIsNullOrEmpty(IconInfo? value)
|
||||
{
|
||||
return value == null || (string.IsNullOrEmpty(value.Light?.Icon) && value.Light?.Data is null) || (string.IsNullOrEmpty(value.Dark?.Icon) && value.Dark?.Data is null);
|
||||
}
|
||||
|
||||
private async Task<Details> BuildDetails()
|
||||
{
|
||||
// Build metadata, with app type, path, etc.
|
||||
@@ -107,24 +124,49 @@ public sealed partial class AppListItem : ListItem
|
||||
metadata.Add(new DetailsElement() { Key = "[DEBUG] AppIdentifier", Data = new DetailsLink() { Text = _app.AppIdentifier } });
|
||||
metadata.Add(new DetailsElement() { Key = "[DEBUG] ExePath", Data = new DetailsLink() { Text = _app.ExePath } });
|
||||
metadata.Add(new DetailsElement() { Key = "[DEBUG] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } });
|
||||
metadata.Add(new DetailsElement() { Key = "[DEBUG] JumboIconPath", Data = new DetailsLink() { Text = _app.JumboIconPath ?? "(null)" } });
|
||||
#endif
|
||||
|
||||
// Icon
|
||||
IconInfo? heroImage = null;
|
||||
if (_app.IsPackaged)
|
||||
{
|
||||
heroImage = new IconInfo(_app.IcoPath);
|
||||
heroImage = new IconInfo(_app.JumboIconPath ?? _app.IcoPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the icon from the system
|
||||
if (!string.IsNullOrEmpty(_app.JumboIconPath))
|
||||
{
|
||||
var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.JumboIconPath, 64);
|
||||
if (randomAccessStream != null)
|
||||
{
|
||||
heroImage = IconInfo.FromStream(randomAccessStream);
|
||||
}
|
||||
}
|
||||
|
||||
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath))
|
||||
{
|
||||
var randomAccessStream = await IconExtractor.GetIconStreamAsync(_app.IcoPath, 64);
|
||||
if (randomAccessStream != null)
|
||||
{
|
||||
heroImage = IconInfo.FromStream(randomAccessStream);
|
||||
}
|
||||
}
|
||||
|
||||
// do nothing if we fail to load an icon.
|
||||
// Logging it would be too NOISY, there's really no need.
|
||||
if (!string.IsNullOrEmpty(_app.IcoPath))
|
||||
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.JumboIconPath))
|
||||
{
|
||||
heroImage = await TryLoadThumbnail(_app.JumboIconPath, jumbo: true, logOnFailure: false);
|
||||
}
|
||||
|
||||
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.IcoPath))
|
||||
{
|
||||
heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false);
|
||||
}
|
||||
|
||||
if (heroImage == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||
if (IconIsNullOrEmpty(heroImage) && !string.IsNullOrEmpty(_app.ExePath))
|
||||
{
|
||||
heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
|
||||
}
|
||||
@@ -133,8 +175,8 @@ public sealed partial class AppListItem : ListItem
|
||||
return new Details()
|
||||
{
|
||||
Title = this.Title,
|
||||
HeroImage = heroImage ?? this.Icon ?? Icons.GenericAppIcon,
|
||||
Metadata = metadata.ToArray(),
|
||||
HeroImage = CoalesceIcon(CoalesceIcon(heroImage, this.Icon as IconInfo)),
|
||||
Metadata = [..metadata],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,7 +196,7 @@ public sealed partial class AppListItem : ListItem
|
||||
icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
|
||||
}
|
||||
|
||||
if (icon == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||
if (IconIsNullOrEmpty(icon) && !string.IsNullOrEmpty(_app.ExePath))
|
||||
{
|
||||
icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true);
|
||||
}
|
||||
@@ -196,22 +238,25 @@ public sealed partial class AppListItem : ListItem
|
||||
|
||||
private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
|
||||
{
|
||||
try
|
||||
return await Task.Run(async () =>
|
||||
{
|
||||
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo);
|
||||
if (stream is not null)
|
||||
try
|
||||
{
|
||||
return IconInfo.FromStream(stream);
|
||||
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo).ConfigureAwait(false);
|
||||
if (stream is not null)
|
||||
{
|
||||
return IconInfo.FromStream(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logOnFailure)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
|
||||
if (logOnFailure)
|
||||
{
|
||||
Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
|
||||
internal static class AppxIconLoader
|
||||
{
|
||||
private const string ContrastWhite = "contrast-white";
|
||||
private const string ContrastBlack = "contrast-black";
|
||||
|
||||
private static readonly Dictionary<UWP.PackageVersion, List<int>> _scaleFactors = new()
|
||||
{
|
||||
{ UWP.PackageVersion.Windows10, [100, 125, 150, 200, 400] },
|
||||
{ UWP.PackageVersion.Windows81, [100, 120, 140, 160, 180] },
|
||||
{ UWP.PackageVersion.Windows8, [100] },
|
||||
};
|
||||
|
||||
private static readonly List<int> TargetSizes = [16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256];
|
||||
|
||||
private static IconSearchResult GetScaleIcons(
|
||||
string path,
|
||||
string colorscheme,
|
||||
UWP.PackageVersion packageVersion,
|
||||
bool highContrast = false)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
if (extension is null)
|
||||
{
|
||||
return IconSearchResult.NotFound();
|
||||
}
|
||||
|
||||
var end = path.Length - extension.Length;
|
||||
var prefix = path[..end];
|
||||
|
||||
if (!_scaleFactors.TryGetValue(packageVersion, out var factors))
|
||||
{
|
||||
return IconSearchResult.NotFound();
|
||||
}
|
||||
|
||||
var logoType = highContrast ? LogoType.HighContrast : LogoType.Colored;
|
||||
|
||||
// Check from highest scale factor to lowest for best quality
|
||||
for (var i = factors.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var factor = factors[i];
|
||||
string[] pathsToTry = highContrast
|
||||
?
|
||||
[
|
||||
$"{prefix}.scale-{factor}_{colorscheme}{extension}",
|
||||
$"{prefix}.{colorscheme}_scale-{factor}{extension}",
|
||||
]
|
||||
:
|
||||
[
|
||||
$"{prefix}.scale-{factor}{extension}",
|
||||
];
|
||||
|
||||
foreach (var p in pathsToTry)
|
||||
{
|
||||
if (File.Exists(p))
|
||||
{
|
||||
return IconSearchResult.FoundScaled(p, logoType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check base path (100% scale) as last resort
|
||||
if (!highContrast && File.Exists(path))
|
||||
{
|
||||
return IconSearchResult.FoundScaled(path, logoType);
|
||||
}
|
||||
|
||||
return IconSearchResult.NotFound();
|
||||
}
|
||||
|
||||
private static IconSearchResult GetTargetSizeIcon(
|
||||
string path,
|
||||
string colorscheme,
|
||||
bool highContrast = false,
|
||||
int appIconSize = 36,
|
||||
double maxSizeCoefficient = 8.0)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
if (extension is null)
|
||||
{
|
||||
return IconSearchResult.NotFound();
|
||||
}
|
||||
|
||||
var end = path.Length - extension.Length;
|
||||
var prefix = path[..end];
|
||||
var pathSizePairs = new List<(string Path, int Size)>();
|
||||
|
||||
foreach (var size in TargetSizes)
|
||||
{
|
||||
if (highContrast)
|
||||
{
|
||||
pathSizePairs.Add(($"{prefix}.targetsize-{size}_{colorscheme}{extension}", size));
|
||||
pathSizePairs.Add(($"{prefix}.{colorscheme}_targetsize-{size}{extension}", size));
|
||||
}
|
||||
else
|
||||
{
|
||||
pathSizePairs.Add(($"{prefix}.targetsize-{size}_altform-unplated{extension}", size));
|
||||
pathSizePairs.Add(($"{prefix}.targetsize-{size}{extension}", size));
|
||||
}
|
||||
}
|
||||
|
||||
var maxAllowedSize = (int)(appIconSize * maxSizeCoefficient);
|
||||
var logoType = highContrast ? LogoType.HighContrast : LogoType.Colored;
|
||||
|
||||
string? bestLargerPath = null;
|
||||
var bestLargerSize = int.MaxValue;
|
||||
|
||||
string? bestSmallerPath = null;
|
||||
var bestSmallerSize = 0;
|
||||
|
||||
foreach (var (p, size) in pathSizePairs)
|
||||
{
|
||||
if (!File.Exists(p))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size >= appIconSize && size <= maxAllowedSize)
|
||||
{
|
||||
if (size < bestLargerSize)
|
||||
{
|
||||
bestLargerSize = size;
|
||||
bestLargerPath = p;
|
||||
}
|
||||
}
|
||||
else if (size < appIconSize)
|
||||
{
|
||||
if (size > bestSmallerSize)
|
||||
{
|
||||
bestSmallerSize = size;
|
||||
bestSmallerPath = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestLargerPath is not null)
|
||||
{
|
||||
return IconSearchResult.FoundTargetSize(bestLargerPath, logoType, bestLargerSize);
|
||||
}
|
||||
|
||||
if (bestSmallerPath is not null)
|
||||
{
|
||||
return IconSearchResult.FoundTargetSize(bestSmallerPath, logoType, bestSmallerSize);
|
||||
}
|
||||
|
||||
return IconSearchResult.NotFound();
|
||||
}
|
||||
|
||||
private static IconSearchResult GetColoredIcon(
|
||||
string path,
|
||||
string colorscheme,
|
||||
int iconSize,
|
||||
UWP package)
|
||||
{
|
||||
// First priority: targetsize icons (we know the exact size)
|
||||
var targetResult = GetTargetSizeIcon(path, colorscheme, highContrast: false, appIconSize: iconSize);
|
||||
if (targetResult.MeetsMinimumSize(iconSize))
|
||||
{
|
||||
return targetResult;
|
||||
}
|
||||
|
||||
var hcTargetResult = GetTargetSizeIcon(path, colorscheme, highContrast: true, appIconSize: iconSize);
|
||||
if (hcTargetResult.MeetsMinimumSize(iconSize))
|
||||
{
|
||||
return hcTargetResult;
|
||||
}
|
||||
|
||||
// Second priority: scale icons (size unknown, but higher scale = likely better)
|
||||
var scaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: false);
|
||||
if (scaleResult.IsFound)
|
||||
{
|
||||
return scaleResult;
|
||||
}
|
||||
|
||||
var hcScaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: true);
|
||||
if (hcScaleResult.IsFound)
|
||||
{
|
||||
return hcScaleResult;
|
||||
}
|
||||
|
||||
// Last resort: return undersized targetsize if we found one
|
||||
if (targetResult.IsFound)
|
||||
{
|
||||
return targetResult;
|
||||
}
|
||||
|
||||
if (hcTargetResult.IsFound)
|
||||
{
|
||||
return hcTargetResult;
|
||||
}
|
||||
|
||||
return IconSearchResult.NotFound();
|
||||
}
|
||||
|
||||
private static IconSearchResult SetHighContrastIcon(
|
||||
string path,
|
||||
string colorscheme,
|
||||
int iconSize,
|
||||
UWP package)
|
||||
{
|
||||
// First priority: HC targetsize icons (we know the exact size)
|
||||
var hcTargetResult = GetTargetSizeIcon(path, colorscheme, highContrast: true, appIconSize: iconSize);
|
||||
if (hcTargetResult.MeetsMinimumSize(iconSize))
|
||||
{
|
||||
return hcTargetResult;
|
||||
}
|
||||
|
||||
var targetResult = GetTargetSizeIcon(path, colorscheme, highContrast: false, appIconSize: iconSize);
|
||||
if (targetResult.MeetsMinimumSize(iconSize))
|
||||
{
|
||||
return targetResult;
|
||||
}
|
||||
|
||||
// Second priority: scale icons
|
||||
var hcScaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: true);
|
||||
if (hcScaleResult.IsFound)
|
||||
{
|
||||
return hcScaleResult;
|
||||
}
|
||||
|
||||
var scaleResult = GetScaleIcons(path, colorscheme, package.Version, highContrast: false);
|
||||
if (scaleResult.IsFound)
|
||||
{
|
||||
return scaleResult;
|
||||
}
|
||||
|
||||
// Last resort: undersized targetsize
|
||||
if (hcTargetResult.IsFound)
|
||||
{
|
||||
return hcTargetResult;
|
||||
}
|
||||
|
||||
if (targetResult.IsFound)
|
||||
{
|
||||
return targetResult;
|
||||
}
|
||||
|
||||
return IconSearchResult.NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an icon from a UWP package, attempting to find the best match for the requested size.
|
||||
/// </summary>
|
||||
/// <param name="uri">The relative URI to the logo asset.</param>
|
||||
/// <param name="theme">The current theme.</param>
|
||||
/// <param name="iconSize">The requested icon size in pixels.</param>
|
||||
/// <param name="package">The UWP package.</param>
|
||||
/// <returns>
|
||||
/// An IconSearchResult. Use <see cref="IconSearchResult.MeetsMinimumSize"/> to check if
|
||||
/// the icon is confirmed to be large enough, or <see cref="IconSearchResult.IsTargetSizeIcon"/>
|
||||
/// to determine if the size is known.
|
||||
/// </returns>
|
||||
internal static IconSearchResult LogoPathFromUri(
|
||||
string uri,
|
||||
Theme theme,
|
||||
int iconSize,
|
||||
UWP package)
|
||||
{
|
||||
var path = Path.Combine(package.Location, uri);
|
||||
var logo = Probe(theme, path, iconSize, package);
|
||||
if (!logo.IsFound && !uri.Contains('\\', StringComparison.Ordinal))
|
||||
{
|
||||
path = Path.Combine(package.Location, "Assets", uri);
|
||||
logo = Probe(theme, path, iconSize, package);
|
||||
}
|
||||
|
||||
return logo;
|
||||
}
|
||||
|
||||
private static IconSearchResult Probe(Theme theme, string path, int iconSize, UWP package)
|
||||
{
|
||||
return theme switch
|
||||
{
|
||||
Theme.HighContrastBlack or Theme.HighContrastOne or Theme.HighContrastTwo
|
||||
=> SetHighContrastIcon(path, ContrastBlack, iconSize, package),
|
||||
Theme.HighContrastWhite
|
||||
=> SetHighContrastIcon(path, ContrastWhite, iconSize, package),
|
||||
Theme.Light
|
||||
=> GetColoredIcon(path, ContrastWhite, iconSize, package),
|
||||
_
|
||||
=> GetColoredIcon(path, ContrastBlack, iconSize, package),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
using Windows.Win32.UI.Shell;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
|
||||
internal static class IconExtractor
|
||||
{
|
||||
public static async Task<IRandomAccessStream?> GetIconStreamAsync(string path, int size)
|
||||
{
|
||||
var bitmap = GetIcon(path, size);
|
||||
if (bitmap == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var stream = new InMemoryRandomAccessStream();
|
||||
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
|
||||
encoder.SetSoftwareBitmap(bitmap);
|
||||
await encoder.FlushAsync();
|
||||
|
||||
stream.Seek(0);
|
||||
return stream;
|
||||
}
|
||||
|
||||
public static unsafe SoftwareBitmap? GetIcon(string path, int size)
|
||||
{
|
||||
IShellItemImageFactory* factory = null;
|
||||
HBITMAP hBitmap = default;
|
||||
|
||||
try
|
||||
{
|
||||
fixed (char* pPath = path)
|
||||
{
|
||||
var iid = IShellItemImageFactory.IID_Guid;
|
||||
var hr = PInvoke.SHCreateItemFromParsingName(
|
||||
pPath,
|
||||
null,
|
||||
&iid,
|
||||
(void**)&factory);
|
||||
|
||||
if (hr.Failed || factory == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var requestedSize = new SIZE { cx = size, cy = size };
|
||||
var hr2 = factory->GetImage(
|
||||
requestedSize,
|
||||
SIIGBF.SIIGBF_ICONONLY | SIIGBF.SIIGBF_BIGGERSIZEOK | SIIGBF.SIIGBF_CROPTOSQUARE,
|
||||
&hBitmap);
|
||||
|
||||
if (hr2.Failed || hBitmap.IsNull)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return CreateSoftwareBitmap(hBitmap, size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load icon from path='{path}',size={size}", ex);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!hBitmap.IsNull)
|
||||
{
|
||||
PInvoke.DeleteObject(hBitmap);
|
||||
}
|
||||
|
||||
if (factory != null)
|
||||
{
|
||||
factory->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe SoftwareBitmap CreateSoftwareBitmap(HBITMAP hBitmap, int size)
|
||||
{
|
||||
var pixels = new byte[size * size * 4];
|
||||
|
||||
var bmi = new BITMAPINFO
|
||||
{
|
||||
bmiHeader = new BITMAPINFOHEADER
|
||||
{
|
||||
biSize = (uint)sizeof(BITMAPINFOHEADER),
|
||||
biWidth = size,
|
||||
biHeight = -size,
|
||||
biPlanes = 1,
|
||||
biBitCount = 32,
|
||||
biCompression = 0,
|
||||
},
|
||||
};
|
||||
|
||||
var hdc = PInvoke.GetDC(default);
|
||||
try
|
||||
{
|
||||
fixed (byte* pPixels = pixels)
|
||||
{
|
||||
_ = PInvoke.GetDIBits(
|
||||
hdc,
|
||||
hBitmap,
|
||||
0,
|
||||
(uint)size,
|
||||
pPixels,
|
||||
&bmi,
|
||||
DIB_USAGE.DIB_RGB_COLORS);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = PInvoke.ReleaseDC(default, hdc);
|
||||
}
|
||||
|
||||
var bitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, size, size, BitmapAlphaMode.Premultiplied);
|
||||
bitmap.CopyFromBuffer(pixels.AsBuffer());
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Result of an icon search operation.
|
||||
/// </summary>
|
||||
internal readonly record struct IconSearchResult(
|
||||
string? LogoPath,
|
||||
LogoType LogoType,
|
||||
bool IsTargetSizeIcon,
|
||||
int? KnownSize = null)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether an icon was found.
|
||||
/// </summary>
|
||||
public bool IsFound => LogoPath is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if we can confirm the icon meets the minimum size.
|
||||
/// Only possible for targetsize icons where the size is encoded in the filename.
|
||||
/// </summary>
|
||||
public bool MeetsMinimumSize(int minimumSize) =>
|
||||
IsTargetSizeIcon && KnownSize >= minimumSize;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if we know the icon is undersized.
|
||||
/// Returns false if not found, or if size is unknown (scale-based icons).
|
||||
/// </summary>
|
||||
public bool IsKnownUndersized(int minimumSize) =>
|
||||
IsTargetSizeIcon && KnownSize < minimumSize;
|
||||
|
||||
public static IconSearchResult NotFound() => new(null, default, false);
|
||||
|
||||
public static IconSearchResult FoundTargetSize(string path, LogoType logoType, int size)
|
||||
=> new(path, logoType, IsTargetSizeIcon: true, size);
|
||||
|
||||
public static IconSearchResult FoundScaled(string path, LogoType logoType)
|
||||
=> new(path, logoType, IsTargetSizeIcon: false);
|
||||
}
|
||||
@@ -18,3 +18,9 @@ ShellLink
|
||||
IPersistFile
|
||||
CoTaskMemFree
|
||||
IUnknown
|
||||
IShellItemImageFactory
|
||||
DeleteObject
|
||||
GetDIBits
|
||||
GetDC
|
||||
ReleaseDC
|
||||
SIIGBF
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.IO.Abstractions;
|
||||
using System.Xml;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||
using Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -23,8 +24,10 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
[Serializable]
|
||||
public class UWPApplication : IUWPApplication
|
||||
{
|
||||
private const int ListIconSize = 20;
|
||||
private const int JumboIconSize = 64;
|
||||
|
||||
private static readonly IFileSystem FileSystem = new FileSystem();
|
||||
private static readonly IPath Path = FileSystem.Path;
|
||||
private static readonly IFile File = FileSystem.File;
|
||||
|
||||
public string AppListEntry { get; set; } = string.Empty;
|
||||
@@ -56,13 +59,15 @@ public class UWPApplication : IUWPApplication
|
||||
|
||||
public LogoType LogoType { get; set; }
|
||||
|
||||
public string JumboLogoPath { get; set; } = string.Empty;
|
||||
|
||||
public LogoType JumboLogoType { get; set; }
|
||||
|
||||
public UWP Package { get; set; }
|
||||
|
||||
private string logoUri;
|
||||
private string _logoUri;
|
||||
|
||||
private const string ContrastWhite = "contrast-white";
|
||||
|
||||
private const string ContrastBlack = "contrast-black";
|
||||
private string _jumboLogoUri;
|
||||
|
||||
// Function to set the subtitle based on the Type of application
|
||||
public static string Type()
|
||||
@@ -154,7 +159,8 @@ public class UWPApplication : IUWPApplication
|
||||
|
||||
DisplayName = ResourceFromPri(package.FullName, DisplayName);
|
||||
Description = ResourceFromPri(package.FullName, Description);
|
||||
logoUri = LogoUriFromManifest(manifestApp);
|
||||
_logoUri = LogoUriFromManifest(manifestApp);
|
||||
_jumboLogoUri = LogoUriFromManifest(manifestApp, jumbo: true);
|
||||
|
||||
Enabled = true;
|
||||
CanRunElevated = IfApplicationCanRunElevated();
|
||||
@@ -280,16 +286,24 @@ public class UWPApplication : IUWPApplication
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<PackageVersion, string> _logoKeyFromVersion = new Dictionary<PackageVersion, string>
|
||||
private static readonly Dictionary<PackageVersion, string> _smallLogoKeyFromVersion = new Dictionary<PackageVersion, string>
|
||||
{
|
||||
{ PackageVersion.Windows10, "Square44x44Logo" },
|
||||
{ PackageVersion.Windows81, "Square30x30Logo" },
|
||||
{ PackageVersion.Windows8, "SmallLogo" },
|
||||
};
|
||||
|
||||
internal unsafe string LogoUriFromManifest(IAppxManifestApplication* app)
|
||||
private static readonly Dictionary<PackageVersion, string> _largeLogoKeyFromVersion = new Dictionary<PackageVersion, string>
|
||||
{
|
||||
if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key))
|
||||
{ PackageVersion.Windows10, "Square150x150Logo" },
|
||||
{ PackageVersion.Windows81, "Square150x150Logo" },
|
||||
{ PackageVersion.Windows8, "Logo" },
|
||||
};
|
||||
|
||||
internal unsafe string LogoUriFromManifest(IAppxManifestApplication* app, bool jumbo = false)
|
||||
{
|
||||
var logoMap = jumbo ? _largeLogoKeyFromVersion : _smallLogoKeyFromVersion;
|
||||
if (logoMap.TryGetValue(Package.Version, out var key))
|
||||
{
|
||||
var hr = app->GetStringValue(key, out var logoUriFromAppPtr);
|
||||
return ComFreeHelper.GetStringAndFree(hr, logoUriFromAppPtr);
|
||||
@@ -302,257 +316,55 @@ public class UWPApplication : IUWPApplication
|
||||
|
||||
public void UpdateLogoPath(Theme theme)
|
||||
{
|
||||
LogoPathFromUri(logoUri, theme);
|
||||
}
|
||||
|
||||
// scale factors on win10: https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables,
|
||||
private static readonly Dictionary<PackageVersion, List<int>> _scaleFactors = new Dictionary<PackageVersion, List<int>>
|
||||
// Update small logo
|
||||
var logo = AppxIconLoader.LogoPathFromUri(_logoUri, theme, ListIconSize, Package);
|
||||
if (logo.IsFound)
|
||||
{
|
||||
{ PackageVersion.Windows10, new List<int> { 100, 125, 150, 200, 400 } },
|
||||
{ PackageVersion.Windows81, new List<int> { 100, 120, 140, 160, 180 } },
|
||||
{ PackageVersion.Windows8, new List<int> { 100 } },
|
||||
};
|
||||
|
||||
private bool SetScaleIcons(string path, string colorscheme, bool highContrast = false)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
if (extension is not null)
|
||||
{
|
||||
var end = path.Length - extension.Length;
|
||||
var prefix = path.Substring(0, end);
|
||||
var paths = new List<string> { };
|
||||
|
||||
if (!highContrast)
|
||||
{
|
||||
paths.Add(path);
|
||||
}
|
||||
|
||||
if (_scaleFactors.TryGetValue(Package.Version, out var factors))
|
||||
{
|
||||
foreach (var factor in factors)
|
||||
{
|
||||
if (highContrast)
|
||||
{
|
||||
paths.Add($"{prefix}.scale-{factor}_{colorscheme}{extension}");
|
||||
paths.Add($"{prefix}.{colorscheme}_scale-{factor}{extension}");
|
||||
}
|
||||
else
|
||||
{
|
||||
paths.Add($"{prefix}.scale-{factor}{extension}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// By working from the highest resolution to the lowest, we make
|
||||
// sure that we use the highest quality possible icon for the app.
|
||||
//
|
||||
// FirstOrDefault would result in us using the 1x scaled icon
|
||||
// always, which is usually too small for our needs.
|
||||
for (var i = paths.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (File.Exists(paths[i]))
|
||||
{
|
||||
LogoPath = paths[i];
|
||||
if (highContrast)
|
||||
{
|
||||
LogoType = LogoType.HighContrast;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogoType = LogoType.Colored;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool SetTargetSizeIcon(string path, string colorscheme, bool highContrast = false)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
if (extension is not null)
|
||||
{
|
||||
var end = path.Length - extension.Length;
|
||||
var prefix = path.Substring(0, end);
|
||||
var paths = new List<string> { };
|
||||
const int appIconSize = 36;
|
||||
var targetSizes = new List<int> { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 };
|
||||
var pathFactorPairs = new Dictionary<string, int>();
|
||||
|
||||
foreach (var factor in targetSizes)
|
||||
{
|
||||
if (highContrast)
|
||||
{
|
||||
var suffixThemePath = $"{prefix}.targetsize-{factor}_{colorscheme}{extension}";
|
||||
var prefixThemePath = $"{prefix}.{colorscheme}_targetsize-{factor}{extension}";
|
||||
paths.Add(suffixThemePath);
|
||||
paths.Add(prefixThemePath);
|
||||
pathFactorPairs.Add(suffixThemePath, factor);
|
||||
pathFactorPairs.Add(prefixThemePath, factor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var simplePath = $"{prefix}.targetsize-{factor}{extension}";
|
||||
var altformUnPlatedPath = $"{prefix}.targetsize-{factor}_altform-unplated{extension}";
|
||||
paths.Add(simplePath);
|
||||
paths.Add(altformUnPlatedPath);
|
||||
pathFactorPairs.Add(simplePath, factor);
|
||||
pathFactorPairs.Add(altformUnPlatedPath, factor);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort paths by distance to desired app icon size
|
||||
var selectedIconPath = string.Empty;
|
||||
var closestDistance = int.MaxValue;
|
||||
|
||||
foreach (var p in paths)
|
||||
{
|
||||
if (File.Exists(p) && pathFactorPairs.TryGetValue(p, out var factor))
|
||||
{
|
||||
var distance = Math.Abs(factor - appIconSize);
|
||||
if (distance < closestDistance)
|
||||
{
|
||||
closestDistance = distance;
|
||||
selectedIconPath = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedIconPath))
|
||||
{
|
||||
LogoPath = selectedIconPath;
|
||||
if (highContrast)
|
||||
{
|
||||
LogoType = LogoType.HighContrast;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogoType = LogoType.Colored;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool SetColoredIcon(string path, string colorscheme)
|
||||
{
|
||||
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
|
||||
if (isSetColoredScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
|
||||
if (isSetColoredTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
|
||||
if (isSetHighContrastScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
|
||||
if (isSetHighContrastTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool SetHighContrastIcon(string path, string colorscheme)
|
||||
{
|
||||
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
|
||||
if (isSetHighContrastScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
|
||||
if (isSetHighContrastTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
|
||||
if (isSetColoredScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
|
||||
if (isSetColoredTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void LogoPathFromUri(string uri, Theme theme)
|
||||
{
|
||||
// all https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
|
||||
// windows 10 https://msdn.microsoft.com/library/windows/apps/dn934817.aspx
|
||||
// windows 8.1 https://msdn.microsoft.com/library/windows/apps/hh965372.aspx#target_size
|
||||
// windows 8 https://msdn.microsoft.com/library/windows/apps/br211475.aspx
|
||||
string path;
|
||||
bool isLogoUriSet;
|
||||
|
||||
// Using Ordinal since this is used internally with uri
|
||||
if (uri.Contains('\\', StringComparison.Ordinal))
|
||||
{
|
||||
path = Path.Combine(Package.Location, uri);
|
||||
LogoPath = logo.LogoPath!;
|
||||
LogoType = logo.LogoType;
|
||||
}
|
||||
else
|
||||
{
|
||||
// for C:\Windows\MiracastView, etc.
|
||||
path = Path.Combine(Package.Location, "Assets", uri);
|
||||
}
|
||||
|
||||
switch (theme)
|
||||
{
|
||||
case Theme.HighContrastBlack:
|
||||
case Theme.HighContrastOne:
|
||||
case Theme.HighContrastTwo:
|
||||
isLogoUriSet = SetHighContrastIcon(path, ContrastBlack);
|
||||
break;
|
||||
case Theme.HighContrastWhite:
|
||||
isLogoUriSet = SetHighContrastIcon(path, ContrastWhite);
|
||||
break;
|
||||
case Theme.Light:
|
||||
isLogoUriSet = SetColoredIcon(path, ContrastWhite);
|
||||
break;
|
||||
default:
|
||||
isLogoUriSet = SetColoredIcon(path, ContrastBlack);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isLogoUriSet)
|
||||
{
|
||||
LogoPath = string.Empty;
|
||||
LogoType = LogoType.Error;
|
||||
}
|
||||
|
||||
// Jumbo logo ... small logo can actually provide better result
|
||||
var jumboLogo = AppxIconLoader.LogoPathFromUri(_logoUri, theme, JumboIconSize, Package);
|
||||
if (jumboLogo.IsFound)
|
||||
{
|
||||
JumboLogoPath = jumboLogo.LogoPath!;
|
||||
JumboLogoType = jumboLogo.LogoType;
|
||||
}
|
||||
else
|
||||
{
|
||||
JumboLogoPath = string.Empty;
|
||||
JumboLogoType = LogoType.Error;
|
||||
}
|
||||
|
||||
if (!jumboLogo.MeetsMinimumSize(JumboIconSize) || !jumboLogo.IsFound)
|
||||
{
|
||||
var jumboLogoAlt = AppxIconLoader.LogoPathFromUri(_jumboLogoUri, theme, JumboIconSize, Package);
|
||||
if (jumboLogoAlt.IsFound)
|
||||
{
|
||||
JumboLogoPath = jumboLogoAlt.LogoPath!;
|
||||
JumboLogoType = jumboLogoAlt.LogoType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AppItem ToAppItem()
|
||||
{
|
||||
var app = this;
|
||||
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
|
||||
var item = new AppItem()
|
||||
var jumboIconPath = app.JumboLogoType != LogoType.Error ? app.JumboLogoPath : string.Empty;
|
||||
var item = new AppItem
|
||||
{
|
||||
Name = app.Name,
|
||||
Subtitle = app.Description,
|
||||
Type = UWPApplication.Type(),
|
||||
IcoPath = iconPath,
|
||||
JumboIconPath = jumboIconPath,
|
||||
DirPath = app.Location,
|
||||
UserModelId = app.UserModelId,
|
||||
IsPackaged = true,
|
||||
@@ -563,116 +375,6 @@ public class UWPApplication : IUWPApplication
|
||||
return item;
|
||||
}
|
||||
|
||||
/*
|
||||
public ImageSource Logo()
|
||||
{
|
||||
if (LogoType == LogoType.Colored)
|
||||
{
|
||||
var logo = ImageFromPath(LogoPath);
|
||||
var platedImage = PlatedImage(logo);
|
||||
return platedImage;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ImageFromPath(LogoPath);
|
||||
}
|
||||
}
|
||||
|
||||
private const int _dpiScale100 = 96;
|
||||
|
||||
private ImageSource PlatedImage(BitmapImage image)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BackgroundColor))
|
||||
{
|
||||
string currentBackgroundColor;
|
||||
if (BackgroundColor == "transparent")
|
||||
{
|
||||
// Using InvariantCulture since this is internal
|
||||
currentBackgroundColor = SystemParameters.WindowGlassBrush.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentBackgroundColor = BackgroundColor;
|
||||
}
|
||||
|
||||
var padding = 8;
|
||||
var width = image.Width + (2 * padding);
|
||||
var height = image.Height + (2 * padding);
|
||||
var x = 0;
|
||||
var y = 0;
|
||||
|
||||
var group = new DrawingGroup();
|
||||
var converted = ColorConverter.ConvertFromString(currentBackgroundColor);
|
||||
if (converted is not null)
|
||||
{
|
||||
var color = (Color)converted;
|
||||
var brush = new SolidColorBrush(color);
|
||||
var pen = new Pen(brush, 1);
|
||||
var backgroundArea = new Rect(0, 0, width, height);
|
||||
var rectangleGeometry = new RectangleGeometry(backgroundArea, 8, 8);
|
||||
var rectDrawing = new GeometryDrawing(brush, pen, rectangleGeometry);
|
||||
group.Children.Add(rectDrawing);
|
||||
|
||||
var imageArea = new Rect(x + padding, y + padding, image.Width, image.Height);
|
||||
var imageDrawing = new ImageDrawing(image, imageArea);
|
||||
group.Children.Add(imageDrawing);
|
||||
|
||||
// http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
|
||||
var visual = new DrawingVisual();
|
||||
var context = visual.RenderOpen();
|
||||
context.DrawDrawing(group);
|
||||
context.Close();
|
||||
|
||||
var bitmap = new RenderTargetBitmap(
|
||||
Convert.ToInt32(width),
|
||||
Convert.ToInt32(height),
|
||||
_dpiScale100,
|
||||
_dpiScale100,
|
||||
PixelFormats.Pbgra32);
|
||||
|
||||
bitmap.Render(visual);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
else
|
||||
{
|
||||
ProgramLogger.Exception($"Unable to convert background string {BackgroundColor} to color for {Package.Location}", new InvalidOperationException(), GetType(), Package.Location);
|
||||
|
||||
return new BitmapImage(new Uri(Constant.ErrorIcon));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo use windows theme as background
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapImage ImageFromPath(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
using (var fileStream = File.OpenRead(path))
|
||||
{
|
||||
fileStream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.StreamSource = memoryStream;
|
||||
image.EndInit();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ProgramLogger.Exception($"Unable to get logo for {UserModelId} from {path} and located in {Package.Location}", new FileNotFoundException(), GetType(), path);
|
||||
return new BitmapImage(new Uri(ImageLoader.ErrorIconPath));
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{DisplayName}: {Description}";
|
||||
|
||||
Reference in New Issue
Block a user