mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-04 19:36:57 +01:00
Compare commits
22 Commits
dev/mjolle
...
yuleng/ima
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c02956370 | ||
|
|
6c4c29b5eb | ||
|
|
8740ddacc4 | ||
|
|
ac392893a9 | ||
|
|
fb36d9965e | ||
|
|
150e09bedb | ||
|
|
e221a4b80d | ||
|
|
1e228341e3 | ||
|
|
847a7bf0a8 | ||
|
|
131e593c84 | ||
|
|
291723aa28 | ||
|
|
2b126b5b14 | ||
|
|
12d857f895 | ||
|
|
7db4bf83d7 | ||
|
|
5632285383 | ||
|
|
c80517b97b | ||
|
|
6f1af6123d | ||
|
|
36e383a5c8 | ||
|
|
0b90d10507 | ||
|
|
cceee12583 | ||
|
|
f56293763c | ||
|
|
1c73f0c518 |
@@ -8,7 +8,6 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Utilities;
|
||||
@@ -20,8 +19,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 +53,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;
|
||||
@@ -48,23 +71,115 @@ namespace ImageResizer
|
||||
/* 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;
|
||||
}
|
||||
|
||||
// Check AI availability at startup (not relying on cached settings)
|
||||
AiAvailabilityState = CheckAiAvailability();
|
||||
Logger.LogInfo($"AI availability checked at startup: {AiAvailabilityState}");
|
||||
|
||||
// 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)
|
||||
var mainWindow = new MainWindow(new MainViewModel(batch, Settings.Default));
|
||||
mainWindow.Show();
|
||||
Logger.LogInfo("MainWindow shown (unpackaged or activation fallback path).");
|
||||
|
||||
// Temporary workaround for issue #1273
|
||||
WindowHelpers.BringToForeground(new System.Windows.Interop.WindowInteropHelper(mainWindow).Handle);
|
||||
}
|
||||
|
||||
/// <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 - fallback to NoOp
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogWarning("AI Super Resolution service initialization failed. Using fallback.");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error and use NoOp service as fallback
|
||||
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,6 +19,7 @@
|
||||
<RootNamespace>ImageResizer</RootNamespace>
|
||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -46,6 +48,7 @@
|
||||
<Resource Include="Resources\ImageResizer.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="WPF-UI" />
|
||||
|
||||
48
src/modules/imageresizer/ui/Models/AiSize.cs
Normal file
48
src/modules/imageresizer/ui/Models/AiSize.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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;
|
||||
|
||||
[JsonIgnore]
|
||||
public override string Name
|
||||
{
|
||||
get => Resources.Input_AiSuperResolution;
|
||||
set { /* no-op */ }
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
public static ResizeBatch FromCommandLine(TextReader standardInput, string[] args)
|
||||
{
|
||||
var batch = new ResizeBatch();
|
||||
@@ -119,6 +132,9 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
protected virtual void Execute(string file)
|
||||
=> new ResizeOperation(file, DestinationDirectory, Settings.Default).Execute();
|
||||
{
|
||||
var aiService = _aiSuperResolutionService ?? NoOpAiSuperResolutionService.Instance;
|
||||
new ResizeOperation(file, DestinationDirectory, Settings.Default, 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);
|
||||
}
|
||||
|
||||
var originalWidth = source.PixelWidth;
|
||||
var originalHeight = source.PixelHeight;
|
||||
var width = _settings.SelectedSize.GetPixelWidth(originalWidth, source.DpiX);
|
||||
@@ -217,6 +229,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.
|
||||
@@ -323,19 +360,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);
|
||||
|
||||
|
||||
@@ -194,7 +194,187 @@ namespace ImageResizer.Properties {
|
||||
return ResourceManager.GetString("Input_ShrinkOnly.Content", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to AI Super Resolution.
|
||||
/// </summary>
|
||||
public static string Input_AiSuperResolution {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiSuperResolution", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Original size.
|
||||
/// </summary>
|
||||
public static string Input_AiOriginalSizeLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiOriginalSizeLabel", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Enhanced size.
|
||||
/// </summary>
|
||||
public static string Input_AiEnhancedSizeLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiEnhancedSizeLabel", 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 Multiple files selected.
|
||||
/// </summary>
|
||||
public static string Input_AiMultipleFiles {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiMultipleFiles", 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 Upscale to {0} times the original size..
|
||||
/// </summary>
|
||||
public static string Input_AiScaleHelp {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiScaleHelp", 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 Current.
|
||||
/// </summary>
|
||||
public static string Input_AiCurrentLabel {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiCurrentLabel", 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 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 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 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 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 Downloading AI model...
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloading {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloading", 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 Download AI Model.
|
||||
/// </summary>
|
||||
public static string Input_AiModelDownloadButton {
|
||||
get {
|
||||
return ResourceManager.GetString("Input_AiModelDownloadButton", 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 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 scaling operation failed..
|
||||
/// </summary>
|
||||
public static string Error_AiScalingFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_AiScalingFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Large.
|
||||
/// </summary>
|
||||
|
||||
@@ -296,4 +296,52 @@
|
||||
<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>AI 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 AI Model</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>
|
||||
</root>
|
||||
@@ -19,10 +19,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();
|
||||
@@ -50,6 +62,7 @@ namespace ImageResizer.Properties
|
||||
private bool _keepDateModified;
|
||||
private System.Guid _fallbackEncoder;
|
||||
private CustomSize _customSize;
|
||||
private AiSize _aiSize;
|
||||
|
||||
public Settings()
|
||||
{
|
||||
@@ -72,9 +85,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; }
|
||||
|
||||
@@ -94,15 +126,35 @@ 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
|
||||
{
|
||||
return AiSize;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -138,13 +190,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;
|
||||
@@ -163,6 +219,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;
|
||||
@@ -185,12 +253,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);
|
||||
@@ -410,6 +496,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;
|
||||
@@ -475,6 +573,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)
|
||||
@@ -485,6 +584,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();
|
||||
});
|
||||
|
||||
_jsonMutex.ReleaseMutex();
|
||||
|
||||
@@ -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,116 @@ 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 AiScaleDescription => FormatLabeledSize(Resources.Input_AiScaleLabel, AiScaleDisplay);
|
||||
|
||||
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 +183,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 +215,242 @@ 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;
|
||||
var currentValue = hasConcreteSize
|
||||
? FormatDimensions(_originalWidth!.Value, _originalHeight!.Value)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
CurrentResolutionDescription = FormatLabeledSize(Resources.Input_AiCurrentLabel, currentValue);
|
||||
|
||||
var scale = Settings.AiSize.Scale;
|
||||
var newValue = hasConcreteSize
|
||||
? FormatDimensions((long)_originalWidth!.Value * scale, (long)_originalHeight!.Value * scale)
|
||||
: Resources.Input_AiUnknownSize;
|
||||
NewResolutionDescription = FormatLabeledSize(Resources.Input_AiNewLabel, newValue);
|
||||
}
|
||||
|
||||
private static string FormatDimensions(long width, long height)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0} × {1}", width, height);
|
||||
}
|
||||
|
||||
private static string FormatLabeledSize(string label, string value)
|
||||
{
|
||||
return string.Format(CultureInfo.CurrentCulture, "{0}: {1}", label, value);
|
||||
}
|
||||
|
||||
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));
|
||||
OnPropertyChanged(nameof(AiScaleDescription));
|
||||
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,60 +32,140 @@
|
||||
<!-- 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>
|
||||
</DataTemplate>
|
||||
<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>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
<DataTemplate DataType="{x:Type m:CustomSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type m:AiSize}">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock FontWeight="SemiBold" Text="{Binding Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ComboBox.Resources>
|
||||
</ComboBox>
|
||||
|
||||
<!-- AI Configuration Panel -->
|
||||
<Grid Margin="0,8,0,0">
|
||||
<!-- 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>
|
||||
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{Binding AiScaleDescription}" />
|
||||
<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,8,0,0" Visibility="{Binding ShowAiSizeDescriptions, Converter={StaticResource BoolValueConverter}}">
|
||||
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="{Binding CurrentResolutionDescription}" />
|
||||
<TextBlock
|
||||
Margin="0,4,0,0"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{Binding NewResolutionDescription}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -280,7 +377,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}" />
|
||||
|
||||
Reference in New Issue
Block a user