mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-21 16:14:28 +08:00
0.19
This commit is contained in:
475
LanMontainDesktop/Services/UwpManifestIconResolver.cs
Normal file
475
LanMontainDesktop/Services/UwpManifestIconResolver.cs
Normal file
@@ -0,0 +1,475 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace LanMontainDesktop.Services;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal static class UwpManifestIconResolver
|
||||
{
|
||||
private const int ErrorSuccess = 0;
|
||||
private const int ErrorInsufficientBuffer = 122;
|
||||
|
||||
private static readonly Regex ScaleRegex =
|
||||
new(@"scale-(?<n>\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex TargetSizeRegex =
|
||||
new(@"targetsize-(?<n>\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly string[] ManifestLogoAttributePriority =
|
||||
[
|
||||
"Square44x44Logo",
|
||||
"Square150x150Logo",
|
||||
"Square310x310Logo",
|
||||
"Square71x71Logo",
|
||||
"Wide310x150Logo",
|
||||
"Logo",
|
||||
"SmallLogo"
|
||||
];
|
||||
|
||||
private static readonly Dictionary<string, byte[]?> IconCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly object CacheLock = new();
|
||||
|
||||
public static bool TryGetIconPngBytesFromAumid(string aumid, out byte[]? pngBytes)
|
||||
{
|
||||
pngBytes = null;
|
||||
if (string.IsNullOrWhiteSpace(aumid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (CacheLock)
|
||||
{
|
||||
if (IconCache.TryGetValue(aumid, out var cached))
|
||||
{
|
||||
pngBytes = cached;
|
||||
return cached is not null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TrySplitAumid(aumid, out var packageFamilyName, out var appId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var installLocation in GetPackageInstallLocations(packageFamilyName))
|
||||
{
|
||||
if (TryExtractIconFromManifestInstallLocation(installLocation, appId, out pngBytes))
|
||||
{
|
||||
lock (CacheLock)
|
||||
{
|
||||
IconCache[aumid] = pngBytes;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
lock (CacheLock)
|
||||
{
|
||||
IconCache[aumid] = null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TrySplitAumid(string aumid, out string packageFamilyName, out string appId)
|
||||
{
|
||||
packageFamilyName = string.Empty;
|
||||
appId = string.Empty;
|
||||
var separatorIndex = aumid.IndexOf('!');
|
||||
if (separatorIndex <= 0 || separatorIndex >= aumid.Length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
packageFamilyName = aumid[..separatorIndex].Trim();
|
||||
appId = aumid[(separatorIndex + 1)..].Trim();
|
||||
return !string.IsNullOrWhiteSpace(packageFamilyName) && !string.IsNullOrWhiteSpace(appId);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetPackageInstallLocations(string packageFamilyName)
|
||||
{
|
||||
var fromApi = GetPackageInstallLocationsByFamilyApi(packageFamilyName);
|
||||
return fromApi.Count > 0
|
||||
? fromApi
|
||||
: GetPackageInstallLocationsByDirectoryScan(packageFamilyName);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetPackageInstallLocationsByFamilyApi(string packageFamilyName)
|
||||
{
|
||||
var results = new List<(string PackageFullName, string InstallLocation)>();
|
||||
|
||||
uint packageCount = 0;
|
||||
uint bufferLength = 0;
|
||||
var firstCall = GetPackagesByPackageFamily(
|
||||
packageFamilyName,
|
||||
ref packageCount,
|
||||
null,
|
||||
ref bufferLength,
|
||||
null);
|
||||
if (firstCall != ErrorInsufficientBuffer && firstCall != ErrorSuccess)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (packageCount == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var packageFullNamePointers = new IntPtr[packageCount];
|
||||
var packageNamesBuffer = new char[Math.Max(1, (int)bufferLength)];
|
||||
var secondCall = GetPackagesByPackageFamily(
|
||||
packageFamilyName,
|
||||
ref packageCount,
|
||||
packageFullNamePointers,
|
||||
ref bufferLength,
|
||||
packageNamesBuffer);
|
||||
if (secondCall != ErrorSuccess)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
for (var i = 0; i < packageCount; i++)
|
||||
{
|
||||
var packageFullName = Marshal.PtrToStringUni(packageFullNamePointers[i]);
|
||||
if (string.IsNullOrWhiteSpace(packageFullName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint pathLength = 0;
|
||||
var getPathFirst = GetPackagePathByFullName(packageFullName, ref pathLength, null);
|
||||
if (getPathFirst != ErrorInsufficientBuffer || pathLength == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pathBuilder = new StringBuilder((int)pathLength);
|
||||
var getPathSecond = GetPackagePathByFullName(packageFullName, ref pathLength, pathBuilder);
|
||||
if (getPathSecond != ErrorSuccess)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var installPath = pathBuilder.ToString().TrimEnd('\0').Trim();
|
||||
if (string.IsNullOrWhiteSpace(installPath) || !Directory.Exists(installPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add((packageFullName, installPath));
|
||||
}
|
||||
|
||||
return results
|
||||
.OrderByDescending(item => ParsePackageVersion(item.PackageFullName))
|
||||
.Select(item => item.InstallLocation)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetPackageInstallLocationsByDirectoryScan(string packageFamilyName)
|
||||
{
|
||||
if (!TrySplitPackageFamilyName(packageFamilyName, out var packageName, out var publisherId))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var windowsAppsDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
|
||||
"WindowsApps");
|
||||
|
||||
if (!Directory.Exists(windowsAppsDirectory))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var directories = Directory
|
||||
.EnumerateDirectories(windowsAppsDirectory)
|
||||
.Where(directoryPath =>
|
||||
{
|
||||
var directoryName = Path.GetFileName(directoryPath);
|
||||
if (string.IsNullOrWhiteSpace(directoryName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return directoryName.StartsWith(packageName + "_", StringComparison.OrdinalIgnoreCase) &&
|
||||
directoryName.Contains("__" + publisherId, StringComparison.OrdinalIgnoreCase);
|
||||
})
|
||||
.OrderByDescending(directoryPath => ParsePackageVersion(Path.GetFileName(directoryPath)))
|
||||
.ToList();
|
||||
|
||||
return directories;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySplitPackageFamilyName(string packageFamilyName, out string packageName, out string publisherId)
|
||||
{
|
||||
packageName = string.Empty;
|
||||
publisherId = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(packageFamilyName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var separatorIndex = packageFamilyName.LastIndexOf('_');
|
||||
if (separatorIndex <= 0 || separatorIndex >= packageFamilyName.Length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
packageName = packageFamilyName[..separatorIndex].Trim();
|
||||
publisherId = packageFamilyName[(separatorIndex + 1)..].Trim();
|
||||
return !string.IsNullOrWhiteSpace(packageName) && !string.IsNullOrWhiteSpace(publisherId);
|
||||
}
|
||||
|
||||
private static Version ParsePackageVersion(string? packageIdentity)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(packageIdentity))
|
||||
{
|
||||
return new Version(0, 0);
|
||||
}
|
||||
|
||||
var identity = packageIdentity.Trim();
|
||||
var firstUnderscore = identity.IndexOf('_');
|
||||
if (firstUnderscore < 0 || firstUnderscore >= identity.Length - 1)
|
||||
{
|
||||
return new Version(0, 0);
|
||||
}
|
||||
|
||||
var secondUnderscore = identity.IndexOf('_', firstUnderscore + 1);
|
||||
if (secondUnderscore < 0)
|
||||
{
|
||||
return new Version(0, 0);
|
||||
}
|
||||
|
||||
var versionText = identity[(firstUnderscore + 1)..secondUnderscore];
|
||||
return Version.TryParse(versionText, out var version)
|
||||
? version
|
||||
: new Version(0, 0);
|
||||
}
|
||||
|
||||
private static bool TryExtractIconFromManifestInstallLocation(string installLocation, string appId, out byte[]? pngBytes)
|
||||
{
|
||||
pngBytes = null;
|
||||
var manifestPath = Path.Combine(installLocation, "AppxManifest.xml");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
XDocument document;
|
||||
try
|
||||
{
|
||||
document = XDocument.Load(manifestPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var applicationNodes = document
|
||||
.Descendants()
|
||||
.Where(node => string.Equals(node.Name.LocalName, "Application", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
if (applicationNodes.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var applicationNode = applicationNodes.FirstOrDefault(node =>
|
||||
string.Equals(
|
||||
node.Attributes().FirstOrDefault(attr => string.Equals(attr.Name.LocalName, "Id", StringComparison.OrdinalIgnoreCase))?.Value,
|
||||
appId,
|
||||
StringComparison.OrdinalIgnoreCase)) ?? applicationNodes.First();
|
||||
|
||||
var logoCandidates = new List<string>();
|
||||
CollectManifestLogoCandidates(applicationNode, logoCandidates);
|
||||
|
||||
foreach (var rawLogoPath in logoCandidates.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (var candidateFilePath in EnumerateManifestLogoFiles(installLocation, rawLogoPath))
|
||||
{
|
||||
if (TryConvertImageFileToPngBytes(candidateFilePath, out pngBytes))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void CollectManifestLogoCandidates(XElement applicationNode, List<string> output)
|
||||
{
|
||||
foreach (var node in applicationNode.DescendantsAndSelf())
|
||||
{
|
||||
var localName = node.Name.LocalName;
|
||||
if (!string.Equals(localName, "VisualElements", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(localName, "DefaultTile", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var attributeName in ManifestLogoAttributePriority)
|
||||
{
|
||||
var attribute = node
|
||||
.Attributes()
|
||||
.FirstOrDefault(attr => string.Equals(attr.Name.LocalName, attributeName, StringComparison.OrdinalIgnoreCase));
|
||||
if (attribute is not null && !string.IsNullOrWhiteSpace(attribute.Value))
|
||||
{
|
||||
output.Add(attribute.Value.Trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateManifestLogoFiles(string installLocation, string rawLogoPath)
|
||||
{
|
||||
var normalizedAssetPath = NormalizeManifestAssetPath(rawLogoPath);
|
||||
if (string.IsNullOrWhiteSpace(normalizedAssetPath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var baseCandidatePath = Path.GetFullPath(Path.Combine(installLocation, normalizedAssetPath));
|
||||
var files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (File.Exists(baseCandidatePath))
|
||||
{
|
||||
files.Add(baseCandidatePath);
|
||||
}
|
||||
|
||||
var directoryPath = Path.GetDirectoryName(baseCandidatePath);
|
||||
if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath))
|
||||
{
|
||||
return files;
|
||||
}
|
||||
|
||||
var baseName = Path.GetFileNameWithoutExtension(baseCandidatePath);
|
||||
var extension = Path.GetExtension(baseCandidatePath);
|
||||
var searchPattern = string.IsNullOrWhiteSpace(extension)
|
||||
? baseName + ".*"
|
||||
: baseName + "*" + extension;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var filePath in Directory.EnumerateFiles(directoryPath, searchPattern, SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
files.Add(filePath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore inaccessible folders
|
||||
}
|
||||
|
||||
return files
|
||||
.OrderByDescending(ScoreManifestIconCandidate)
|
||||
.ThenBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string NormalizeManifestAssetPath(string rawValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawValue))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var cleaned = rawValue.Trim().Trim('"');
|
||||
if (cleaned.StartsWith("ms-resource:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (cleaned.StartsWith("ms-appx:///", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cleaned = cleaned["ms-appx:///".Length..];
|
||||
}
|
||||
|
||||
cleaned = cleaned
|
||||
.Replace('/', Path.DirectorySeparatorChar)
|
||||
.Replace('\\', Path.DirectorySeparatorChar)
|
||||
.TrimStart(Path.DirectorySeparatorChar);
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static int ScoreManifestIconCandidate(string filePath)
|
||||
{
|
||||
var score = 0;
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
var targetSizeMatch = TargetSizeRegex.Match(fileName);
|
||||
if (targetSizeMatch.Success && int.TryParse(targetSizeMatch.Groups["n"].Value, out var targetSize))
|
||||
{
|
||||
score += targetSize * 100;
|
||||
}
|
||||
|
||||
var scaleMatch = ScaleRegex.Match(fileName);
|
||||
if (scaleMatch.Success && int.TryParse(scaleMatch.Groups["n"].Value, out var scale))
|
||||
{
|
||||
score += scale * 10;
|
||||
}
|
||||
|
||||
if (fileName.Contains("altform-unplated", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 300;
|
||||
}
|
||||
|
||||
if (Path.GetExtension(fileName).Equals(".png", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
score += 80;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private static bool TryConvertImageFileToPngBytes(string filePath, out byte[]? pngBytes)
|
||||
{
|
||||
pngBytes = null;
|
||||
try
|
||||
{
|
||||
using var image = Image.FromFile(filePath);
|
||||
using var stream = new MemoryStream();
|
||||
image.Save(stream, ImageFormat.Png);
|
||||
pngBytes = stream.ToArray();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern int GetPackagesByPackageFamily(
|
||||
string packageFamilyName,
|
||||
ref uint count,
|
||||
IntPtr[]? packageFullNames,
|
||||
ref uint bufferLength,
|
||||
char[]? buffer);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern int GetPackagePathByFullName(
|
||||
string packageFullName,
|
||||
ref uint pathLength,
|
||||
StringBuilder? path);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user