mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/imagecli
This commit is contained in:
@@ -124,7 +124,7 @@
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed AND WINDOWSBUILDNUMBER >= 22000" />
|
||||
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
|
||||
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
|
||||
<!-- TODO: Use to activate embedded MSIX -->
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Utilities;
|
||||
@@ -20,8 +20,32 @@ namespace ImageResizer
|
||||
{
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private const string LogSubFolder = "\\ImageResizer\\Logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached AI availability state, checked at app startup.
|
||||
/// Can be updated after model download completes or background initialization.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState AiAvailabilityState { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when AI initialization completes in background.
|
||||
/// Allows UI to refresh state when initialization finishes.
|
||||
/// </summary>
|
||||
public static event EventHandler<AiAvailabilityState> AiInitializationCompleted;
|
||||
|
||||
static App()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Initialize logger early (mirroring PowerOCR pattern)
|
||||
Logger.InitializeLogger(LogSubFolder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* swallow logger init issues silently */
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
@@ -30,9 +54,9 @@ namespace ImageResizer
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
|
||||
}
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
catch (CultureNotFoundException ex)
|
||||
{
|
||||
// error
|
||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
||||
}
|
||||
|
||||
Console.InputEncoding = Encoding.Unicode;
|
||||
@@ -43,15 +67,59 @@ namespace ImageResizer
|
||||
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
|
||||
NativeMethods.SetProcessDPIAware();
|
||||
|
||||
// Check for AI detection mode (called by Runner in background)
|
||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||
{
|
||||
RunAiDetectionMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
/* TODO: Add logs to ImageResizer.
|
||||
* Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
*/
|
||||
Logger.LogWarning("GPO policy disables ImageResizer. Exiting.");
|
||||
Environment.Exit(0); // Current.Exit won't work until there's a window opened.
|
||||
return;
|
||||
}
|
||||
|
||||
// AI Super Resolution is not supported on Windows 10 - skip cache check entirely
|
||||
if (OSVersionHelper.IsWindows10())
|
||||
{
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogInfo("AI Super Resolution not supported on Windows 10");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load AI availability from cache (written by Runner's background detection)
|
||||
var cachedState = Services.AiAvailabilityCacheService.LoadCache();
|
||||
|
||||
if (cachedState.HasValue)
|
||||
{
|
||||
AiAvailabilityState = cachedState.Value;
|
||||
Logger.LogInfo($"AI state loaded from cache: {AiAvailabilityState}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// No valid cache - default to NotSupported (Runner will detect and cache for next startup)
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
Logger.LogInfo("No AI cache found, defaulting to NotSupported");
|
||||
}
|
||||
|
||||
// If AI is potentially available, start background initialization (non-blocking)
|
||||
if (AiAvailabilityState == AiAvailabilityState.Ready)
|
||||
{
|
||||
_ = InitializeAiServiceAsync(); // Fire and forget - don't block UI
|
||||
}
|
||||
else
|
||||
{
|
||||
// AI not available - set NoOp service immediately
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
var batch = ResizeBatch.FromCommandLine(Console.In, e?.Args);
|
||||
|
||||
// TODO: Add command-line parameters that can be used in lieu of the input page (issue #14)
|
||||
@@ -62,9 +130,121 @@ namespace ImageResizer
|
||||
WindowHelpers.BringToForeground(new System.Windows.Interop.WindowInteropHelper(mainWindow).Handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI detection mode: perform detection, write to cache, and exit.
|
||||
/// Called by Runner in background to avoid blocking ImageResizer UI startup.
|
||||
/// </summary>
|
||||
private void RunAiDetectionMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("Running AI detection mode...");
|
||||
|
||||
// AI Super Resolution is not supported on Windows 10
|
||||
if (OSVersionHelper.IsWindows10())
|
||||
{
|
||||
Logger.LogInfo("AI detection skipped: Windows 10 does not support AI Super Resolution");
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform detection (reuse existing logic)
|
||||
var state = CheckAiAvailability();
|
||||
|
||||
// Write result to cache file
|
||||
Services.AiAvailabilityCacheService.SaveCache(state);
|
||||
|
||||
Logger.LogInfo($"AI detection complete: {state}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"AI detection failed: {ex.Message}");
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
}
|
||||
|
||||
// Exit silently without showing UI
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check AI Super Resolution availability on this system.
|
||||
/// Performs architecture check and model availability check.
|
||||
/// </summary>
|
||||
private static AiAvailabilityState CheckAiAvailability()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check Windows AI service model ready state
|
||||
// it's so slow, why?
|
||||
var readyState = Services.WinAiSuperResolutionService.GetModelReadyState();
|
||||
|
||||
// Map AI service state to our availability state
|
||||
switch (readyState)
|
||||
{
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
|
||||
return AiAvailabilityState.Ready;
|
||||
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
|
||||
return AiAvailabilityState.ModelNotReady;
|
||||
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser:
|
||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem:
|
||||
default:
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize AI Super Resolution service asynchronously in background.
|
||||
/// Runs without blocking UI startup - state change event notifies completion.
|
||||
/// </summary>
|
||||
private static async System.Threading.Tasks.Task InitializeAiServiceAsync()
|
||||
{
|
||||
AiAvailabilityState finalState;
|
||||
|
||||
try
|
||||
{
|
||||
// Create and initialize AI service using async factory
|
||||
var aiService = await Services.WinAiSuperResolutionService.CreateAsync();
|
||||
|
||||
if (aiService != null)
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||
Logger.LogInfo("AI Super Resolution service initialized successfully.");
|
||||
finalState = AiAvailabilityState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Initialization failed - use default NoOp service
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogWarning("AI Super Resolution service initialization failed. Using default service.");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error and use default NoOp service
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
|
||||
// Update cached state and notify listeners
|
||||
AiAvailabilityState = finalState;
|
||||
AiInitializationCompleted?.Invoke(null, finalState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
// Dispose AI Super Resolution service
|
||||
ResizeBatch.DisposeAiSuperResolutionService();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<UseWPF>true</UseWPF>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -18,19 +19,20 @@
|
||||
<RootNamespace>ImageResizer</RootNamespace>
|
||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- <PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||
</PropertyGroup> -->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
@@ -46,6 +48,8 @@
|
||||
<Resource Include="Resources\ImageResizer.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
|
||||
41
src/modules/imageresizer/ui/Models/AiSize.cs
Normal file
41
src/modules/imageresizer/ui/Models/AiSize.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class AiSize : ResizeSize
|
||||
{
|
||||
private static readonly CompositeFormat ScaleFormat = CompositeFormat.Parse(Resources.Input_AiScaleFormat);
|
||||
private int _scale = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatted scale display string (e.g., "2×").
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, _scale);
|
||||
|
||||
[JsonPropertyName("scale")]
|
||||
public int Scale
|
||||
{
|
||||
get => _scale;
|
||||
set => Set(ref _scale, value);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public AiSize(int scale)
|
||||
{
|
||||
Scale = scale;
|
||||
}
|
||||
|
||||
public AiSize()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,30 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class ResizeBatch
|
||||
{
|
||||
private readonly IFileSystem _fileSystem = new FileSystem();
|
||||
private static IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
public ICollection<string> Files { get; } = new List<string>();
|
||||
|
||||
public static void SetAiSuperResolutionService(IAISuperResolutionService service)
|
||||
{
|
||||
_aiSuperResolutionService = service;
|
||||
}
|
||||
|
||||
public static void DisposeAiSuperResolutionService()
|
||||
{
|
||||
_aiSuperResolutionService?.Dispose();
|
||||
_aiSuperResolutionService = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ResizeBatch from CliOptions.
|
||||
/// </summary>
|
||||
@@ -132,6 +145,9 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
protected virtual void Execute(string file, Settings settings)
|
||||
=> new ResizeOperation(file, DestinationDirectory, settings).Execute();
|
||||
{
|
||||
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
new ResizeOperation(file, DestinationDirectory, settings, aiService).Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Extensions;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Utilities;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
|
||||
@@ -30,6 +32,10 @@ namespace ImageResizer.Models
|
||||
private readonly string _file;
|
||||
private readonly string _destinationDirectory;
|
||||
private readonly Settings _settings;
|
||||
private readonly IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
// Cache CompositeFormat for AI error message formatting (CA1863)
|
||||
private static readonly CompositeFormat _aiErrorFormat = CompositeFormat.Parse(Resources.Error_AiProcessingFailed);
|
||||
|
||||
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
||||
private static readonly string[] _avoidFilenames =
|
||||
@@ -39,11 +45,12 @@ namespace ImageResizer.Models
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
};
|
||||
|
||||
public ResizeOperation(string file, string destinationDirectory, Settings settings)
|
||||
public ResizeOperation(string file, string destinationDirectory, Settings settings, IAISuperResolutionService aiSuperResolutionService = null)
|
||||
{
|
||||
_file = file;
|
||||
_destinationDirectory = destinationDirectory;
|
||||
_settings = settings;
|
||||
_aiSuperResolutionService = aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
@@ -167,6 +174,11 @@ namespace ImageResizer.Models
|
||||
|
||||
private BitmapSource Transform(BitmapSource source)
|
||||
{
|
||||
if (_settings.SelectedSize is AiSize)
|
||||
{
|
||||
return TransformWithAi(source);
|
||||
}
|
||||
|
||||
int originalWidth = source.PixelWidth;
|
||||
int originalHeight = source.PixelHeight;
|
||||
|
||||
@@ -257,6 +269,31 @@ namespace ImageResizer.Models
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
private BitmapSource TransformWithAi(BitmapSource source)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _aiSuperResolutionService.ApplySuperResolution(
|
||||
source,
|
||||
_settings.AiSize.Scale,
|
||||
_file);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException(Properties.Resources.Error_AiConversionFailed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Wrap the exception with a localized message
|
||||
// This will be caught by ResizeBatch.Process() and displayed to the user
|
||||
var errorMessage = string.Format(CultureInfo.CurrentCulture, _aiErrorFormat, ex.Message);
|
||||
throw new InvalidOperationException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
|
||||
/// In case of errors, we try to rebuild the metadata object and check again.
|
||||
@@ -363,19 +400,24 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
// Remove directory characters from the size's name.
|
||||
string sizeNameSanitized = _settings.SelectedSize.Name;
|
||||
sizeNameSanitized = sizeNameSanitized
|
||||
// For AI Size, use the scale display (e.g., "2×") instead of the full name
|
||||
string sizeName = _settings.SelectedSize is AiSize aiSize
|
||||
? aiSize.ScaleDisplay
|
||||
: _settings.SelectedSize.Name;
|
||||
string sizeNameSanitized = sizeName
|
||||
.Replace('\\', '_')
|
||||
.Replace('/', '_');
|
||||
|
||||
// Using CurrentCulture since this is user facing
|
||||
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
|
||||
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
|
||||
var fileName = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
_settings.FileNameFormat,
|
||||
originalFileName,
|
||||
sizeNameSanitized,
|
||||
_settings.SelectedSize.Width,
|
||||
_settings.SelectedSize.Height,
|
||||
selectedWidth,
|
||||
selectedHeight,
|
||||
encoder.Frames[0].PixelWidth,
|
||||
encoder.Frames[0].PixelHeight);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace ImageResizer.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@@ -78,6 +78,33 @@ namespace ImageResizer.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to convert image format for AI processing..
|
||||
/// </summary>
|
||||
public static string Error_AiConversionFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiConversionFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI super resolution processing failed: {0}.
|
||||
/// </summary>
|
||||
public static string Error_AiProcessingFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiProcessingFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI scaling operation failed..
|
||||
/// </summary>
|
||||
public static string Error_AiScalingFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiScalingFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Height.
|
||||
/// </summary>
|
||||
@@ -105,6 +132,132 @@ namespace ImageResizer.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current:.
|
||||
/// </summary>
|
||||
public static string Input_AiCurrentLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiCurrentLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Checking AI model availability....
|
||||
/// </summary>
|
||||
public static string Input_AiModelChecking {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelChecking", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI feature is disabled by system settings..
|
||||
/// </summary>
|
||||
public static string Input_AiModelDisabledByUser {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDisabledByUser", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Download.
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloadButton {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloadButton", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to download AI model. Please try again..
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloadFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloadFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Downloading AI model....
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloading {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloading", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI model not downloaded. Click Download to get started..
|
||||
/// </summary>
|
||||
public static string Input_AiModelNotAvailable {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelNotAvailable", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI feature is not supported on this system..
|
||||
/// </summary>
|
||||
public static string Input_AiModelNotSupported {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelNotSupported", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to New:.
|
||||
/// </summary>
|
||||
public static string Input_AiNewLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiNewLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0}×.
|
||||
/// </summary>
|
||||
public static string Input_AiScaleFormat {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleFormat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Scale.
|
||||
/// </summary>
|
||||
public static string Input_AiScaleLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Super resolution.
|
||||
/// </summary>
|
||||
public static string Input_AiSuperResolution {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiSuperResolution", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Upscale images using on-device AI.
|
||||
/// </summary>
|
||||
public static string Input_AiSuperResolutionDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiSuperResolutionDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unavailable.
|
||||
/// </summary>
|
||||
public static string Input_AiUnknownSize {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiUnknownSize", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to (auto).
|
||||
/// </summary>
|
||||
|
||||
@@ -296,4 +296,55 @@
|
||||
<data name="Input_ShrinkOnly.Content" xml:space="preserve">
|
||||
<value>_Make pictures smaller but not larger</value>
|
||||
</data>
|
||||
<data name="Input_AiSuperResolution" xml:space="preserve">
|
||||
<value>Super resolution</value>
|
||||
</data>
|
||||
<data name="Input_AiUnknownSize" xml:space="preserve">
|
||||
<value>Unavailable</value>
|
||||
</data>
|
||||
<data name="Input_AiScaleFormat" xml:space="preserve">
|
||||
<value>{0}×</value>
|
||||
</data>
|
||||
<data name="Input_AiScaleLabel" xml:space="preserve">
|
||||
<value>Scale</value>
|
||||
</data>
|
||||
<data name="Input_AiCurrentLabel" xml:space="preserve">
|
||||
<value>Current:</value>
|
||||
</data>
|
||||
<data name="Input_AiNewLabel" xml:space="preserve">
|
||||
<value>New:</value>
|
||||
</data>
|
||||
<data name="Input_AiModelChecking" xml:space="preserve">
|
||||
<value>Checking AI model availability...</value>
|
||||
</data>
|
||||
<data name="Input_AiModelNotAvailable" xml:space="preserve">
|
||||
<value>AI model not downloaded. Click Download to get started.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDisabledByUser" xml:space="preserve">
|
||||
<value>AI feature is disabled by system settings.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelNotSupported" xml:space="preserve">
|
||||
<value>AI feature is not supported on this system.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloading" xml:space="preserve">
|
||||
<value>Downloading AI model...</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloadFailed" xml:space="preserve">
|
||||
<value>Failed to download AI model. Please try again.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloadButton" xml:space="preserve">
|
||||
<value>Download</value>
|
||||
</data>
|
||||
<data name="Error_AiProcessingFailed" xml:space="preserve">
|
||||
<value>AI super resolution processing failed: {0}</value>
|
||||
</data>
|
||||
<data name="Error_AiConversionFailed" xml:space="preserve">
|
||||
<value>Failed to convert image format for AI processing.</value>
|
||||
</data>
|
||||
<data name="Error_AiScalingFailed" xml:space="preserve">
|
||||
<value>AI scaling operation failed.</value>
|
||||
</data>
|
||||
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
|
||||
<value>Upscale images using on-device AI</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -20,10 +20,22 @@ using System.Threading;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.ViewModels;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the availability state of AI Super Resolution feature.
|
||||
/// </summary>
|
||||
public enum AiAvailabilityState
|
||||
{
|
||||
NotSupported, // System doesn't support AI (architecture issue or policy disabled)
|
||||
ModelNotReady, // AI supported but model not downloaded
|
||||
Ready, // AI fully ready to use
|
||||
}
|
||||
|
||||
public sealed partial class Settings : IDataErrorInfo, INotifyPropertyChanged
|
||||
{
|
||||
private static readonly IFileSystem _fileSystem = new FileSystem();
|
||||
@@ -52,6 +64,7 @@ namespace ImageResizer.Properties
|
||||
private bool _keepDateModified;
|
||||
private System.Guid _fallbackEncoder;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public Settings()
|
||||
{
|
||||
@@ -74,9 +87,28 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = false;
|
||||
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
|
||||
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
|
||||
AiSize = new AiSize(2); // Initialize with default scale of 2
|
||||
AllSizes = new AllSizesCollection(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SelectedSizeIndex to ensure it's within the valid range.
|
||||
/// This handles cross-device migration where settings saved on ARM64 with AI selected
|
||||
/// are loaded on non-ARM64 devices.
|
||||
/// </summary>
|
||||
private void ValidateSelectedSizeIndex()
|
||||
{
|
||||
// Index structure: 0 to Sizes.Count-1 (regular), Sizes.Count (CustomSize), Sizes.Count+1 (AiSize)
|
||||
var maxIndex = ImageResizer.App.AiAvailabilityState == AiAvailabilityState.NotSupported
|
||||
? Sizes.Count // CustomSize only
|
||||
: Sizes.Count + 1; // CustomSize + AiSize
|
||||
|
||||
if (_selectedSizeIndex > maxIndex)
|
||||
{
|
||||
_selectedSizeIndex = 0; // Reset to first size
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<ResizeSize> AllSizes { get; set; }
|
||||
|
||||
@@ -96,15 +128,40 @@ namespace ImageResizer.Properties
|
||||
[JsonIgnore]
|
||||
public ResizeSize SelectedSize
|
||||
{
|
||||
get => SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count
|
||||
? Sizes[SelectedSizeIndex]
|
||||
: CustomSize;
|
||||
get
|
||||
{
|
||||
if (SelectedSizeIndex >= 0 && SelectedSizeIndex < Sizes.Count)
|
||||
{
|
||||
return Sizes[SelectedSizeIndex];
|
||||
}
|
||||
else if (SelectedSizeIndex == Sizes.Count)
|
||||
{
|
||||
return CustomSize;
|
||||
}
|
||||
else if (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported && SelectedSizeIndex == Sizes.Count + 1)
|
||||
{
|
||||
return AiSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to CustomSize when index is out of range or AI is not available
|
||||
return CustomSize;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
var index = Sizes.IndexOf(value);
|
||||
if (index == -1)
|
||||
{
|
||||
index = Sizes.Count;
|
||||
if (value is AiSize)
|
||||
{
|
||||
index = Sizes.Count + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = Sizes.Count;
|
||||
}
|
||||
}
|
||||
|
||||
SelectedSizeIndex = index;
|
||||
@@ -140,13 +197,17 @@ namespace ImageResizer.Properties
|
||||
|
||||
private class AllSizesCollection : IEnumerable<ResizeSize>, INotifyCollectionChanged, INotifyPropertyChanged
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
private ObservableCollection<ResizeSize> _sizes;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public AllSizesCollection(Settings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_sizes = settings.Sizes;
|
||||
_customSize = settings.CustomSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
_sizes.CollectionChanged += HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
||||
@@ -165,6 +226,18 @@ namespace ImageResizer.Properties
|
||||
oldCustomSize,
|
||||
_sizes.Count));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Models.AiSize))
|
||||
{
|
||||
var oldAiSize = _aiSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace,
|
||||
_aiSize,
|
||||
oldAiSize,
|
||||
_sizes.Count + 1));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Sizes))
|
||||
{
|
||||
var oldSizes = _sizes;
|
||||
@@ -187,12 +260,30 @@ namespace ImageResizer.Properties
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public int Count
|
||||
=> _sizes.Count + 1;
|
||||
=> _sizes.Count + 1 + (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported ? 1 : 0);
|
||||
|
||||
public ResizeSize this[int index]
|
||||
=> index == _sizes.Count
|
||||
? _customSize
|
||||
: _sizes[index];
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < _sizes.Count)
|
||||
{
|
||||
return _sizes[index];
|
||||
}
|
||||
else if (index == _sizes.Count)
|
||||
{
|
||||
return _customSize;
|
||||
}
|
||||
else if (ImageResizer.App.AiAvailabilityState != AiAvailabilityState.NotSupported && index == _sizes.Count + 1)
|
||||
{
|
||||
return _aiSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for AllSizesCollection.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<ResizeSize> GetEnumerator()
|
||||
=> new AllSizesEnumerator(this);
|
||||
@@ -412,6 +503,18 @@ namespace ImageResizer.Properties
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_aiSize")]
|
||||
public AiSize AiSize
|
||||
{
|
||||
get => _aiSize;
|
||||
set
|
||||
{
|
||||
_aiSize = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public static string SettingsPath { get => _settingsPath; set => _settingsPath = value; }
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
@@ -489,6 +592,7 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = jsonSettings.KeepDateModified;
|
||||
FallbackEncoder = jsonSettings.FallbackEncoder;
|
||||
CustomSize = jsonSettings.CustomSize;
|
||||
AiSize = jsonSettings.AiSize ?? new AiSize(InputViewModel.DefaultAiScale);
|
||||
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
|
||||
|
||||
if (jsonSettings.Sizes.Count > 0)
|
||||
@@ -499,6 +603,10 @@ namespace ImageResizer.Properties
|
||||
// Ensure Ids are unique and handle missing Ids
|
||||
IdRecoveryHelper.RecoverInvalidIds(Sizes);
|
||||
}
|
||||
|
||||
// Validate SelectedSizeIndex after Sizes collection has been updated
|
||||
// This handles cross-device migration (e.g., ARM64 -> non-ARM64)
|
||||
ValidateSelectedSizeIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using ImageResizer.Properties;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for caching AI availability detection results.
|
||||
/// Persists results to avoid slow API calls on every startup.
|
||||
/// Runner calls ImageResizer --detect-ai to perform detection,
|
||||
/// and ImageResizer reads the cached result on normal startup.
|
||||
/// </summary>
|
||||
public static class AiAvailabilityCacheService
|
||||
{
|
||||
private const string CacheFileName = "ai_capabilities.json";
|
||||
private const int CacheVersion = 1;
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private static string CachePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft",
|
||||
"PowerToys",
|
||||
CacheFileName);
|
||||
|
||||
/// <summary>
|
||||
/// Load AI availability state from cache.
|
||||
/// Returns null if cache doesn't exist, is invalid, or read fails.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState? LoadCache()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(CachePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(CachePath);
|
||||
var cache = JsonSerializer.Deserialize<AiCapabilityCache>(json);
|
||||
|
||||
if (!IsCacheValid(cache))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (AiAvailabilityState)cache.State;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Read failure (file locked, corrupted JSON, etc.) - return null and use fallback
|
||||
Logger.LogError($"Failed to load AI cache: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save AI availability state to cache.
|
||||
/// Called by --detect-ai mode after performing detection.
|
||||
/// </summary>
|
||||
public static void SaveCache(AiAvailabilityState state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cache = new AiCapabilityCache
|
||||
{
|
||||
Version = CacheVersion,
|
||||
State = (int)state,
|
||||
WindowsBuild = Environment.OSVersion.Version.ToString(),
|
||||
Architecture = RuntimeInformation.ProcessArchitecture.ToString(),
|
||||
Timestamp = DateTime.UtcNow.ToString("o"),
|
||||
};
|
||||
|
||||
var dir = Path.GetDirectoryName(CachePath);
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(cache, SerializerOptions);
|
||||
File.WriteAllText(CachePath, json);
|
||||
|
||||
Logger.LogInfo($"AI cache saved: {state}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save AI cache: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate cache against current system environment.
|
||||
/// Cache is invalid if version, architecture, or Windows build changed.
|
||||
/// </summary>
|
||||
private static bool IsCacheValid(AiCapabilityCache cache)
|
||||
{
|
||||
if (cache == null || cache.Version != CacheVersion)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache.Architecture != RuntimeInformation.ProcessArchitecture.ToString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cache.WindowsBuild != Environment.OSVersion.Version.ToString())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/modules/imageresizer/ui/Services/AiCapabilityCache.cs
Normal file
22
src/modules/imageresizer/ui/Services/AiCapabilityCache.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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.
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Data model for AI capability cache file.
|
||||
/// </summary>
|
||||
internal sealed class AiCapabilityCache
|
||||
{
|
||||
public int Version { get; set; }
|
||||
|
||||
public int State { get; set; }
|
||||
|
||||
public string WindowsBuild { get; set; }
|
||||
|
||||
public string Architecture { get; set; }
|
||||
|
||||
public string Timestamp { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public interface IAISuperResolutionService : IDisposable
|
||||
{
|
||||
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.Windows.Media.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class NoOpAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
public static NoOpAiSuperResolutionService Instance { get; } = new NoOpAiSuperResolutionService();
|
||||
|
||||
private NoOpAiSuperResolutionService()
|
||||
{
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No resources to dispose in no-op implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
// 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;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Windows.AI;
|
||||
using Microsoft.Windows.AI.Imaging;
|
||||
using Windows.Graphics.Imaging;
|
||||
|
||||
namespace ImageResizer.Services
|
||||
{
|
||||
public sealed class WinAiSuperResolutionService : IAISuperResolutionService
|
||||
{
|
||||
private readonly ImageScaler _imageScaler;
|
||||
private readonly object _usageLock = new object();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WinAiSuperResolutionService"/> class.
|
||||
/// Private constructor. Use CreateAsync() factory method to create instances.
|
||||
/// </summary>
|
||||
private WinAiSuperResolutionService(ImageScaler imageScaler)
|
||||
{
|
||||
_imageScaler = imageScaler ?? throw new ArgumentNullException(nameof(imageScaler));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async factory method to create and initialize WinAiSuperResolutionService.
|
||||
/// Returns null if initialization fails.
|
||||
/// </summary>
|
||||
public static async Task<WinAiSuperResolutionService> CreateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageScaler = await ImageScaler.CreateAsync();
|
||||
if (imageScaler == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new WinAiSuperResolutionService(imageScaler);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static AIFeatureReadyState GetModelReadyState()
|
||||
{
|
||||
try
|
||||
{
|
||||
return ImageScaler.GetReadyState();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If we can't get the state, treat it as disabled by user
|
||||
// The caller should check if it's Ready or NotReady
|
||||
return AIFeatureReadyState.DisabledByUser;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<AIFeatureReadyResult> EnsureModelReadyAsync(IProgress<double> progress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var operation = ImageScaler.EnsureReadyAsync();
|
||||
|
||||
// Register progress handler if provided
|
||||
if (progress != null)
|
||||
{
|
||||
operation.Progress = (asyncInfo, progressValue) =>
|
||||
{
|
||||
// progressValue is a double representing completion percentage (0.0 to 1.0 or 0 to 100)
|
||||
progress.Report(progressValue);
|
||||
};
|
||||
}
|
||||
|
||||
return await operation;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath)
|
||||
{
|
||||
if (source == null || _disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Note: filePath parameter reserved for future use (e.g., logging, caching)
|
||||
// Currently not used by the ImageScaler API
|
||||
try
|
||||
{
|
||||
// Convert WPF BitmapSource to WinRT SoftwareBitmap
|
||||
var softwareBitmap = ConvertBitmapSourceToSoftwareBitmap(source);
|
||||
if (softwareBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Calculate target dimensions
|
||||
var newWidth = softwareBitmap.PixelWidth * scale;
|
||||
var newHeight = softwareBitmap.PixelHeight * scale;
|
||||
|
||||
// Apply super resolution with thread-safe access
|
||||
// _usageLock protects concurrent access from Parallel.ForEach threads
|
||||
SoftwareBitmap scaledBitmap;
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
scaledBitmap = _imageScaler.ScaleSoftwareBitmap(softwareBitmap, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (scaledBitmap == null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Convert back to WPF BitmapSource
|
||||
return ConvertSoftwareBitmapToBitmapSource(scaledBitmap);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Any error, return original image gracefully
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
private static SoftwareBitmap ConvertBitmapSourceToSoftwareBitmap(BitmapSource bitmapSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure the bitmap is in a compatible format
|
||||
var convertedBitmap = new FormatConvertedBitmap();
|
||||
convertedBitmap.BeginInit();
|
||||
convertedBitmap.Source = bitmapSource;
|
||||
convertedBitmap.DestinationFormat = PixelFormats.Bgra32;
|
||||
convertedBitmap.EndInit();
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra32
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
convertedBitmap.CopyPixels(pixels, stride, 0);
|
||||
|
||||
// Create SoftwareBitmap from pixel data
|
||||
var softwareBitmap = new SoftwareBitmap(
|
||||
BitmapPixelFormat.Bgra8,
|
||||
width,
|
||||
height,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
using (var buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy(pixels, 0, (IntPtr)dataInBytes, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return softwareBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapSource ConvertSoftwareBitmapToBitmapSource(SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Convert to Bgra8 format if needed
|
||||
var convertedBitmap = SoftwareBitmap.Convert(
|
||||
softwareBitmap,
|
||||
BitmapPixelFormat.Bgra8,
|
||||
BitmapAlphaMode.Premultiplied);
|
||||
|
||||
int width = convertedBitmap.PixelWidth;
|
||||
int height = convertedBitmap.PixelHeight;
|
||||
int stride = width * 4; // 4 bytes per pixel for Bgra8
|
||||
byte[] pixels = new byte[height * stride];
|
||||
|
||||
using (var buffer = convertedBitmap.LockBuffer(BitmapBufferAccessMode.Read))
|
||||
using (var reference = buffer.CreateReference())
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
((IMemoryBufferByteAccess)reference).GetBuffer(out byte* dataInBytes, out uint capacity);
|
||||
System.Runtime.InteropServices.Marshal.Copy((IntPtr)dataInBytes, pixels, 0, pixels.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// Create WPF BitmapSource from pixel data
|
||||
var wpfBitmap = BitmapSource.Create(
|
||||
width,
|
||||
height,
|
||||
96, // DPI X
|
||||
96, // DPI Y
|
||||
PixelFormats.Bgra32,
|
||||
null,
|
||||
pixels,
|
||||
stride);
|
||||
|
||||
wpfBitmap.Freeze(); // Make it thread-safe
|
||||
return wpfBitmap;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IMemoryBufferByteAccess
|
||||
{
|
||||
unsafe void GetBuffer(out byte* buffer, out uint capacity);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_usageLock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ImageScaler implements IDisposable
|
||||
(_imageScaler as IDisposable)?.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,22 +6,41 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
|
||||
using System.Windows.Media.Imaging;
|
||||
using Common.UI;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Views;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public class InputViewModel : Observable
|
||||
{
|
||||
public const int DefaultAiScale = 2;
|
||||
private const int MinAiScale = 1;
|
||||
private const int MaxAiScale = 8;
|
||||
|
||||
private readonly ResizeBatch _batch;
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
private readonly IMainView _mainView;
|
||||
private readonly bool _hasMultipleFiles;
|
||||
private bool _originalDimensionsLoaded;
|
||||
private int? _originalWidth;
|
||||
private int? _originalHeight;
|
||||
private string _currentResolutionDescription;
|
||||
private string _newResolutionDescription;
|
||||
private bool _isDownloadingModel;
|
||||
private string _modelStatusMessage;
|
||||
private double _modelDownloadProgress;
|
||||
|
||||
public enum Dimension
|
||||
{
|
||||
@@ -45,24 +64,114 @@ namespace ImageResizer.ViewModels
|
||||
_batch = batch;
|
||||
_mainViewModel = mainViewModel;
|
||||
_mainView = mainView;
|
||||
_hasMultipleFiles = _batch?.Files.Count > 1;
|
||||
|
||||
Settings = settings;
|
||||
if (settings != null)
|
||||
{
|
||||
settings.CustomSize.PropertyChanged += (sender, e) => settings.SelectedSize = (CustomSize)sender;
|
||||
settings.AiSize.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(AiSize.Scale))
|
||||
{
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
};
|
||||
settings.PropertyChanged += HandleSettingsPropertyChanged;
|
||||
}
|
||||
|
||||
ResizeCommand = new RelayCommand(Resize);
|
||||
ResizeCommand = new RelayCommand(Resize, () => CanResize);
|
||||
CancelCommand = new RelayCommand(Cancel);
|
||||
OpenSettingsCommand = new RelayCommand(OpenSettings);
|
||||
EnterKeyPressedCommand = new RelayCommand<KeyPressParams>(HandleEnterKeyPress);
|
||||
DownloadModelCommand = new RelayCommand(async () => await DownloadModelAsync());
|
||||
|
||||
// Initialize AI UI state based on Settings availability
|
||||
InitializeAiState();
|
||||
}
|
||||
|
||||
public Settings Settings { get; }
|
||||
|
||||
public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues(typeof(ResizeFit)).Cast<ResizeFit>();
|
||||
public IEnumerable<ResizeFit> ResizeFitValues => Enum.GetValues<ResizeFit>();
|
||||
|
||||
public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues(typeof(ResizeUnit)).Cast<ResizeUnit>();
|
||||
public IEnumerable<ResizeUnit> ResizeUnitValues => Enum.GetValues<ResizeUnit>();
|
||||
|
||||
public int AiSuperResolutionScale
|
||||
{
|
||||
get => Settings?.AiSize?.Scale ?? DefaultAiScale;
|
||||
set
|
||||
{
|
||||
if (Settings?.AiSize != null && Settings.AiSize.Scale != value)
|
||||
{
|
||||
Settings.AiSize.Scale = value;
|
||||
NotifyAiScaleChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string AiScaleDisplay => Settings?.AiSize?.ScaleDisplay ?? string.Empty;
|
||||
|
||||
public string CurrentResolutionDescription
|
||||
{
|
||||
get => _currentResolutionDescription;
|
||||
private set => Set(ref _currentResolutionDescription, value);
|
||||
}
|
||||
|
||||
public string NewResolutionDescription
|
||||
{
|
||||
get => _newResolutionDescription;
|
||||
private set => Set(ref _newResolutionDescription, value);
|
||||
}
|
||||
|
||||
// ==================== UI State Properties ====================
|
||||
|
||||
// Show AI size descriptions only when AI size is selected and not multiple files
|
||||
public bool ShowAiSizeDescriptions => Settings?.SelectedSize is AiSize && !_hasMultipleFiles;
|
||||
|
||||
// Helper property: Is model currently being downloaded?
|
||||
public bool IsModelDownloading => _isDownloadingModel;
|
||||
|
||||
public string ModelStatusMessage
|
||||
{
|
||||
get => _modelStatusMessage;
|
||||
private set => Set(ref _modelStatusMessage, value);
|
||||
}
|
||||
|
||||
public double ModelDownloadProgress
|
||||
{
|
||||
get => _modelDownloadProgress;
|
||||
private set => Set(ref _modelDownloadProgress, value);
|
||||
}
|
||||
|
||||
// Show download prompt when: AI size is selected and model is not ready (including downloading)
|
||||
public bool ShowModelDownloadPrompt =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
(App.AiAvailabilityState == Properties.AiAvailabilityState.ModelNotReady || _isDownloadingModel);
|
||||
|
||||
// Show AI controls when: AI size is selected and AI is ready
|
||||
public bool ShowAiControls =>
|
||||
Settings?.SelectedSize is AiSize &&
|
||||
App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the resize operation can proceed.
|
||||
/// For AI resize: only enabled when AI is fully ready.
|
||||
/// For non-AI resize: always enabled.
|
||||
/// </summary>
|
||||
public bool CanResize
|
||||
{
|
||||
get
|
||||
{
|
||||
// If AI size is selected, only allow resize when AI is fully ready
|
||||
if (Settings?.SelectedSize is AiSize)
|
||||
{
|
||||
return App.AiAvailabilityState == Properties.AiAvailabilityState.Ready;
|
||||
}
|
||||
|
||||
// Non-AI resize can always proceed
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ResizeCommand { get; }
|
||||
|
||||
@@ -72,9 +181,11 @@ namespace ImageResizer.ViewModels
|
||||
|
||||
public ICommand EnterKeyPressedCommand { get; private set; }
|
||||
|
||||
public ICommand DownloadModelCommand { get; private set; }
|
||||
|
||||
// Any of the files is a gif
|
||||
public bool TryingToResizeGifFiles =>
|
||||
_batch.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase));
|
||||
_batch?.Files.Any(filename => filename.EndsWith(".gif", System.StringComparison.InvariantCultureIgnoreCase)) == true;
|
||||
|
||||
public void Resize()
|
||||
{
|
||||
@@ -102,5 +213,234 @@ namespace ImageResizer.ViewModels
|
||||
|
||||
public void Cancel()
|
||||
=> _mainView.Close();
|
||||
|
||||
private void HandleSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Settings.SelectedSizeIndex):
|
||||
case nameof(Settings.SelectedSize):
|
||||
// Notify UI state properties that depend on SelectedSize
|
||||
NotifyAiStateChanged();
|
||||
UpdateAiDetails();
|
||||
|
||||
// Trigger CanExecuteChanged for ResizeCommand
|
||||
if (ResizeCommand is RelayCommand cmd)
|
||||
{
|
||||
cmd.OnCanExecuteChanged();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAiScaleWithinRange()
|
||||
{
|
||||
if (Settings?.AiSize != null)
|
||||
{
|
||||
Settings.AiSize.Scale = Math.Clamp(
|
||||
Settings.AiSize.Scale,
|
||||
MinAiScale,
|
||||
MaxAiScale);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAiDetails()
|
||||
{
|
||||
// Clear AI details if AI size not selected
|
||||
if (Settings == null || Settings.SelectedSize is not AiSize)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureAiScaleWithinRange();
|
||||
|
||||
if (_hasMultipleFiles)
|
||||
{
|
||||
CurrentResolutionDescription = string.Empty;
|
||||
NewResolutionDescription = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureOriginalDimensionsLoaded();
|
||||
|
||||
var hasConcreteSize = _originalWidth.HasValue && _originalHeight.HasValue;
|
||||
CurrentResolutionDescription = hasConcreteSize
|
||||
? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
|
||||
var scale = Settings.AiSize.Scale;
|
||||
NewResolutionDescription = hasConcreteSize
|
||||
? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
}
|
||||
|
||||
private static string FormatDimensions(long width, long height)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0} × {1}", width, height);
|
||||
}
|
||||
|
||||
private void EnsureOriginalDimensionsLoaded()
|
||||
{
|
||||
if (_originalDimensionsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var file = _batch?.Files.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(file))
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(file);
|
||||
var decoder = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
|
||||
var frame = decoder.Frames.FirstOrDefault();
|
||||
if (frame != null)
|
||||
{
|
||||
_originalWidth = frame.PixelWidth;
|
||||
_originalHeight = frame.PixelHeight;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Failed to load image dimensions - clear values
|
||||
_originalWidth = null;
|
||||
_originalHeight = null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_originalDimensionsLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes AI UI state based on App's cached availability state.
|
||||
/// Subscribe to state change event to update UI when background initialization completes.
|
||||
/// </summary>
|
||||
private void InitializeAiState()
|
||||
{
|
||||
// Subscribe to initialization completion event to refresh UI
|
||||
App.AiInitializationCompleted += OnAiInitializationCompleted;
|
||||
|
||||
// Set initial status message based on current state
|
||||
UpdateStatusMessage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles AI initialization completion event from App.
|
||||
/// Refreshes UI when background initialization finishes.
|
||||
/// </summary>
|
||||
private void OnAiInitializationCompleted(object sender, Properties.AiAvailabilityState finalState)
|
||||
{
|
||||
UpdateStatusMessage();
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates status message based on current App availability state.
|
||||
/// </summary>
|
||||
private void UpdateStatusMessage()
|
||||
{
|
||||
ModelStatusMessage = App.AiAvailabilityState switch
|
||||
{
|
||||
Properties.AiAvailabilityState.Ready => string.Empty,
|
||||
Properties.AiAvailabilityState.ModelNotReady => Resources.Input_AiModelNotAvailable,
|
||||
Properties.AiAvailabilityState.NotSupported => Resources.Input_AiModelNotSupported,
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies UI when AI state changes (model availability, download status).
|
||||
/// </summary>
|
||||
private void NotifyAiStateChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsModelDownloading));
|
||||
OnPropertyChanged(nameof(ShowModelDownloadPrompt));
|
||||
OnPropertyChanged(nameof(ShowAiControls));
|
||||
OnPropertyChanged(nameof(ShowAiSizeDescriptions));
|
||||
OnPropertyChanged(nameof(CanResize));
|
||||
|
||||
// Trigger CanExecuteChanged for ResizeCommand
|
||||
if (ResizeCommand is RelayCommand resizeCommand)
|
||||
{
|
||||
resizeCommand.OnCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies UI when AI scale changes (slider value).
|
||||
/// </summary>
|
||||
private void NotifyAiScaleChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(AiSuperResolutionScale));
|
||||
OnPropertyChanged(nameof(AiScaleDisplay));
|
||||
UpdateAiDetails();
|
||||
}
|
||||
|
||||
private async Task DownloadModelAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Set downloading flag and show progress
|
||||
_isDownloadingModel = true;
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloading;
|
||||
ModelDownloadProgress = 0;
|
||||
NotifyAiStateChanged();
|
||||
|
||||
// Create progress reporter to update UI
|
||||
var progress = new Progress<double>(value =>
|
||||
{
|
||||
// progressValue could be 0-1 or 0-100, normalize to 0-100
|
||||
ModelDownloadProgress = value > 1 ? value : value * 100;
|
||||
});
|
||||
|
||||
// Call EnsureReadyAsync to download and prepare the AI model
|
||||
var result = await WinAiSuperResolutionService.EnsureModelReadyAsync(progress);
|
||||
|
||||
if (result?.Status == Microsoft.Windows.AI.AIFeatureReadyResultState.Success)
|
||||
{
|
||||
// Model successfully downloaded and ready
|
||||
ModelDownloadProgress = 100;
|
||||
|
||||
// Update App's cached state
|
||||
App.AiAvailabilityState = Properties.AiAvailabilityState.Ready;
|
||||
UpdateStatusMessage();
|
||||
|
||||
// Initialize the AI service now that model is ready
|
||||
var aiService = await WinAiSuperResolutionService.CreateAsync();
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService ?? (Services.IAISuperResolutionService)NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Download failed
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Exception during download
|
||||
ModelStatusMessage = Resources.Input_AiModelDownloadFailed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clear downloading flag
|
||||
_isDownloadingModel = false;
|
||||
|
||||
// Reset progress if not successful
|
||||
if (App.AiAvailabilityState != Properties.AiAvailabilityState.Ready)
|
||||
{
|
||||
ModelDownloadProgress = 0;
|
||||
}
|
||||
|
||||
NotifyAiStateChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,21 @@ using System.Windows.Data;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
[ValueConversion(typeof(Enum), typeof(string))]
|
||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||
internal class BoolValueConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> (bool)value ? Visibility.Visible : Visibility.Collapsed;
|
||||
{
|
||||
bool boolValue = (bool)value;
|
||||
bool invert = parameter is string param && param.Equals("Inverted", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (invert)
|
||||
{
|
||||
boolValue = !boolValue;
|
||||
}
|
||||
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> (Visibility)value == Visibility.Visible;
|
||||
|
||||
@@ -7,6 +7,23 @@
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:v="clr-namespace:ImageResizer.Views">
|
||||
|
||||
<UserControl.Resources>
|
||||
<Style
|
||||
x:Key="ReadableDisabledButtonStyle"
|
||||
BasedOn="{StaticResource {x:Type ui:Button}}"
|
||||
TargetType="ui:Button">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<!-- Improved disabled state: keep readable but clearly disabled -->
|
||||
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
|
||||
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" />
|
||||
<Setter Property="Opacity" Value="0.75" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -15,61 +32,67 @@
|
||||
<!-- other controls -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ComboBox
|
||||
Name="SizeComboBox"
|
||||
Grid.Row="0"
|
||||
Height="64"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.HelpText="{Binding Settings.SelectedSize, Converter={StaticResource SizeTypeToHelpTextConverter}}"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Image_Sizes}"
|
||||
ItemsSource="{Binding Settings.AllSizes}"
|
||||
SelectedIndex="{Binding Settings.SelectedSizeIndex}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource {x:Type ComboBoxItem}}" TargetType="ComboBoxItem">
|
||||
<Setter Property="AutomationProperties.Name" Value="{Binding Name}" />
|
||||
<Setter Property="AutomationProperties.HelpText" Value="{Binding ., Converter={StaticResource SizeTypeToHelpTextConverter}}" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.Resources>
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding Name}" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="×"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||
<StackPanel Grid.Row="0" Margin="16">
|
||||
<ComboBox
|
||||
Name="SizeComboBox"
|
||||
Height="64"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.HelpText="{Binding Settings.SelectedSize, Converter={StaticResource SizeTypeToHelpTextConverter}}"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Image_Sizes}"
|
||||
ItemsSource="{Binding Settings.AllSizes}"
|
||||
SelectedItem="{Binding Settings.SelectedSize, Mode=TwoWay}">
|
||||
<ComboBox.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource {x:Type ComboBoxItem}}" TargetType="ComboBoxItem">
|
||||
<Setter Property="AutomationProperties.Name" Value="{Binding Name}" />
|
||||
<Setter Property="AutomationProperties.HelpText" Value="{Binding ., Converter={StaticResource SizeTypeToHelpTextConverter}}" />
|
||||
</Style>
|
||||
</ComboBox.ItemContainerStyle>
|
||||
<ComboBox.Resources>
|
||||
<DataTemplate DataType="{x:Type m:ResizeSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding Name}" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="×"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{Binding Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||
Visibility="{Binding ShowHeight, Converter={StaticResource BoolValueConverter}}" />
|
||||
<TextBlock Margin="4,0,0,0" Text="{Binding Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:CustomSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:AiSize}">
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Vertical">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Static p:Resources.Input_AiSuperResolution}" />
|
||||
<TextBlock Text="{x:Static p:Resources.Input_AiSuperResolutionDescription}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:CustomSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
|
||||
</DataTemplate>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -84,6 +107,90 @@
|
||||
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0" />
|
||||
|
||||
<!-- AI Configuration Panel -->
|
||||
<Grid Margin="16">
|
||||
<!-- AI Model Download Prompt -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowModelDownloadPrompt}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
<ui:InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{Binding ModelStatusMessage}"
|
||||
Severity="Informational" />
|
||||
|
||||
<ui:Button
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Appearance="Primary"
|
||||
Command="{Binding DownloadModelCommand}"
|
||||
Content="{x:Static p:Resources.Input_AiModelDownloadButton}"
|
||||
Visibility="{Binding IsModelDownloading, Converter={StaticResource BoolValueConverter}, ConverterParameter=Inverted}" />
|
||||
|
||||
<StackPanel Margin="0,8,0,0" Visibility="{Binding IsModelDownloading, Converter={StaticResource BoolValueConverter}}">
|
||||
<ui:ProgressRing IsIndeterminate="True" />
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding ModelStatusMessage}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- AI Scale Controls -->
|
||||
<StackPanel>
|
||||
<StackPanel.Style>
|
||||
<Style TargetType="StackPanel">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowAiControls}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</StackPanel.Style>
|
||||
|
||||
<Grid>
|
||||
<TextBlock Text="{x:Static p:Resources.Input_AiCurrentLabel}" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding AiScaleDisplay}" />
|
||||
</Grid>
|
||||
|
||||
<Slider
|
||||
Margin="0,8,0,0"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Input_AiScaleLabel}"
|
||||
IsSelectionRangeEnabled="False"
|
||||
IsSnapToTickEnabled="True"
|
||||
Maximum="8"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="BottomRight"
|
||||
Ticks="1,2,3,4,5,6,7,8"
|
||||
Value="{Binding AiSuperResolutionScale, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel Margin="0,16,0,0" Visibility="{Binding ShowAiSizeDescriptions, Converter={StaticResource BoolValueConverter}}">
|
||||
<Grid>
|
||||
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}" Text="{x:Static p:Resources.Input_AiCurrentLabel}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding CurrentResolutionDescription}" />
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<TextBlock Text="{x:Static p:Resources.Input_AiNewLabel}" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding NewResolutionDescription}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- "Custom" input matrix -->
|
||||
<Grid Margin="16" Visibility="{Binding ElementName=SizeComboBox, Path=SelectedValue, Converter={StaticResource SizeTypeToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -280,7 +387,8 @@
|
||||
Appearance="Primary"
|
||||
AutomationProperties.Name="{x:Static p:Resources.Resize_Tooltip}"
|
||||
Command="{Binding ResizeCommand}"
|
||||
IsDefault="True">
|
||||
IsDefault="True"
|
||||
Style="{StaticResource ReadableDisabledButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ui:SymbolIcon FontSize="16" Symbol="ResizeLarge16" />
|
||||
<TextBlock Margin="8,0,0,0" Text="{x:Static p:Resources.Input_Resize}" />
|
||||
|
||||
11
src/runner/ai_detection.h
Normal file
11
src/runner/ai_detection.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// Detect AI capabilities by calling ImageResizer in detection mode.
|
||||
// This runs in a background thread to avoid blocking.
|
||||
// ImageResizer writes the result to a cache file that it reads on normal startup.
|
||||
//
|
||||
// Parameters:
|
||||
// skipSettingsCheck - If true, skip checking if ImageResizer is enabled in settings.
|
||||
// Use this when called from apply_general_settings where we know
|
||||
// ImageResizer is being enabled but settings file may not be saved yet.
|
||||
void DetectAiCapabilitiesAsync(bool skipSettingsCheck = false);
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <common/themes/windows_colors.h>
|
||||
|
||||
#include "trace.h"
|
||||
#include "ai_detection.h"
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/version/version.h>
|
||||
#include <common/utils/resources.h>
|
||||
@@ -279,6 +280,13 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
||||
powertoy->enable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.EnableHotkeyByModule(name);
|
||||
|
||||
// Trigger AI capability detection when ImageResizer is enabled
|
||||
if (name == L"Image Resizer")
|
||||
{
|
||||
Logger::info(L"ImageResizer enabled, triggering AI capability detection");
|
||||
DetectAiCapabilitiesAsync(true); // Skip settings check since we know it's being enabled
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -34,8 +34,11 @@
|
||||
|
||||
#include <Psapi.h>
|
||||
#include <RestartManager.h>
|
||||
#include <shellapi.h>
|
||||
#include "centralized_kb_hook.h"
|
||||
#include "centralized_hotkeys.h"
|
||||
#include "ai_detection.h"
|
||||
#include <common/utils/package.h>
|
||||
|
||||
#if _DEBUG && _WIN64
|
||||
#include "unhandled_exception_handler.h"
|
||||
@@ -76,6 +79,87 @@ void chdir_current_executable()
|
||||
}
|
||||
}
|
||||
|
||||
// Detect AI capabilities by calling ImageResizer in detection mode.
|
||||
// This runs in a background thread to avoid blocking the main startup.
|
||||
// ImageResizer writes the result to a cache file that it reads on normal startup.
|
||||
void DetectAiCapabilitiesAsync(bool skipSettingsCheck)
|
||||
{
|
||||
std::thread([skipSettingsCheck]() {
|
||||
try
|
||||
{
|
||||
// Check if ImageResizer module is enabled (skip if called from apply_general_settings)
|
||||
if (!skipSettingsCheck)
|
||||
{
|
||||
auto settings = PTSettingsHelper::load_general_settings();
|
||||
if (json::has(settings, L"enabled", json::JsonValueType::Object))
|
||||
{
|
||||
auto enabledModules = settings.GetNamedObject(L"enabled");
|
||||
if (json::has(enabledModules, L"Image Resizer", json::JsonValueType::Boolean))
|
||||
{
|
||||
bool isEnabled = enabledModules.GetNamedBoolean(L"Image Resizer", false);
|
||||
if (!isEnabled)
|
||||
{
|
||||
Logger::info(L"ImageResizer module is disabled, skipping AI detection");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get ImageResizer.exe path (located in WinUI3Apps folder)
|
||||
std::wstring imageResizerPath = get_module_folderpath();
|
||||
imageResizerPath += L"\\WinUI3Apps\\PowerToys.ImageResizer.exe";
|
||||
|
||||
if (!std::filesystem::exists(imageResizerPath))
|
||||
{
|
||||
Logger::warn(L"ImageResizer.exe not found at {}, skipping AI detection", imageResizerPath);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger::info(L"Starting AI capability detection via ImageResizer");
|
||||
|
||||
// Call ImageResizer --detect-ai
|
||||
SHELLEXECUTEINFO sei = { sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
||||
sei.lpFile = imageResizerPath.c_str();
|
||||
sei.lpParameters = L"--detect-ai";
|
||||
sei.nShow = SW_HIDE;
|
||||
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
// Wait for detection to complete (with timeout)
|
||||
DWORD waitResult = WaitForSingleObject(sei.hProcess, 30000); // 30 second timeout
|
||||
CloseHandle(sei.hProcess);
|
||||
|
||||
if (waitResult == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"AI capability detection completed successfully");
|
||||
}
|
||||
else if (waitResult == WAIT_TIMEOUT)
|
||||
{
|
||||
Logger::warn(L"AI capability detection timed out");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"AI capability detection wait failed");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Failed to launch ImageResizer for AI detection, error: {}", GetLastError());
|
||||
}
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
Logger::error("Exception during AI capability detection: {}", e.what());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Unknown exception during AI capability detection");
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
inline wil::unique_mutex_nothrow create_msi_mutex()
|
||||
{
|
||||
return createAppMutex(POWERTOYS_MSI_MUTEX_NAME);
|
||||
@@ -127,6 +211,18 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
||||
PeriodicUpdateWorker();
|
||||
} }.detach();
|
||||
|
||||
// Start AI capability detection in background (Windows 11+ only)
|
||||
// AI Super Resolution is not supported on Windows 10
|
||||
// This calls ImageResizer --detect-ai which writes result to cache file
|
||||
if (package::IsWin11OrGreater())
|
||||
{
|
||||
DetectAiCapabilitiesAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"AI capability detection skipped: Windows 10 does not support AI Super Resolution");
|
||||
}
|
||||
|
||||
std::thread{ [] {
|
||||
if (updating::uninstall_previous_msix_version_async().get())
|
||||
{
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ActionRunnerUtils.h" />
|
||||
<ClInclude Include="ai_detection.h" />
|
||||
<ClInclude Include="auto_start_helper.h" />
|
||||
<ClInclude Include="bug_report.h" />
|
||||
<ClInclude Include="centralized_hotkeys.h" />
|
||||
|
||||
@@ -81,6 +81,9 @@
|
||||
<ClInclude Include="ActionRunnerUtils.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ai_detection.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Utils</Filter>
|
||||
</ClInclude>
|
||||
|
||||
Reference in New Issue
Block a user